Promote 0x212 byte 3 from S11's binary vcustop to the full Dilong-style
OBC_ControlCMD vocabulary:
0 = Charging — walk to / hold at EVSEACTIVATE, produce current
1 = Stopped — sit idle in OFF/WAITSTART/ENABLE, no current
2 = Complete — drop to STOP (cycle terminated by VCU)
VCU is now authoritative for the charge-cycle lifecycle. The charger
keeps hardware-level wisdom (EVSE pilot via CheckStartCondition,
plug-out safety via CheckUnplugged, module-fault detection via
CheckChargerFaults) but obeys vcucmd for "should I be charging?".
FSM changes:
- ENABLE→ACTIVATE now gated on vcucmd==Charging (was unconditional).
ENABLE on vcucmd==Complete drops to STOP; vcucmd==Stopped holds
HV-armed waiting for next Charging command.
- EVSEACTIVATE self-decided exits (CheckVoltage, CheckTimeout) are
gone. Those were band-aids for the absence of an authoritative VCU
stop signal. vcucmd==Stopped now parks back in ENABLE (HV held);
vcucmd==Complete drops to STOP. CheckUnplugged + CheckChargerFaults
still own hardware-safety exits.
- STOP→OFF now also triggers on vcucmd != Complete (was unplug-only).
This is the fix for the "raise ChargeLimit mid-COMPLETE doesn't
restart charging" wedge — the VCU's vcucmd flip from Complete to
Stopped on a limit-raise lets the natural OFF→WAITSTART→ENABLE→
ACTIVATE→EVSEACTIVATE flow resume charging without an unplug cycle.
Param rename vcustop→vcucmd. External tooling that reads/writes the
S11 vcustop param via the openinverter web UI will see the new vcucmd
param with 3-state semantics — single-user bench, acceptable.
VERSTR bumped to S12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>