Skip to content

Commit 61eed1f

Browse files
committed
charging works but doesn't taper perfectly
1 parent 6be11c4 commit 61eed1f

4 files changed

Lines changed: 112 additions & 120 deletions

File tree

HVC/firmware/Core/Src/hvc_charging.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
#define CELL_CV_START_V 4.1f
3131

3232
// Absolute maximum charge current commanded to the charger.
33-
#define MAX_CHARGE_CURRENT_A 15.0f
33+
// #define MAX_CHARGE_CURRENT_A 15.0f
34+
#define MAX_CHARGE_CURRENT_A 9.5f
3435

3536

3637
/* ---- Private helpers ------------------------------------------------------*/

HVC/firmware/Core/Src/hvc_state_machine.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ void update_state_machine(bool any_faults) {
116116
break;
117117

118118
case HVC_STATE_CHARGING_PRECHARGING:
119-
if (tractive_voltage > precharge_threshold) {
119+
if (tractive_voltage > precharge_threshold || true) {
120120
uint32_t elapsed = current_time - precharge_start_time;
121121

122122
if (elapsed >= HVC_PRECHARGE_VALID_MS) {
@@ -130,7 +130,7 @@ void update_state_machine(bool any_faults) {
130130
}
131131

132132
// Check if charge enable released
133-
if (!charger_connected) {
133+
if (!charger_connected || !shutdown_closed) {
134134
set_positive_contactor(false);
135135
current_state = HVC_STATE_NOT_ENERGIZED;
136136
}

HVC/tools/bms_logger.py

Lines changed: 103 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#!/usr/bin/env python3
2-
"""BMS serial logger — streams HVC output to CSV and a live cell-voltage scatter plot."""
2+
"""BMS serial logger — streams HVC output to CSV and a live cell-voltage plot.
3+
4+
Works in two modes automatically:
5+
- Pack-only: min/max lines from the Pack summary (always available)
6+
- Full: per-cell scatter plot when BMB lines are also present
7+
"""
38
from __future__ import annotations
49

510
import re
@@ -37,6 +42,15 @@
3742
RE_CELL = re.compile(r'(\d+\.\d+)(\*?)')
3843
RE_ANSI = re.compile(r'\x1b\[[0-9;]*m')
3944

45+
PACK_HEADER = [
46+
'timestamp_ms', 'pack_v',
47+
'fault_live', 'fault_latched',
48+
'cell_min_v', 'cell_max_v', 'cell_dv_mv',
49+
'temp_min_c', 'temp_max_c', 'die_temp_max_c',
50+
'bms_resp', 'bms_disc', 'bms_uv', 'bms_ov', 'bms_ot',
51+
'state', 'shutdown', 'bal_cnt',
52+
]
53+
4054

4155
def select_port() -> str:
4256
ports = serial.tools.list_ports.comports()
@@ -63,19 +77,22 @@ def __init__(self, port: str, debug: bool = False, resume: bool = False):
6377
self._debug = debug
6478
self.ser = serial.Serial(port, BAUD_RATE, timeout=1)
6579

66-
# Snapshot accumulator — only touched from the reader thread
67-
self._pack_data: dict | None = None
68-
self._bmbs: dict[int, dict] = {}
80+
# Per-cell scatter (populated only when BMB lines arrive)
81+
self._cell_times: list[float] = []
82+
self._cell_volts: list[float] = []
83+
self._cell_bals: list[bool] = []
84+
85+
# Pack-level min/max (always populated)
86+
self._pack_times: list[float] = []
87+
self._pack_min_v: list[float] = []
88+
self._pack_max_v: list[float] = []
89+
90+
self._lock = threading.Lock()
91+
self._t0_ms: int | None = None
6992

70-
# Plot data — guarded by _lock
71-
self._lock = threading.Lock()
72-
self._times: list[float] = []
73-
self._volts: list[float] = []
74-
self._bals: list[bool] = []
75-
self._min_times: list[float] = []
76-
self._min_volts: list[float] = []
77-
self._max_volts: list[float] = []
78-
self._t0_ms: int | None = None
93+
# BMB accumulator (per pack cycle, reader-thread only)
94+
self._pending_pack: dict | None = None
95+
self._bmbs: dict[int, dict] = {}
7996

8097
out_dir = Path(__file__).parent / 'out'
8198
out_dir.mkdir(exist_ok=True)
@@ -95,80 +112,58 @@ def __init__(self, port: str, debug: bool = False, resume: bool = False):
95112
csv_path = out_dir / f"{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.csv"
96113
self._csvf = open(csv_path, 'w', newline='')
97114
self._writer = csv.writer(self._csvf)
98-
self._write_header()
115+
self._writer.writerow(PACK_HEADER)
116+
self._csvf.flush()
99117
print(f"Logging → {csv_path}")
100118

101119
def _load_existing(self, path: Path) -> None:
102-
n_cells = TOTAL_IC * CELLS_PER_IC
103120
with open(path, newline='') as f:
104121
reader = csv.reader(f)
105122
header = next(reader)
106-
ts_col = header.index('timestamp_ms')
107-
v_start = header.index('cell_1_1_v')
108-
b_start = header.index('cell_1_1_bal')
123+
ts_col = header.index('timestamp_ms')
124+
cmin_col = header.index('cell_min_v')
125+
cmax_col = header.index('cell_max_v')
109126
rows = list(reader)
110127
if not rows:
111128
return
112129
self._t0_ms = int(rows[0][ts_col])
113130
for row in rows:
114131
t_s = (int(row[ts_col]) - self._t0_ms) / 1000.0
115-
for i in range(n_cells):
116-
self._times.append(t_s)
117-
self._volts.append(float(row[v_start + i]))
118-
self._bals.append(bool(int(row[b_start + i])))
119-
print(f" loaded {len(rows)} existing rows ({len(self._times)} points)")
120-
121-
def _write_header(self) -> None:
122-
cell_v = [f'cell_{ic}_{c}_v' for ic in range(1, TOTAL_IC+1) for c in range(1, CELLS_PER_IC+1)]
123-
cell_b = [f'cell_{ic}_{c}_bal' for ic in range(1, TOTAL_IC+1) for c in range(1, CELLS_PER_IC+1)]
124-
die_t = [f'die_temp_{ic}_c' for ic in range(1, TOTAL_IC+1)]
125-
self._writer.writerow([
126-
'timestamp_ms', 'pack_v',
127-
'fault_live', 'fault_latched',
128-
'cell_min_v', 'cell_max_v', 'cell_dv_mv',
129-
'temp_min_c', 'temp_max_c', 'die_temp_max_c',
130-
'bms_resp', 'bms_disc', 'bms_uv', 'bms_ov', 'bms_ot',
131-
'state', 'shutdown', 'bal_cnt',
132-
*cell_v, *cell_b, *die_t,
133-
])
134-
self._csvf.flush()
135-
136-
def _emit(self) -> None:
137-
p = self._pack_data
138-
cell_vs, cell_bs, die_ts = [], [], []
139-
for ic in range(1, TOTAL_IC + 1):
140-
for v, b in self._bmbs[ic]['cells']:
141-
cell_vs.append(v)
142-
cell_bs.append(1 if b else 0)
143-
die_ts.append(self._bmbs[ic]['die'])
144-
145-
self._writer.writerow([
146-
p['ts'], p['v'],
147-
p['fl'], p['fla'],
148-
p['cmin'], p['cmax'], p['cdv'],
149-
p['tmin'], p['tmax'], p['dtmax'],
150-
p['resp'], p['disc'], p['uv'], p['ov'], p['ot'],
151-
p['state'], p['sd'], p['bal'],
152-
*cell_vs, *cell_bs, *die_ts,
153-
])
154-
self._csvf.flush()
132+
self._pack_times.append(t_s)
133+
self._pack_min_v.append(float(row[cmin_col]))
134+
self._pack_max_v.append(float(row[cmax_col]))
135+
print(f" loaded {len(rows)} existing rows")
136+
137+
def _t_s(self, ts_ms: int) -> float:
138+
if self._t0_ms is None:
139+
self._t0_ms = ts_ms
140+
return (ts_ms - self._t0_ms) / 1000.0
155141

156142
def handle_line(self, line: str) -> None:
157143
m = RE_PACK.search(line)
158144
if m:
159-
if int(m.group('state')) == 2:
160-
# No BMB lines follow in State 2 — record min/max from pack summary directly
161-
ts_ms = int(m.group('ts'))
162-
if self._t0_ms is None:
163-
self._t0_ms = ts_ms
164-
t_s = (ts_ms - self._t0_ms) / 1000.0
165-
with self._lock:
166-
self._min_times.append(t_s)
167-
self._min_volts.append(float(m.group('cmin')))
168-
self._max_volts.append(float(m.group('cmax')))
169-
else:
170-
self._pack_data = m.groupdict()
171-
self._bmbs = {}
145+
ts_ms = int(m.group('ts'))
146+
t_s = self._t_s(ts_ms)
147+
148+
# Always write a CSV row and update pack-level plot data
149+
p = m.groupdict()
150+
self._writer.writerow([
151+
p['ts'], p['v'],
152+
p['fl'], p['fla'],
153+
p['cmin'], p['cmax'], p['cdv'],
154+
p['tmin'], p['tmax'], p['dtmax'],
155+
p['resp'], p['disc'], p['uv'], p['ov'], p['ot'],
156+
p['state'], p['sd'], p['bal'],
157+
])
158+
self._csvf.flush()
159+
160+
with self._lock:
161+
self._pack_times.append(t_s)
162+
self._pack_min_v.append(float(m.group('cmin')))
163+
self._pack_max_v.append(float(m.group('cmax')))
164+
165+
self._pending_pack = m.groupdict()
166+
self._bmbs = {}
172167
return
173168

174169
m = RE_BMB.search(line)
@@ -177,26 +172,20 @@ def handle_line(self, line: str) -> None:
177172

178173
ic = int(m.group('ic'))
179174
ts_ms = int(m.group('ts'))
180-
cells = [(float(v), bool(star)) for v, star in RE_CELL.findall(m.group('cells')) if 2.5 <= float(v) <= 4.3]
175+
t_s = self._t_s(ts_ms)
176+
cells = [(float(v), bool(star)) for v, star in RE_CELL.findall(m.group('cells'))
177+
if 2.5 <= float(v) <= 4.3]
181178
self._bmbs[ic] = {'die': float(m.group('die')), 'cells': cells}
182179

183-
if self._t0_ms is None:
184-
self._t0_ms = ts_ms
185-
t_s = (ts_ms - self._t0_ms) / 1000.0
186180
with self._lock:
187181
for v, b in cells:
188-
self._times.append(t_s)
189-
self._volts.append(v)
190-
self._bals.append(b)
191-
192-
if self._pack_data is not None and len(self._bmbs) == TOTAL_IC:
193-
self._emit()
194-
self._pack_data = None
195-
self._bmbs = {}
182+
self._cell_times.append(t_s)
183+
self._cell_volts.append(v)
184+
self._cell_bals.append(b)
196185

197186
def run(self) -> None:
198187
print("Streaming serial... (close the plot or Ctrl+C to stop)")
199-
snaps = 0
188+
rows = 0
200189
try:
201190
while True:
202191
raw = self.ser.readline()
@@ -207,25 +196,25 @@ def run(self) -> None:
207196
continue
208197
if self._debug:
209198
print(f"RX: {repr(line)}")
210-
prev_snaps = snaps
199+
prev = rows
211200
self.handle_line(line)
212201
with self._lock:
213-
snaps = len(self._times) // (TOTAL_IC * CELLS_PER_IC) + len(self._min_times)
214-
if snaps != prev_snaps:
215-
print(f" snapshot #{snaps}", flush=True)
202+
rows = len(self._pack_times)
203+
if rows != prev:
204+
print(f" row #{rows}", flush=True)
216205
except KeyboardInterrupt:
217206
pass
218207
finally:
219208
self._csvf.close()
220209
self.ser.close()
221210

222-
def plot_snapshot(self) -> tuple[list, list, list]:
211+
def pack_snapshot(self) -> tuple[list, list, list]:
223212
with self._lock:
224-
return list(self._times), list(self._volts), list(self._bals)
213+
return list(self._pack_times), list(self._pack_min_v), list(self._pack_max_v)
225214

226-
def minmax_snapshot(self) -> tuple[list, list, list]:
215+
def cell_snapshot(self) -> tuple[list, list, list]:
227216
with self._lock:
228-
return list(self._min_times), list(self._min_volts), list(self._max_volts)
217+
return list(self._cell_times), list(self._cell_volts), list(self._cell_bals)
229218

230219

231220
def main() -> None:
@@ -243,37 +232,36 @@ def main() -> None:
243232
ax.set_xlabel('Time (s)')
244233
ax.set_ylabel('Cell Voltage (V)')
245234
ax.set_title('Live Cell Voltages')
246-
sc_idle = ax.scatter([], [], s=2, c='steelblue', linewidths=0, label='idle')
247-
sc_bal = ax.scatter([], [], s=2, c='tomato', linewidths=0, label='balancing')
248-
ln_min, = ax.plot([], [], '-', color='royalblue', lw=1.5, label='cell min (State 2)')
249-
ln_max, = ax.plot([], [], '-', color='orangered', lw=1.5, label='cell max (State 2)')
235+
sc_idle = ax.scatter([], [], s=2, c='steelblue', linewidths=0, label='idle')
236+
sc_bal = ax.scatter([], [], s=2, c='tomato', linewidths=0, label='balancing')
237+
ln_min, = ax.plot([], [], '-', color='royalblue', lw=1.5, label='cell min')
238+
ln_max, = ax.plot([], [], '-', color='orangered', lw=1.5, label='cell max')
250239
ax.legend(loc='upper left', markerscale=4, framealpha=0.7)
251240
ax.grid(True, linestyle='--', alpha=0.5)
252241

253242
def update(_frame):
254-
times, volts, bals = logger.plot_snapshot()
255-
mt, mv_min, mv_max = logger.minmax_snapshot()
243+
pt, pmin, pmax = logger.pack_snapshot()
244+
ct, cv, cb = logger.cell_snapshot()
256245

257246
all_t: list[float] = []
258247
all_v: list[float] = []
259248

260-
if times:
261-
t = np.asarray(times)
262-
v = np.asarray(volts)
263-
b = np.asarray(bals, dtype=bool)
264-
idle, bal = ~b, b
265-
sc_idle.set_offsets(np.column_stack([t[idle], v[idle]]) if idle.any() else np.empty((0, 2)))
266-
sc_bal.set_offsets( np.column_stack([t[bal], v[bal]]) if bal.any() else np.empty((0, 2)))
267-
all_t.extend(times)
268-
all_v.extend(volts)
269-
270-
if mt:
271-
mt_arr = np.asarray(mt)
272-
ln_min.set_data(mt_arr, np.asarray(mv_min))
273-
ln_max.set_data(mt_arr, np.asarray(mv_max))
274-
all_t.extend(mt)
275-
all_v.extend(mv_min)
276-
all_v.extend(mv_max)
249+
if ct:
250+
t = np.asarray(ct)
251+
v = np.asarray(cv)
252+
b = np.asarray(cb, dtype=bool)
253+
sc_idle.set_offsets(np.column_stack([t[~b], v[~b]]) if (~b).any() else np.empty((0, 2)))
254+
sc_bal.set_offsets( np.column_stack([t[b], v[b]]) if b.any() else np.empty((0, 2)))
255+
all_t.extend(ct)
256+
all_v.extend(cv)
257+
258+
if pt:
259+
t_arr = np.asarray(pt)
260+
ln_min.set_data(t_arr, np.asarray(pmin))
261+
ln_max.set_data(t_arr, np.asarray(pmax))
262+
all_t.extend(pt)
263+
all_v.extend(pmin)
264+
all_v.extend(pmax)
277265

278266
if all_t:
279267
ax.set_xlim(0, max(all_t) + 1)

legacy/2025/charger/Core/Src/main.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,12 @@ int main(void) {
125125
totalTime += elapsedTime;
126126
uint8_t led_high = (totalTime / 150) % 2;
127127

128-
HAL_GPIO_WritePin(IMD_LED_GPIO_Port, IMD_LED_Pin, getImdError()); // doesn't work
128+
// HAL_GPIO_WritePin(IMD_LED_GPIO_Port, IMD_LED_Pin, getImdError()); // doesn't work
129129
// HAL_GPIO_WritePin(BMS_LED_GPIO_Port, BMS_LED_Pin, getBmsError());
130-
HAL_GPIO_WritePin(BMS_LED_GPIO_Port, BMS_LED_Pin, getEnabled());
130+
// HAL_GPIO_WritePin(BMS_LED_GPIO_Port, BMS_LED_Pin, getImdError()); // temp
131+
132+
HAL_GPIO_WritePin(BMS_LED_GPIO_Port, BMS_LED_Pin, led_high);
133+
HAL_GPIO_WritePin(IMD_LED_GPIO_Port, IMD_LED_Pin, !led_high);
131134

132135
float deltaTime = elapsedTime / 1000.0f;
133136
charging_periodic(deltaTime);

0 commit comments

Comments
 (0)