Repository navigation
fix!: write settings at byte-direct addresses, not word indices - #33
Merged
Merged
Conversation
RegisterDef.address is a word index into the settings block (base + spec_byte/2), which is exactly what decode_register_value needs to slice a bulk read starting at byte 0. write_executor passed that value straight to write_registers as a wire address, so every numeric write landed at half the intended byte offset - inside a different parameter, frequently a safety threshold. max_charge_current wrote into balance_trigger_voltage. Add jk_settings.write_address() (block base + spec byte offset) and use it at the single wire site. Every read path keeps using .address unchanged, because its word-index meaning is already correct there. Also fall back from FC06 to a one-register FC16 write. PB2A16S20P firmware 15.41 rejects FC06 against the packed-bit register 0x1114 with illegal-data-address, although FC03 reads that register and FC16 writes it - so packed-bit writes never reached the BMS at all. Verified on real hardware (BMS 1, JK_PB2A16S20P fw 15.41): all 32 numeric parameters and 3 packed bits written, read back, checked against the whole 120-word window for side effects, and restored. The shipped write_executor path was then driven end to end on the same pack. Evidence in scripts/captures/hw_evidence/, runbook in docs/HW_TESTBENCH.md. Adds scripts/hw_testbench.py, the harness that produced that evidence, with its own address arithmetic kept deliberately independent of production so it validates rather than inherits. Refs #32 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The evidence README described pack 1's 3.50 V cell over-voltage recovery as a standing difference from packs 2-5. It was aligned to 3.58 V at the owner's request, leaving pack 6's 360 Ah capacity as the bank's only remaining intentional difference. The baseline JSONs deliberately keep the original as-found state - that is what makes them a valid restore source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pack 1's cell over-voltage recovery was raised from 3.50 V to 3.58 V to match packs 2-5. Re-read the pack and stored that capture rather than editing the values in place: these files are hardware captures, so a hand-edited one would assert a reading that never happened. Pack 1 is now byte-identical to pack 2 across the whole settings window. The original as-found capture is kept locally in the gitignored hw_snapshots/ directory as the restore source for the old value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #32.
The bug
RegisterDef.addresscarries two meanings. For reads it is a word index into the settings block (base + spec_byte/2) — exactly whatdecode_register_valueneeds to slice a bulk read that starts at byte 0, so every read path is correct today. For writes,write_executorpassed that same value towrite_registers()as a literal Modbus address.The firmware addresses this block by byte offset, so every numeric write landed at half the intended offset — inside a different parameter:
max_charge_current0x1016balance_trigger_voltagemax_discharge_current0x101ccell_soc0_voltagebalance_starting_voltage0x1042discharge_overcurrent_protection_recovery_timeOnly the parameter at byte 0 was unaffected, because 0 halves to itself. Several of the victims are safety thresholds, so with basic writes enabled the bridge silently rewrote protection limits.
scripts/hw_testbench.py --collision-reportprints the full table with no hardware attached.The fix
Add
jk_settings.write_address(reg)returningbase + spec_byte, and use it at the one wire site. Read paths are untouched — their word-index semantics are already right, and they are covered by an integration test that decodes a real capture.This deliberately does not rewrite the table to byte offsets as the issue proposed. That meaning is load-bearing for
decode_register_value,bms_runner, both dump scripts and four test modules; changing it would touch every read consumer to fix one line.Second fix:
_safe_write_registernow falls back from FC06 to a one-register FC16 write. PB2A16S20P firmware 15.41 rejects FC06 against the packed-bit register0x1114with illegal-data-address, although FC03 reads that register and FC16 writes it. Packed-bit writes were never reaching the BMS.Evidence
Three independent sources agreed on the rule before any hardware was touched:
BMS_1_sweep.txt— a read requested at0x1478returns the bytes at offset0x78of the0x1400window, so start address = base + byte offset.flows.json, nodefunction 4— FC06 to0x1504for RCVTime, whose spec byte offset in the0x1400block is0x104.docs/FIELD_MATRIX.md— phinix writes0x1070/0x1074/0x1078for the switches at spec bytes0x70/0x74/0x78.Then on real hardware (BMS 1,
JK_PB2A16S20Pfw 15.41, pack disconnected):write_executorpath driven end to end on the same pack: numeric at wire0x1020, packed bit0x3200 → 0x3240with siblings preserved, no/errortopics, pack restored.Raw evidence in
scripts/captures/hw_evidence/.Two firmware quirks found, neither an addressing fault
max_discharge_current(200.0 A) andmax_balance_current(2.0 A) sat exactly at their firmware limits, so perturbing upward returned illegal-data-value. ThePB2Ain the model name is a 2 A balancer. The bench now retries downward.Also in this PR
scripts/hw_testbench.py— read / perturb / verify / side-effect-check / restore bench, plusdocs/HW_TESTBENCH.md. Read-only by default; writes need--write --confirm-writes; a pack-current interlock refuses to write while current flows; a snapshot is written before the first write; restore replays the captured raw words rather than re-encoding; a failed restore aborts the run.It computes addresses independently of production on purpose, so it validates the fix rather than inheriting it. A test pins the two implementations together.
The bench never writes at the un-corrected address — that lands on an unidentified register, so the bug is demonstrated arithmetically instead.
Checks
ruff clean · mypy strict clean (13 files) · 651 tests · 100% branch coverage
🤖 Generated with Claude Code