From 23716a260b981f76fe666d87a9b3b2afdf556449 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Fri, 15 May 2026 11:30:56 -0400 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20unbrick=20boot=20=E2=80=94=20revert?= =?UTF-8?q?=20-S6=20Clear(),=20add=20reentrancy=20guard,=20S7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- include/param_prj.h | 7 ++++++- src/main.cpp | 31 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index d35551a..80972a5 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -129,7 +129,12 @@ // SKUDAK customization suffix: bump on every Skudak-side change so the OpenInverter // web UI shows a distinct version (e.g. "4=1.20.R-S4") and we can visually confirm // the right firmware is flashed. Match the stm32-sine -S convention. -#define VERSTR STRINGIFY(4=VER-S5) +// -S6 was withdrawn (PR #8 closed): the canary-rebuild attempt called +// canMap->Clear() inside MapChargerMessages, which recursed through the +// HandleClear callback and stack-overflowed at boot before the terminal +// came up. Jumping straight to -S7 so any bench that still reports -S6 +// in the openinverter UI is visibly the broken build and gets re-flashed. +#define VERSTR STRINGIFY(4=VER-S7) /***** enums ******/ diff --git a/src/main.cpp b/src/main.cpp index 790efe8..1f6ff69 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -108,20 +108,41 @@ static void Ms100Task(void) static void MapChargerMessages() { + // SKUDAK: Reentrancy guard. canMap->Clear() (libopeninv canmap.cpp) ends + // with canHardware->ClearUserMessages(), which fires the HandleClear + // callback below — which calls back into THIS function. If this function + // ever calls Clear() itself, the chain becomes infinite recursion → stack + // overflow → HardFault before the terminal/web UI come up (this is what + // bricked -S6 on Kyle's bench, see PR #8 postmortem). The static guard + // makes any future "accidental Clear() inside the rebuild path" a no-op + // re-entry rather than a chip-bricking fault. Safe because this function + // is only invoked from main() and from the HandleClear callback, both + // single-threaded contexts; never from an ISR. + static bool in_progress = false; + if (in_progress) return; + uint32_t dummyId; uint8_t dummyOfs; int8_t dummyAdd, dummyLen; float dummyGain; bool dummyrx; - //check sample value, if it is mapped assume valid CAN map - if (canMap->FindMap(Param::hwaclim, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return; - - //canMap->Clear(); + // SKUDAK: tmpobcmax is the canary for the -S5+ CAN-map schema. If it's + // already mapped, the persistent flash map is up-to-date — preserve it + // and any user customizations. If it's NOT mapped, the saved map is + // from an older firmware that didn't broadcast OBC temp on 0x211 byte 7. + // + // We deliberately do NOT call canMap->Clear() here. Clear() triggers + // HandleClear, and the recursion is fatal (see guard comment above). The + // upgrade path is: user issues "canclear" via the openinverter terminal + // or web UI to nuke the stale map manually. That single Clear fires + // HandleClear once, this function re-runs additively, and Save() persists. + if (canMap->FindMap(Param::tmpobcmax, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return; + in_progress = true; ChargerCAN::MapMessages(canMap); - canMap->Save(); + in_progress = false; } /** This function is called when the user changes a parameter */ -- 2.52.0 From c905e1236b1807e4ab4526dc68c817d6aa329864 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 18 May 2026 14:07:19 -0400 Subject: [PATCH 2/4] feat: S8/S9 - trim chargercan.cpp + restore c2/c3 RX for multi-module Tesla OBC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- include/param_prj.h | 7 +++- src/chargercan.cpp | 88 ++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index 80972a5..01bd5cd 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -134,7 +134,12 @@ // HandleClear callback and stack-overflowed at boot before the terminal // came up. Jumping straight to -S7 so any bench that still reports -S6 // in the openinverter UI is visibly the broken build and gets re-flashed. -#define VERSTR STRINGIFY(4=VER-S7) +// -S8 trims chargercan.cpp from 73 → 37 entries so the libopeninv 50-slot +// pool doesn't silently drop the SKUDAK 0x211 broadcast on rebuild. +// -S9 restores 8 c2/c3 RX entries (uac/flag/idc/udc per module). -S8 over- +// trimmed multi-module Tesla OBC support: CalcTotals was summing only +// module 1's contribution, so reported pack power was 1/3 of actual delivery. +#define VERSTR STRINGIFY(4=VER-S9) /***** enums ******/ diff --git a/src/chargercan.cpp b/src/chargercan.cpp index 42c3ca2..96e381b 100644 --- a/src/chargercan.cpp +++ b/src/chargercan.cpp @@ -18,47 +18,62 @@ */ #include "chargercan.h" +// SKUDAK-S8/S9: trimmed entry list to fit libopeninv's MAX_ITEMS=50 CANPOS pool. +// The upstream teslacharger registers ~73 AddRecv+AddSend mappings, which +// silently overflows the 50-slot pool and drops the last ~23 entries +// (including the SKUDAK 0x211 broadcast). We dropped things we don't use: +// +// - hwaclim duplicates on 0x209/0x20B (keep just 0x207; the three modules +// broadcast identical hardware AC limit values) -2 +// - c2*/c3* iac, stt, tmp1/2/in (12 entries — full per-module diagnostics +// not needed; CheckChargerFaults only uses uac+flag, CalcTotals only +// uses idc+udc, and tmpobcmax aggregates fine from c1's 3 probes) -12 +// - CHAdeMO RX on 0x102 (canenable/idcspnt/udclim/soc — Polarity VCU +// doesn't use CHAdeMO at all) -4 +// - 0x368 version-only idle frame (purely cosmetic for stock OI UI) -7 +// - CHAdeMO TX on 0x108/0x109 (version, idclim, udc, idc, opmode — +// unused on this build) -5 +// +// Total dropped: 30 entries. From 73 → 42, with 8-slot headroom. +// +// -S9 RESTORED 8 entries the -S8 trim wrongly killed for 3-module Tesla OBCs: +// c2/c3 uac (CheckChargerFaults active2/active3 detection), +// c2/c3 flag (clears CheckAlive timeout under fault-check logic), +// c2/c3 idc (CalcTotals current SUM — was reporting 1/3 of true delivery), +// c2/c3 udc (CalcTotals voltage MAX). Without these the bench reported +// ~2.7kW for what was actually 9.7kW going to the pack. +// +// 0x43c/0x44c TX kept on purpose — they're commands to OBC sub-modules 2/3, +// required for multi-module operation. Drop them too only if pool pressure +// returns and you've confirmed the bench is single-module. void ChargerCAN::MapMessages(CanMap* can) { can->AddRecv(Param::hwaclim, 0x207, 32, 9, 0.06666f); //gain 0.06666 - can->AddRecv(Param::hwaclim, 0x209, 32, 9, 0.06666f); //gain 0.06666 - can->AddRecv(Param::hwaclim, 0x20B, 32, 9, 0.06666f); //gain 0.06666 can->AddRecv(Param::c1iac, 0x207, 41, 9, 0.06666f); //gain 0.06666 - can->AddRecv(Param::c2iac, 0x209, 41, 9, 0.06666f); //gain 0.06666 - can->AddRecv(Param::c3iac, 0x20B, 41, 9, 0.06666f); //gain 0.06666 can->AddRecv(Param::c1uac, 0x207, 8, 8, 1); - can->AddRecv(Param::c2uac, 0x209, 8, 8, 1); - can->AddRecv(Param::c3uac, 0x20B, 8, 8, 1); can->AddRecv(Param::c1flag, 0x207, 17, 2, 1); - can->AddRecv(Param::c2flag, 0x209, 17, 2, 1); - can->AddRecv(Param::c3flag, 0x20B, 17, 2, 1); can->AddRecv(Param::c1stt, 0x217, 0, 8, 1); - can->AddRecv(Param::c2stt, 0x219, 0, 8, 1); - can->AddRecv(Param::c3stt, 0x21B, 0, 8, 1); can->AddRecv(Param::c1idc, 0x227, 32, 16, 0.000839233f); //gain 0.000839233 - can->AddRecv(Param::c2idc, 0x229, 32, 16, 0.000839233f); //gain 0.000839233 - can->AddRecv(Param::c3idc, 0x22B, 32, 16, 0.000839233f); //gain 0.000839233 can->AddRecv(Param::c1udc, 0x227, 16, 16, 0.01052856f); //gain 0.01052856 - can->AddRecv(Param::c2udc, 0x229, 16, 16, 0.01052856f); //gain 0.01052856 - can->AddRecv(Param::c3udc, 0x22B, 16, 16, 0.01052856f); //gain 0.01052856 can->AddRecv(Param::c1tmp1, 0x237, 0, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c2tmp1, 0x239, 0, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c3tmp1, 0x23B, 0, 8, 1, -40); //offset -40°C can->AddRecv(Param::c1tmp2, 0x237, 8, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c2tmp2, 0x239, 8, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c3tmp2, 0x23B, 8, 8, 1, -40); //offset -40°C can->AddRecv(Param::c1tmpin, 0x237, 40, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c2tmpin, 0x239, 40, 8, 1, -40); //offset -40°C - can->AddRecv(Param::c3tmpin, 0x23B, 40, 8, 1, -40); //offset -40°C - //We don't have enough space for all messages, so we discard these - //can->AddRecv(Param::c1tmplim, 0x247, 0, 8, 7); //gain 0.234375 - //can->AddRecv(Param::c2tmplim, 0x249, 0, 8, 7); //gain 0.234375 - //can->AddRecv(Param::c3tmplim, 0x24B, 0, 8, 7); //gain 0.234375 - /***** CHAdeMO RX *****/ - can->AddRecv(Param::canenable, 0x102, 40, 1, 1); - can->AddRecv(Param::idcspnt, 0x102, 24, 8, 1); - can->AddRecv(Param::udclim, 0x102, 8, 16, 1); - can->AddRecv(Param::soc, 0x102, 48, 8, 1); + + // SKUDAK-S9: module 2 RX. uac/flag prevent the CheckChargerFaults false- + // positive that would silently mask module-2 failure (active2 = (chargerena + // & 2) && (c2uac > 70V); without c2uac mapped, active2==false short-circuits + // the flag check). idc/udc are required so CalcTotals reports the SUM + // current and MAX voltage across all three Tesla OBC sub-modules. + can->AddRecv(Param::c2uac, 0x209, 8, 8, 1); + can->AddRecv(Param::c2flag, 0x209, 17, 2, 1); + can->AddRecv(Param::c2idc, 0x229, 32, 16, 0.000839233f); + can->AddRecv(Param::c2udc, 0x229, 16, 16, 0.01052856f); + + // SKUDAK-S9: module 3 RX, same rationale. + can->AddRecv(Param::c3uac, 0x20B, 8, 8, 1); + can->AddRecv(Param::c3flag, 0x20B, 17, 2, 1); + can->AddRecv(Param::c3idc, 0x22B, 32, 16, 0.000839233f); + can->AddRecv(Param::c3udc, 0x22B, 16, 16, 0.01052856f); /***** Charger TX ******/ can->AddSend(Param::udcspnt, 0x45c, 0, 16, 100); //gain 100 @@ -85,21 +100,6 @@ void ChargerCAN::MapMessages(CanMap* can) can->AddSend(Param::aclim, 0x44c, 16, 16, 1500); can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100); - can->AddSend(Param::version, 0x368, 0, 8, 0, 0x03); - can->AddSend(Param::version, 0x368, 8, 8, 0, 0x49); - can->AddSend(Param::version, 0x368, 16, 8, 0, 0x29); - can->AddSend(Param::version, 0x368, 24, 8, 0, 0x11); - can->AddSend(Param::version, 0x368, 40, 8, 0, 0x0c); - can->AddSend(Param::version, 0x368, 48, 8, 0, 0x40); - can->AddSend(Param::version, 0x368, 56, 8, 0, (int8_t)0xff); - - /***** CHAdeMO TX *****/ - can->AddSend(Param::version, 0x108, 8, 16, 107); //output 428V max = 4*107 - can->AddSend(Param::idclim, 0x108, 24, 8, 1); - can->AddSend(Param::udc, 0x109, 8, 16, 1); - can->AddSend(Param::idc, 0x109, 24, 16, 1); - can->AddSend(Param::opmode, 0x109, 40, 3, 5); //Set charging and connlock at once - /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit *****/ // Byte 0: ChargeLimit_pct (0-100). Bytes 1-7 reserved 0x00. // Param::Change(vcuchglim) translates % to udcspnt setpoint (see src/main.cpp). -- 2.52.0 From 4c1f2e19918521ae13e30f751c95c9a95ec0d1e4 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 18 May 2026 14:39:13 -0400 Subject: [PATCH 3/4] feat: S10 - udclim RX on 0x212 bytes 1-2 for VCU-driven force-stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- include/param_prj.h | 7 ++++++- src/chargercan.cpp | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index 01bd5cd..cd565e6 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -139,7 +139,12 @@ // -S9 restores 8 c2/c3 RX entries (uac/flag/idc/udc per module). -S8 over- // trimmed multi-module Tesla OBC support: CalcTotals was summing only // module 1's contribution, so reported pack power was 1/3 of actual delivery. -#define VERSTR STRINGIFY(4=VER-S9) +// -S10 adds udclim RX on 0x212 bytes 1-2. With this, the VCU can force a +// hard stop by sending udclim=200V (below pack), which fires the existing +// ChargerStateMachine CheckVoltage() exit (udc > udclim → STOP). Closes the +// gap where Fix B's contactor-open left the OBC modules drawing ~0.55 kW AC +// indefinitely because EVSEACTIVATE had no other exit path. +#define VERSTR STRINGIFY(4=VER-S10) /***** enums ******/ diff --git a/src/chargercan.cpp b/src/chargercan.cpp index 96e381b..bb90cb4 100644 --- a/src/chargercan.cpp +++ b/src/chargercan.cpp @@ -100,12 +100,24 @@ void ChargerCAN::MapMessages(CanMap* can) can->AddSend(Param::aclim, 0x44c, 16, 16, 1500); can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100); - /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit *****/ - // Byte 0: ChargeLimit_pct (0-100). Bytes 1-7 reserved 0x00. - // Param::Change(vcuchglim) translates % to udcspnt setpoint (see src/main.cpp). - // VCU broadcasts every 200 ms once teslacharger detected; charger holds last - // value if VCU goes silent. + /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit + S10 stop *****/ + // Byte 0: ChargeLimit_pct (0-100). Param::Change(vcuchglim) translates + // % to udcspnt setpoint (see src/main.cpp). + // Bytes 1-2: udclim_V (uint16 LE, 50-420 V). SKUDAK-S10: VCU drives this + // dynamically so it can force-stop the OBC modules when its + // BMS-side SOC gate trips. Normally 398 V (= default udclim, no + // effect). On the VCU's CHARGE_COMPLETE state it drops to 200 V, + // which fires the existing ChargerStateMachine CheckVoltage() + // path (udc > udclim → STOP within 1 s). Pre-S10 firmware + // ignored these bytes; safe upgrade. NOTE: udclim updates here + // are RAM-only — `save` command WHILE the VCU is forcing 200 + // would persist that value and brick subsequent charging. Don't + // issue `save` mid-stop. + // Bytes 3-7: reserved 0x00. + // VCU broadcasts every 200 ms once teslacharger detected; charger holds + // last received values if VCU goes silent (held udclim defaults to 398 V). can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1); + can->AddRecv(Param::udclim, 0x212, 8, 16, 1); /***** SKUDAK VCU telemetry (0x211) — SKUDAK-448 expanded charger telemetry *****/ // Byte 0: state (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop) -- 2.52.0 From d75027b17cea39e00baa5b59431e21b31d31300b Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 18 May 2026 15:39:36 -0400 Subject: [PATCH 4/4] feat: S11 - explicit VCU stop command on 0x212 byte 3 (replaces udclim hack) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- include/param_prj.h | 21 ++++++++++++++------- src/charger.cpp | 19 ++++++++++++++++--- src/chargercan.cpp | 34 +++++++++++++++++++--------------- src/main.cpp | 12 ++++++++++++ 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index cd565e6..bb59a14 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -47,7 +47,7 @@ 3. Display values */ //Next param id (increase when adding new parameter!): 24 -//Next value Id: 2052 +//Next value Id: 2053 /* category name unit min max default id */ #define PARAM_LIST \ PARAM_ENTRY(CAT_CHARGER, idclim, "A", 0, 45, 45, 3 ) \ @@ -109,6 +109,7 @@ VALUE_ENTRY(c3udc, "V", 2035 ) \ VALUE_ENTRY(c3idc, "A", 2036 ) \ VALUE_ENTRY(tmpobcmax, "°C", 2051 ) \ + VALUE_ENTRY(vcustop, OFFON, 2052 ) \ VALUE_ENTRY(test_time, "s", 3000 ) \ VALUE_ENTRY(test_timer_flag, "X", 3001 ) \ VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \ @@ -139,12 +140,18 @@ // -S9 restores 8 c2/c3 RX entries (uac/flag/idc/udc per module). -S8 over- // trimmed multi-module Tesla OBC support: CalcTotals was summing only // module 1's contribution, so reported pack power was 1/3 of actual delivery. -// -S10 adds udclim RX on 0x212 bytes 1-2. With this, the VCU can force a -// hard stop by sending udclim=200V (below pack), which fires the existing -// ChargerStateMachine CheckVoltage() exit (udc > udclim → STOP). Closes the -// gap where Fix B's contactor-open left the OBC modules drawing ~0.55 kW AC -// indefinitely because EVSEACTIVATE had no other exit path. -#define VERSTR STRINGIFY(4=VER-S10) +// -S10 added udclim RX on 0x212 bytes 1-2. The VCU sent udclim=200V to force +// CheckVoltage() to fire (udc > udclim → STOP). DEPRECATED in -S11: the +// per-module DC voltage readings (c1udc/c2udc/c3udc) collapse below 200V +// once VCU opens HV contactors, so the trigger never accumulates 10 ticks. +// udclim RX is kept mapped but the VCU now always sends 398V (no-op). +// +// -S11 adds vcustop RX on 0x212 byte 3 — an explicit VCU command lever that +// mirrors the Dilong path's OBC_ControlCMD. When VCU enters CHARGE_COMPLETE +// it sets vcustop=1; ChargerStateMachine's EVSEACTIVATE case transitions to +// STOP immediately, the J1772 CP signal drops, EVSE stops delivering AC, +// VCU_IsCharging() returns 0, water pumps power down naturally. +#define VERSTR STRINGIFY(4=VER-S11) /***** enums ******/ diff --git a/src/charger.cpp b/src/charger.cpp index b3cd21d..bf676c1 100644 --- a/src/charger.cpp +++ b/src/charger.cpp @@ -353,15 +353,28 @@ void ChargerStateMachine() DigIo::evseact_out.Set(); DigIo::acpres_out.Set(); - if (CheckVoltage() || CheckTimeout()) + // SKUDAK-S11: explicit VCU stop command takes priority over every + // other exit condition. When the VCU's BMS-side gate determines the + // charge session is complete (or the user lowered ChargeLimit below + // current SOC), it sets vcustop=1 on 0x212 byte 3. We obey + // immediately — drop AC pres + EVSE-active GPIOs and transition to + // STOP. Mirrors how the Dilong path's OBC_ControlCMD = Stopped works. + // Replaces the brittle udclim=200V mechanism from -S10. + if (Param::GetInt(Param::vcustop)) + { + DigIo::acpres_out.Clear(); + DigIo::evseact_out.Clear(); state = STOP; - if (CheckUnplugged()) + } + else if (CheckVoltage() || CheckTimeout()) + state = STOP; + else if (CheckUnplugged()) { DigIo::acpres_out.Clear(); DigIo::evseact_out.Clear(); state = OFF; } - if (CheckChargerFaults()) + else if (CheckChargerFaults()) { DigIo::acpres_out.Clear(); state = OFF; diff --git a/src/chargercan.cpp b/src/chargercan.cpp index bb90cb4..884765c 100644 --- a/src/chargercan.cpp +++ b/src/chargercan.cpp @@ -100,24 +100,28 @@ void ChargerCAN::MapMessages(CanMap* can) can->AddSend(Param::aclim, 0x44c, 16, 16, 1500); can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100); - /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit + S10 stop *****/ + /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 chglim + S10 udclim + S11 stop *****/ // Byte 0: ChargeLimit_pct (0-100). Param::Change(vcuchglim) translates // % to udcspnt setpoint (see src/main.cpp). - // Bytes 1-2: udclim_V (uint16 LE, 50-420 V). SKUDAK-S10: VCU drives this - // dynamically so it can force-stop the OBC modules when its - // BMS-side SOC gate trips. Normally 398 V (= default udclim, no - // effect). On the VCU's CHARGE_COMPLETE state it drops to 200 V, - // which fires the existing ChargerStateMachine CheckVoltage() - // path (udc > udclim → STOP within 1 s). Pre-S10 firmware - // ignored these bytes; safe upgrade. NOTE: udclim updates here - // are RAM-only — `save` command WHILE the VCU is forcing 200 - // would persist that value and brick subsequent charging. Don't - // issue `save` mid-stop. - // Bytes 3-7: reserved 0x00. + // Bytes 1-2: udclim_V (uint16 LE). DEPRECATED in -S11 but mapping kept for + // backward compat with -S10 VCU TX. New VCU always sends 398V + // (= the flash default), so this RX is a no-op. Old -S10 trick + // (VCU sends 200V to force CheckVoltage() STOP) was unreliable + // because c1udc/c2udc/c3udc collapse < 200V once VCU opens HV + // contactors, never accumulating 10 ticks > udclim. Defensive + // clamp in Param::Change(udclim) ignores values < 50 V (would + // otherwise turn an old-VCU bench into a never-charges brick). + // Byte 3: vcustop (uint8, NEW in -S11). 0 = no override / normal state + // machine. 1 = VCU commands STOP. Checked at the top of + // EVSEACTIVATE in ChargerStateMachine; transition to STOP fires + // within one 100 ms tick. Mirrors how the Dilong path uses + // OBC_ControlCMD = Stopped to authoritatively halt charging. + // Bytes 4-7: reserved 0x00. // VCU broadcasts every 200 ms once teslacharger detected; charger holds - // last received values if VCU goes silent (held udclim defaults to 398 V). - can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1); - can->AddRecv(Param::udclim, 0x212, 8, 16, 1); + // last received values if VCU goes silent. + can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1); + can->AddRecv(Param::udclim, 0x212, 8, 16, 1); + can->AddRecv(Param::vcustop, 0x212, 24, 8, 1); /***** SKUDAK VCU telemetry (0x211) — SKUDAK-448 expanded charger telemetry *****/ // Byte 0: state (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop) diff --git a/src/main.cpp b/src/main.cpp index 1f6ff69..c393ad9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -161,6 +161,18 @@ void Param::Change(Param::PARAM_NUM paramNum) spnt = MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim)); dcCurController.SetRef(spnt); break; + case Param::udclim: + // SKUDAK-S11: defensive clamp. udclim is now reverse-mapped from CAN + // (chargercan.cpp), and a pre-PR47 VCU sends 0 in 0x212 bytes 1-2. + // udclim=0 would make CheckVoltage() fire on every tick (udc > 0) + // and prevent any charging. If we see a value below the documented + // min (50V), restore the safe default. Trivial cost — fires only on + // CAN RX of udclim (200 ms cadence) and only ever does work when + // the value is bogus. + if (Param::GetInt(Param::udclim) < 50) { + Param::SetInt(Param::udclim, 398); + } + break; case Param::vcuchglim: { // SKUDAK-516: VCU sends user-set ChargeLimit_pct on CAN 0x212. Translate -- 2.52.0