The -S10 udclim=200V mechanism was a hack — VCU lowered udclim below
pack to fire CheckVoltage() udc>udclim and force STOP. It didn't work
reliably because once VCU opens HV contactors, the OBC per-module
voltage probes (c1udc/c2udc/c3udc) collapse below 200V and udc =
MAX(c1,c2,c3) never accumulates 10 ticks above udclim. Bench symptom:
VCU reaches CHARGE_COMPLETE, opens contactors, but teslacharger stays
in EVSEACTIVATE, OBC modules keep drawing ~0.5kW AC, water pumps run,
CP signal stays charging, EVSE doesn't shut down.
S11 adds an explicit vcustop byte on 0x212 byte 3 — VCU sets it to 1
when it wants charging stopped. ChargerStateMachine checks vcustop
FIRST in the EVSEACTIVATE case and transitions to STOP immediately.
Mirrors how the Dilong OBC obeys OBC_ControlCMD = Stopped.
Wire format (additive, no breaking change to S10):
Byte 0: vcuchglim_pct (existing)
Bytes 1-2: udclim_V (existing, DEPRECATED — VCU sends 398V no-op)
Byte 3: vcustop (NEW, 0=normal / 1=force stop)
Bytes 4-7: reserved
Defensive: Param::Change(udclim) now clamps received <50V values to the
398V flash default. Protects against a pre-PR47 VCU sending all-zeros
in bytes 1-2 (which would otherwise make CheckVoltage() fire constantly
and brick charging).
After this VCU also flashed: the WPUMP issue resolves naturally because
VCU_IsCharging() reads OIOBC.State < ACTIVATE once teslacharger goes
to STOP. The VCU/teslacharger ownership becomes symmetric with the
Dilong path — VCU is authoritative for "should we charge."
Pool: 43 → 44 / 50 slots used.
#patch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix B on the VCU side (b24864e) opens HV contactors when BMS reports
cell voltage above the user's ChargeLimit_pct target. That isolates
the pack but doesn't tell the OBC modules to stop drawing AC. Result:
~0.55 kW continuous EVSE draw dissipated as conversion loss in the OBC,
water pumps stay engaged, app reports "Charging <1 kW" indefinitely.
Root cause: ChargerStateMachine's EVSEACTIVATE state has only four
exits (CheckVoltage udc>udclim, CheckTimeout, CheckUnplugged,
CheckChargerFaults). With contactors open the VCU can't drive pack
voltage above udclim, so none of the exits fire — state machine wedges.
Fix: make udclim CAN-driven on 0x212 bytes 1-2 (uint16 LE, range 50-420V).
The VCU normally sends 398 V (no change in behavior). When VCU is in
CHARGE_COMPLETE state it drops to 200 V — well below any realistic
pack voltage — which fires the existing CheckVoltage() path within
1 second (10 × 100 ms ticks) and transitions EVSEACTIVATE → STOP → OFF.
OBC modules disable, AC draw drops to zero.
Elegant because we reuse the existing CheckVoltage() logic instead of
adding new state-machine code, new params, or new exit paths. Pre-S10
firmware silently ignored bytes 1-2; pre-S10 VCU with S10 teslacharger
leaves udclim at its persistent flash default (398V). Safe upgrade in
both directions.
Pool: 42 → 43 / 50 slots used, still comfortable headroom.
Pairs with stm32-hal-vcu PR #47's matching dynamic-udclim TX commit.
CAUTION: udclim updates here are RAM-only; running `save` while VCU
is forcing 200V would persist that value and require canclear-and-
rebuild to recover. Documented in chargercan.cpp comment block.
#patch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
S8 trimmed the 73-entry chargercan.cpp down to 37 entries to fit
libopeninv's 50-slot CANPOS pool — needed because the upstream firmware
silently overflowed the pool and dropped the SKUDAK 0x211 broadcast at
the tail. Without that broadcast the VCU couldn't see idc/udc/tmpobcmax.
S9 restores 8 of the entries S8 dropped: c2/c3 uac, flag, idc, udc.
These turn out to be NECESSARY for 3-module Tesla OBC operation:
- CalcTotals (charger.cpp:41/48) sums c1+c2+c3 idc and takes the max
udc. Without c2/c3 idc mapped, idc reports only module 1's
contribution. Bench observed 2.7 kW reported vs 9.7 kW actual EVSE
draw — exactly the 1/3 ratio expected.
- CheckChargerFaults (charger.cpp:226-261) gates active2/active3
detection on c2uac/c3uac > 70V. Without those mapped, the active
bits short-circuit to false — which masked the c2flag/c3flag
CheckAlive timeout. We were getting away with the trim by accident.
Kept dropped (still need pool headroom for future):
- c2/c3 iac (AC current — c1iac × 3 is a fine proxy, not used in
aggregation anywhere)
- c2/c3 stt (state — all modules report same value for healthy operation)
- c2/c3 tmp1/2/in (temps — tmpobcmax from c1's 3 probes is sufficient;
full 9-probe worst-case is a nice-to-have)
- CHAdeMO RX/TX (0x102/0x108/0x109) — Polarity VCU doesn't use CHAdeMO
- 0x368 idle frame — cosmetic only
- hwaclim duplicates on 0x209/0x20B — all modules report identical limits
Pool math: 42 / 50 slots used, 8-slot headroom for future additions.
VERSTR S8 → S9 so any bench reporting -S8 in the openinverter UI is
visibly the regressed build and needs canclear + reflash.
After flash: user must issue `canclear` from the openinverter Commands
tab so the persisted map gets rebuilt from the new chargercan.cpp.
Power-cycle, verify `can p` shows the 8 new c2/c3 lines, then EVSE
test should report ~9.5 kW in the app (was 2.7 kW).
#patch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #8 (-S6) caused chip boot fault: MapChargerMessages() uncommented
canMap->Clear(). Clear() ends with canHardware->ClearUserMessages(),
which fires the HandleClear callback (main.cpp:170-173) — and
HandleClear calls MapChargerMessages(). Recursive chain:
main() -> MapChargerMessages
-> Clear() -> ClearUserMessages -> HandleClear
-> MapChargerMessages (re-entry)
-> Clear() (recurse...)
-> ... stack overflow -> HardFault
Bench symptom (Kyle): ESP8266 web UI stops responding after flashing
-S6. Recovery required re-flashing an older firmware (3-5 attempts).
This commit:
1. Re-commented canMap->Clear() inside MapChargerMessages — the safe
path. The upgrade rebuild now requires a manual "canclear" via the
openinverter terminal/web UI, which fires HandleClear once and
triggers an additive MapMessages + Save.
2. Added a static `in_progress` reentrancy guard so any future
accidental Clear() inside this function or its callees turns into
a no-op re-entry instead of a chip-bricking fault.
3. Kept the canary as Param::tmpobcmax (from PR #8) — useful as a
schema marker, just no longer auto-triggers a destructive rebuild.
4. Bumped VERSTR S5 -> S7 (skips S6 so any bench reporting "S6" in
the openinverter UI is visibly the broken build and gets flashed).
Note: this does NOT solve the underlying "73 entries trying to fit in
50-slot CANPOS pool" issue — chargercan.cpp still has more AddRecv +
AddSend calls than MAX_ITEMS. Step 2 (reduce entries) is a separate
follow-up PR.
#patch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tesla OBC hardware already publishes 9 temperature probes (3
modules × {tmp1, tmp2, tmpin}) on CAN 0x237/0x239/0x23B which the
teslacharger receives as c[123]tmp[12,in]. Until now none of these
were forwarded downstream — the Polarity VCU's STATUS_SCREEN OBC-temp
and DCDC-temp fields rendered 0 °C on OI-OBC cars.
This commit:
- Adds Param::tmpobcmax — derived value computed each Ms100Task tick
as the max across the 9 probes, skipping any that read -40 °C
(sensor absent / module not present).
- Maps it to 0x211 byte 7 (previously reserved, the last unused byte
of the 7-byte frame). Wire encoding uses the Tesla OBC convention
of offset 40 (wire = °C + 40), matching c[123]tmp* upstream so
receivers can decode with the same formula.
- Bumps VERSTR to -S5 so the right firmware is visually confirmable
in the OpenInverter web UI.
The new byte is purely additive — existing 0x211 consumers that read
only bytes 0-6 continue to work unchanged.
The Polarity VCU's 0x211 RX handler grows a corresponding decode +
display path in a paired stm32-hal-vcu commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/main.cpp:161:16: warning: cannot declare '::main' with a linkage
specification [-Wpedantic]
Long-standing upstream OpenInverter code (predates SKUDAK). C++ forbids
linkage specifications on main; with -pedantic in CPPFLAGS this fires
every build.
Confirmed safe to drop:
- libopencm3 vector.c:35 forward-declares int main(void) with default
linkage and invokes it at boot.
- Linker resolves main by symbol name, not by linkage attribute.
- stm32-sine has the same upstream issue; same fix applies if we ever
push upstream.
Bump VERSTR -S3 -> -S4 so the OI web UI version field shows the new
build. Verified in the built binary as "4=1.20.R-S4".
Build: clean, no compiler warnings (only the long-standing linker RWX
notice remains, unrelated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with stm32-hal-vcu PR #42. Adds a runtime "vcuchglim" param that
the VCU broadcasts every 200 ms; on receipt, the param's Change()
callback translates 20-100% to the charger's udcspnt (DC voltage
setpoint) using a linear cell-voltage ramp.
Without this, the teslacharger had no path for the user's app-set
charge limit to actually limit charging — udcspnt was effectively
a compile-time setpoint.
What's added
------------
- Param::vcuchglim in include/param_prj.h:
PARAM_ENTRY(CAT_CHARGER, vcuchglim, "%", 20, 100, 80, 23)
- Range 20-100 (matches VCU validation)
- Default 80% if VCU never connects
- ID 23 (next free per the comment, bumped to 24)
- Saveable so a flashed charger paired with a silent VCU still
behaves predictably across reboots
- can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1) registration in
ChargerCAN::MapMessages() — libopeninv's CanMap dispatches to the
Change() callback on each received frame.
- Param::Change(vcuchglim) handler in src/main.cpp:
cell_target = 3.30 + (4.15 - 3.30) * (pct - 20) / 80
udcspnt = cell_target * 96
Endpoints chosen to be safe across Tesla LDU / VW MEB / Volt2:
20% → 3.30 V/cell × 96 = 316.8 V (deep-cycle storage floor)
80% → 3.94 V/cell × 96 = 378.0 V (recommended daily ceiling)
100% → 4.15 V/cell × 96 = 398.4 V (matches existing udclim default)
Tuneable here as constants if a particular pack needs a tighter range.
- VER bump 1.19.R → 1.20.R. Combined with the pre-existing -S1 suffix
the OI web UI shows "4=1.20.R-S1", visually distinct from upstream
and from the previous Skudak build.
Build
-----
text=25288, links clean. The "RWX LOAD segment" linker warning is
pre-existing on this template, unrelated. No new warnings from this
change.
Version string in the binary verified as "4=1.20.R-S1".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream openinverter bug surfaced after recent SKUDAK PRs.
Symptom (reported by Kyle on the bench): teslacharger detects EVSE,
shows the AC limit, but stays stuck in WaitStart and never proceeds
to Enable / Activate / Run. State machine looks like it's the OFF
state but it's actually one step further on.
Root cause in src/charger.cpp:85-91:
bool CheckDelay()
{
uint32_t now = rtc_get_counter_val();
uint32_t start = Param::GetInt(Param::timedly) * 60;
return start <= 0 || (now - startTime) > start;
}
With timedly = -1 (the param's documented "no delay" sentinel and its
default value), the math is:
Param::GetInt(timedly) -> -1 (signed int)
-1 * 60 -> -60 (signed)
assigned to uint32_t -> 0xFFFFFFC4 (~4.29 billion)
start <= 0 -> false (uint32_t can't be negative)
(now - startTime) > 4294967236 -> false for ~136 years
So CheckDelay() returns false forever; charger wedges in WaitStart.
Why "it was working before": every SKUDAK PR that bumped the param
table (uaux broadcast, expanded 0x210, 0x210->0x211, version suffix)
invalidates the saved-params CRC and resets timedly to its -1 default.
Kyle had a non-default value (probably 0) saved before.
Fix: signed math for the short-circuit so timedly <= 0 still means
"start immediately." Cast to uint32_t only on the right operand, which
is only evaluated when start > 0 — can't reinterpret negative as huge.
The companion CheckTimeout() at line 75 has the same uint32_t pattern
but works "correctly by accident" because timelim = -1 makes timeout
huge and (now - startTime) > timeout is false, which matches the
intended "no timeout" semantics. Leaving that alone — fixing it would
change behaviour, not bugs.
VER suffix bumped -S1 -> -S2 so the OI web UI clearly shows the
deployed firmware (post-fix). Confirmed in built binary as
"4=1.19.R-S2".
Build: text=25192, links clean, pre-existing pedantic warning unchanged.
This bug is also present upstream (johanneshuebner/stm32-charger);
worth a PR upstream once we've confirmed the fix on the bench.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The OpenInverter web UI displays the firmware version from the 'version'
parameter (entry 2001), which expands VERSTR. Out of the box this reads
'4=1.19.R' (upstream version, no Skudak indicator). With this change it
reads '4=1.19.R-S1' so we can visually confirm we're running the Skudak
fork and not stock OpenInverter firmware.
Matches the convention already used in stm32-sine ('-sine-S6' / '-foc-S6')
where the -S<N> suffix bumps on each Skudak-side change.
Verified: 'strings stm32_charger.bin | grep S1' shows '4=1.19.R-S1' in the
built artifact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bench capture on the green car showed a non-Polarity module also
broadcasting at 0x210 (DLC=8 frame with mostly-zero payload, byte 5 = 0x85
static — likely a Tesla OEM DI module). Even after a clean reflash + 'can c'
to clear persisted maps, the rogue 0x210 frame keeps appearing on the wire,
racing our intended frame and intermittently corrupting the VCU's read.
Confirmed via 'can p' on the OBC that this firmware's CanMap has exactly
one 0x210 entry with the 5 expected fields — the conflict is external.
Moves all five AddSends to 0x211, an adjacent free ID. The VCU's
CHARGER_CAN_ID_OI_OBC_AUX constant is updated to match in a paired
stm32-hal-vcu PR. Same 7-byte expanded layout (state/udc/idc/uaux@5/soc),
same 100ms cadence — only the arbitration ID changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single uaux-at-byte-0 broadcast (commit d209a47) with a
7-byte expanded telemetry frame. Bench capture on green car showed dual
0x210 frames colliding (DLC=8 simple + DLC=7 expanded) because Kyle's
flashed image had both AddSend sets active; standardising on the
expanded layout eliminates that ambiguity.
Byte layout:
byte 0 state (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop)
bytes 1-2 udc (V, gain=1)
bytes 3-4 idc (0.1A, gain=10)
byte 5 uaux (decivolts, gain=10) — 12V aux battery rail
byte 6 soc (%, gain=1)
byte 7 reserved
Consumed by stm32-hal-vcu Core/Src/main.c CHARGER_CAN_ID_OI_OBC_AUX
handler. The state byte enables future VCU_IsCharging integration via
state == 4 (Run) without requiring a second CAN ID.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds firmware on PR/push to main. Initializes libopencm3 and
libopeninv submodules then runs make. Uploads stm32_charger.bin
as a 30-day retained artifact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds AddSend for Param::uaux at CAN ID 0x210, bit 0, length 8, gain=10.
Transmits 12V aux battery voltage in decivolts/0.1V steps (e.g. 125 = 12.5V).
VCU reads byte 0 of 0x210 for 12V battery protection logic per SKUDAK-448.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>