Add LVSolarBuck64 firmware and debug console with uv support
STM32G474RB firmware for solar buck converter with MPPT, CC control, Vfly compensation, and adaptive deadtime. Includes Textual TUI debug console for real-time telemetry, parameter tuning, and SQLite logging. Added pyproject.toml for uv: `cd code64 && uv run debug-console` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
233
code64/debug_console/protocol.py
Normal file
233
code64/debug_console/protocol.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""Binary protocol encoder/decoder matching STM32 debug_protocol.h"""
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
SYNC_BYTE = 0xAA
|
||||
HEADER_SIZE = 3
|
||||
|
||||
CMD_TELEMETRY = 0x01
|
||||
CMD_PARAM_WRITE = 0x02
|
||||
CMD_PARAM_WRITE_ACK = 0x03
|
||||
CMD_PARAM_READ_ALL = 0x04
|
||||
CMD_PARAM_VALUE = 0x05
|
||||
CMD_PING = 0x10
|
||||
CMD_PONG = 0x11
|
||||
CMD_ERROR_MSG = 0xE0
|
||||
|
||||
PTYPE_FLOAT = 0
|
||||
PTYPE_UINT16 = 1
|
||||
PTYPE_UINT8 = 2
|
||||
PTYPE_INT32 = 3
|
||||
|
||||
# CRC8 table (poly 0x07)
|
||||
_CRC8_TABLE = [0] * 256
|
||||
def _init_crc8():
|
||||
for i in range(256):
|
||||
crc = i
|
||||
for _ in range(8):
|
||||
if crc & 0x80:
|
||||
crc = ((crc << 1) ^ 0x07) & 0xFF
|
||||
else:
|
||||
crc = (crc << 1) & 0xFF
|
||||
_CRC8_TABLE[i] = crc
|
||||
_init_crc8()
|
||||
|
||||
def crc8(data: bytes) -> int:
|
||||
crc = 0x00
|
||||
for b in data:
|
||||
crc = _CRC8_TABLE[crc ^ b]
|
||||
return crc
|
||||
|
||||
|
||||
@dataclass
|
||||
class TelemetryData:
|
||||
vin: float = 0.0
|
||||
vout: float = 0.0
|
||||
iin: float = 0.0
|
||||
iout: float = 0.0
|
||||
vfly: float = 0.0
|
||||
etemp: float = 0.0
|
||||
last_tmp: int = 0
|
||||
VREF: int = 0
|
||||
vfly_correction: int = 0
|
||||
vfly_integral: float = 0.0
|
||||
vfly_avg_debug: float = 0.0
|
||||
cc_output_f: float = 0.0
|
||||
mppt_iref: float = 0.0
|
||||
mppt_last_vin: float = 0.0
|
||||
mppt_last_iin: float = 0.0
|
||||
p_in: float = 0.0
|
||||
p_out: float = 0.0
|
||||
seq: int = 0
|
||||
|
||||
TELEMETRY_FMT = "<6f hHh h 6f 2f B3x" # 68 bytes
|
||||
TELEMETRY_SIZE = struct.calcsize(TELEMETRY_FMT)
|
||||
|
||||
def decode_telemetry(payload: bytes) -> Optional[TelemetryData]:
|
||||
if len(payload) < TELEMETRY_SIZE:
|
||||
return None
|
||||
vals = struct.unpack(TELEMETRY_FMT, payload[:TELEMETRY_SIZE])
|
||||
return TelemetryData(
|
||||
vin=vals[0], vout=vals[1], iin=vals[2], iout=vals[3],
|
||||
vfly=vals[4], etemp=vals[5],
|
||||
last_tmp=vals[6], VREF=vals[7], vfly_correction=vals[8],
|
||||
# vals[9] is pad
|
||||
vfly_integral=vals[10], vfly_avg_debug=vals[11],
|
||||
cc_output_f=vals[12], mppt_iref=vals[13],
|
||||
mppt_last_vin=vals[14], mppt_last_iin=vals[15],
|
||||
p_in=vals[16], p_out=vals[17],
|
||||
seq=vals[18],
|
||||
)
|
||||
|
||||
|
||||
def build_frame(cmd: int, payload: bytes = b"") -> bytes:
|
||||
header = bytes([SYNC_BYTE, cmd, len(payload)])
|
||||
frame_no_crc = header + payload
|
||||
return frame_no_crc + bytes([crc8(frame_no_crc)])
|
||||
|
||||
|
||||
def build_param_write(param_id: int, param_type: int, value) -> bytes:
|
||||
if param_type == PTYPE_FLOAT:
|
||||
val_bytes = struct.pack("<f", float(value))
|
||||
elif param_type == PTYPE_UINT16:
|
||||
val_bytes = struct.pack("<HH", int(value), 0) # pad to 4 bytes
|
||||
elif param_type == PTYPE_UINT8:
|
||||
val_bytes = struct.pack("<Bxxx", int(value))
|
||||
elif param_type == PTYPE_INT32:
|
||||
val_bytes = struct.pack("<i", int(value))
|
||||
else:
|
||||
val_bytes = struct.pack("<I", int(value))
|
||||
payload = struct.pack("<BBxx", param_id, param_type) + val_bytes
|
||||
return build_frame(CMD_PARAM_WRITE, payload)
|
||||
|
||||
|
||||
def build_ping() -> bytes:
|
||||
return build_frame(CMD_PING)
|
||||
|
||||
|
||||
def build_param_read_all() -> bytes:
|
||||
return build_frame(CMD_PARAM_READ_ALL)
|
||||
|
||||
|
||||
def decode_param_value(payload: bytes) -> Optional[tuple[int, float]]:
|
||||
"""Decode a PARAM_VALUE payload. Returns (param_id, value) or None."""
|
||||
if len(payload) < 8:
|
||||
return None
|
||||
param_id = payload[0]
|
||||
param_type = payload[1]
|
||||
value_bytes = payload[4:8]
|
||||
if param_type == PTYPE_FLOAT:
|
||||
value = struct.unpack("<f", value_bytes)[0]
|
||||
elif param_type == PTYPE_UINT16:
|
||||
value = float(struct.unpack("<H", value_bytes[:2])[0])
|
||||
elif param_type == PTYPE_UINT8:
|
||||
value = float(value_bytes[0])
|
||||
elif param_type == PTYPE_INT32:
|
||||
value = float(struct.unpack("<i", value_bytes)[0])
|
||||
else:
|
||||
value = float(struct.unpack("<I", value_bytes)[0])
|
||||
return (param_id, value)
|
||||
|
||||
|
||||
class FrameParser:
|
||||
"""State machine parser for incoming frames."""
|
||||
|
||||
WAIT_SYNC = 0
|
||||
WAIT_CMD = 1
|
||||
WAIT_LEN = 2
|
||||
WAIT_PAYLOAD = 3
|
||||
WAIT_CRC = 4
|
||||
|
||||
def __init__(self):
|
||||
self.state = self.WAIT_SYNC
|
||||
self.cmd = 0
|
||||
self.length = 0
|
||||
self.buf = bytearray()
|
||||
self.payload = bytearray()
|
||||
self.idx = 0
|
||||
|
||||
def feed(self, data: bytes):
|
||||
"""Feed bytes, yield (cmd, payload) tuples for complete frames."""
|
||||
for b in data:
|
||||
if self.state == self.WAIT_SYNC:
|
||||
if b == SYNC_BYTE:
|
||||
self.buf = bytearray([b])
|
||||
self.state = self.WAIT_CMD
|
||||
elif self.state == self.WAIT_CMD:
|
||||
self.cmd = b
|
||||
self.buf.append(b)
|
||||
self.state = self.WAIT_LEN
|
||||
elif self.state == self.WAIT_LEN:
|
||||
self.length = b
|
||||
self.buf.append(b)
|
||||
self.payload = bytearray()
|
||||
self.idx = 0
|
||||
if b == 0:
|
||||
self.state = self.WAIT_CRC
|
||||
elif b > 128:
|
||||
self.state = self.WAIT_SYNC
|
||||
else:
|
||||
self.state = self.WAIT_PAYLOAD
|
||||
elif self.state == self.WAIT_PAYLOAD:
|
||||
self.payload.append(b)
|
||||
self.buf.append(b)
|
||||
self.idx += 1
|
||||
if self.idx >= self.length:
|
||||
self.state = self.WAIT_CRC
|
||||
elif self.state == self.WAIT_CRC:
|
||||
expected = crc8(bytes(self.buf))
|
||||
self.state = self.WAIT_SYNC
|
||||
if b == expected:
|
||||
yield (self.cmd, bytes(self.payload))
|
||||
|
||||
|
||||
# Parameter registry
|
||||
@dataclass
|
||||
class ParamDef:
|
||||
id: int
|
||||
name: str
|
||||
ptype: int
|
||||
group: str
|
||||
min_val: float = -1e9
|
||||
max_val: float = 1e9
|
||||
fmt: str = ".4f"
|
||||
|
||||
PARAMS = [
|
||||
# Compensator
|
||||
ParamDef(0x25, "VREF", PTYPE_UINT16, "Compensator", 3100, 3700, ".0f"),
|
||||
# Vfly
|
||||
ParamDef(0x20, "vfly_kp", PTYPE_FLOAT, "Vfly", -10, 10, ".4f"),
|
||||
ParamDef(0x21, "vfly_ki", PTYPE_FLOAT, "Vfly", -10, 10, ".6f"),
|
||||
ParamDef(0x22, "vfly_clamp", PTYPE_UINT16, "Vfly", 0, 10000, ".0f"),
|
||||
ParamDef(0x23, "vfly_loop_trig", PTYPE_UINT16, "Vfly", 1, 10000, ".0f"),
|
||||
ParamDef(0x24, "vfly_active", PTYPE_UINT8, "Vfly", 0, 1, ".0f"),
|
||||
# CC
|
||||
ParamDef(0x30, "cc_target", PTYPE_FLOAT, "CC", 0, 60000, ".0f"),
|
||||
ParamDef(0x31, "cc_gain", PTYPE_FLOAT, "CC", -1, 1, ".4f"),
|
||||
ParamDef(0x32, "cc_min_step", PTYPE_FLOAT, "CC", -1000, 0, ".1f"),
|
||||
ParamDef(0x33, "cc_max_step", PTYPE_FLOAT, "CC", 0, 1000, ".1f"),
|
||||
ParamDef(0x34, "cc_loop_trig", PTYPE_UINT16, "CC", 1, 10000, ".0f"),
|
||||
ParamDef(0x35, "cc_active", PTYPE_INT32, "CC", 0, 1, ".0f"),
|
||||
# MPPT
|
||||
ParamDef(0x40, "mppt_step", PTYPE_FLOAT, "MPPT", 0, 10000, ".1f"),
|
||||
ParamDef(0x41, "mppt_iref_min", PTYPE_FLOAT, "MPPT", 0, 60000, ".0f"),
|
||||
ParamDef(0x42, "mppt_iref_max", PTYPE_FLOAT, "MPPT", 0, 60000, ".0f"),
|
||||
ParamDef(0x43, "mppt_dv_thresh", PTYPE_FLOAT, "MPPT", 0, 10000, ".1f"),
|
||||
ParamDef(0x44, "mppt_loop_trig", PTYPE_UINT16, "MPPT", 1, 10000, ".0f"),
|
||||
ParamDef(0x45, "mppt_active", PTYPE_INT32, "MPPT", 0, 1, ".0f"),
|
||||
ParamDef(0x46, "mppt_init_iref", PTYPE_FLOAT, "MPPT", 0, 60000, ".0f"),
|
||||
ParamDef(0x47, "mppt_deadband", PTYPE_FLOAT, "MPPT", 0, 1, ".4f"),
|
||||
# Global
|
||||
ParamDef(0x50, "vin_min_ctrl", PTYPE_FLOAT, "Global", 0, 90000, ".0f"),
|
||||
# Deadtime
|
||||
ParamDef(0x60, "dt 0-3A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
ParamDef(0x61, "dt 3-5A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
ParamDef(0x62, "dt 5-10A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
ParamDef(0x63, "dt 10-20A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
ParamDef(0x64, "dt 20-30A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
ParamDef(0x65, "dt 30-45A", PTYPE_UINT8, "Deadtime", 14, 200, ".0f"),
|
||||
]
|
||||
|
||||
PARAM_BY_ID = {p.id: p for p in PARAMS}
|
||||
PARAM_BY_NAME = {p.name: p for p in PARAMS}
|
||||
Reference in New Issue
Block a user