Files
onefinity-firmware/src/py/bbctrl/Comm.py
Henrik Muehe 561d2fd7ea Restart timing: bbctrl.Trace, /api/diag/timing, UI marks
Add a lightweight, self-contained phase tracer for measuring end-to-end
bbctrl restart and Pi boot time. Disabled by setting BBCTRL_TRACE=0.

- src/py/bbctrl/Trace.py: monotonic-anchored event log + sd_notify helper.
- bbctrl/__init__.py: marks for imports, args parsed, ioloop, web init,
  listen, and an sd_notify READY=1 once HTTP is bound.
- bbctrl/Ctrl.py: spans around each subsystem (avr, i2c, lcd, mach,
  preplanner, jog, pwr, hooks, aux, mach.connect).
- bbctrl/Comm.py: avr.firmware_rebooted mark.
- bbctrl/Web.py: TimingHandler (GET /api/diag/timing) and
  UITimingHandler (PUT /api/diag/timing/ui), plus a ws.first_open mark.
- src/js/restart-timing.js + app.js: UI-side performance.now() marks
  (script.load, ws.open, ws.first_msg, ui.first_state, window.load),
  posted once to the controller.
- scripts/bbctrl.service: stdout/stderr -> journal so TRACE lines are
  visible via journalctl -u bbctrl. (Was StandardOutput=null.)

Revert: git revert this commit. To disable at runtime without
reverting, set BBCTRL_TRACE=0 in the bbctrl service environment.
2026-05-01 09:48:10 +02:00

261 lines
9.0 KiB
Python

################################################################################
# #
# This file is part of the Buildbotics firmware. #
# #
# Copyright (c) 2015 - 2018, Buildbotics LLC #
# All rights reserved. #
# #
# This file ("the software") is free software: you can redistribute it #
# and/or modify it under the terms of the GNU General Public License, #
# version 2 as published by the Free Software Foundation. You should #
# have received a copy of the GNU General Public License, version 2 #
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
# #
# The software is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with the software. If not, see #
# <http://www.gnu.org/licenses/>. #
# #
# For information regarding this software email: #
# "Joseph Coffland" <joseph@buildbotics.com> #
# #
################################################################################
import serial
import json
import time
import traceback
from collections import deque
import bbctrl
import bbctrl.Cmd as Cmd
# Must be kept in sync with drv8711.h
DRV8711_STATUS_OTS_bm = 1 << 0
DRV8711_STATUS_AOCP_bm = 1 << 1
DRV8711_STATUS_BOCP_bm = 1 << 2
DRV8711_STATUS_APDF_bm = 1 << 3
DRV8711_STATUS_BPDF_bm = 1 << 4
DRV8711_STATUS_UVLO_bm = 1 << 5
DRV8711_STATUS_STD_bm = 1 << 6
DRV8711_STATUS_STDLAT_bm = 1 << 7
DRV8711_COMM_ERROR_bm = 1 << 8
# Ignoring stall and stall latch flags for now
DRV8711_MASK = ~(DRV8711_STATUS_STD_bm | DRV8711_STATUS_STDLAT_bm)
def _driver_flags_to_string(flags):
if DRV8711_STATUS_OTS_bm & flags: yield 'over temp'
if DRV8711_STATUS_AOCP_bm & flags: yield 'over current a'
if DRV8711_STATUS_BOCP_bm & flags: yield 'over current b'
if DRV8711_STATUS_APDF_bm & flags: yield 'driver fault a'
if DRV8711_STATUS_BPDF_bm & flags: yield 'driver fault b'
if DRV8711_STATUS_UVLO_bm & flags: yield 'undervoltage'
if DRV8711_STATUS_STD_bm & flags: yield 'stall'
if DRV8711_STATUS_STDLAT_bm & flags: yield 'stall latch'
if DRV8711_COMM_ERROR_bm & flags: yield 'comm error'
def driver_flags_to_string(flags):
return ', '.join(_driver_flags_to_string(flags))
class Comm(object):
def __init__(self, ctrl, avr):
self.ctrl = ctrl
self.avr = avr
self.log = self.ctrl.log.get('Comm')
self.queue = deque()
self.in_buf = ''
self.command = None
self.last_motor_flags = [0] * 4
avr.set_handlers(self._read, self._write)
self._poll_cb(False)
def comm_next(self): raise Exception('Not implemented')
def comm_error(self): raise Exception('Not implemented')
def is_active(self): return len(self.queue) or self.command is not None
def i2c_command(self, cmd, byte = None, word = None, block = None):
self.log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block))
self.avr.i2c_command(cmd, byte, word, block)
def flush(self): self.avr.enable_write(True)
def _load_next_command(self, cmd):
self.log.info('< ' + json.dumps(cmd).strip('"'))
self.command = bytes(cmd.strip() + '\n', 'utf-8')
def resume(self): self.queue_command(Cmd.RESUME)
def queue_command(self, cmd):
self.queue.append(cmd)
self.flush()
def _poll_cb(self, now = True):
# Checks periodically for new commands from planner via comm_next()
if now: self.flush()
self.ctrl.ioloop.call_later(1, self._poll_cb)
def _write(self, write_cb):
# Finish writing current command
if self.command is not None:
try:
count = write_cb(self.command)
except Exception as e:
self.command = None
raise e
self.command = self.command[count:]
if len(self.command): return # There's more
self.command = None
# Load next command from queue
if len(self.queue): self._load_next_command(self.queue.popleft())
# Load next command from callback
else:
cmd = self.comm_next() # pylint: disable=assignment-from-no-return
if cmd is None: self.avr.enable_write(False) # Stop writing
else: self._load_next_command(cmd)
def _update_vars(self, msg):
try:
self.ctrl.state.set_machine_vars(msg['variables'])
self.ctrl.configure()
self.queue_command(Cmd.DUMP) # Refresh all vars
# Set axis positions
for axis in 'xyzabc':
position = self.ctrl.state.get(axis + 'p', 0)
self.queue_command(Cmd.set_axis(axis, position))
except Exception as e:
self.log.warning('AVR reload failed: %s', traceback.format_exc())
self.ctrl.ioloop.call_later(1, self.connect)
def _log_msg(self, msg):
level = msg.get('level', 'info')
where = msg.get('where')
msg = msg['msg']
if level == 'info': self.log.info(msg, where = where)
elif level == 'debug': self.log.debug(msg, where = where)
elif level == 'warning': self.log.warning(msg, where = where)
elif level == 'error': self.log.error(msg, where = where)
if level == 'error': self.comm_error()
# Treat machine alarmed warning as an error
if level == 'warning' and 'code' in msg and msg['code'] == 11:
self.comm_error()
def _log_motor_flags(self, update):
for motor in range(3):
var = '%ddf' % motor
if var in update:
flags = update[var] & DRV8711_MASK
if self.last_motor_flags[motor] == flags: continue
self.last_motor_flags[motor] = flags
flags = driver_flags_to_string(flags)
self.log.info('Motor %d flags: %s' % (motor, flags))
def _update_state(self, update):
self.ctrl.state.update(update)
if 'xx' in update: # State change
self.ctrl.ready() # We've received data from AVR
self.flush() # May have more data to send now
self._log_motor_flags(update)
def _read(self, data):
self.in_buf += data.decode('utf-8')
# Parse incoming serial data into lines
while True:
i = self.in_buf.find('\n')
if i == -1: break
line = self.in_buf[0:i].strip()
self.in_buf = self.in_buf[i + 1:]
if line:
self.log.info('> ' + line)
try:
msg = json.loads(line)
except Exception as e:
self.log.warning('%s, data: %s', e, line)
continue
if 'variables' in msg:
self._update_vars(msg)
elif 'msg' in msg:
self._log_msg(msg)
self.ctrl.mach.process_log(msg)
elif 'firmware' in msg:
self.log.info('AVR firmware rebooted')
try:
import bbctrl.Trace as _T
_T.mark('avr.firmware_rebooted')
except Exception: pass
self.connect()
else:
self._update_state(msg)
def estop(self):
if self.ctrl.state.get('xx', '') != 'ESTOPPED':
self.i2c_command(Cmd.ESTOP)
def clear(self):
if self.ctrl.state.get('xx', '') == 'ESTOPPED':
self.i2c_command(Cmd.CLEAR)
def pause(self):
self.i2c_command(Cmd.PAUSE, byte = ord('0')) # User pause
def reboot(self): self.queue_command(Cmd.REBOOT)
def connect(self):
try:
# Resume once current queue of GCode commands has flushed
self.queue_command(Cmd.RESUME)
self.queue_command(Cmd.HELP) # Load AVR commands and variables
except Exception as e:
self.log.warning('Connect failed: %s', e)
self.ctrl.ioloop.call_later(1, self.connect)