You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is a maintainer-facing document describing how this firmware
diverges from the reference codebase
(dingo35/SmartEVSE-3.5).
It is intended for contributors deciding whether a behaviour, fix, or
feature originates here or upstream, and for porting changes between
the two.
For end-user feature documentation see Features. For
operational guidance see the audience guides
(installer, owner,
integrator).
Architecture changes
Structural changes that affect the entire codebase — not specific features.
Upstream threshold scales with ActiveEVSE, unreachable for 2+ nodes (commit 94ca08e). Upstream attempted a different fix in 02dafa2 that we evaluated and rejected — it reproduces the multi-node scaling bug and causes stop/start cycling for fixed 3-phase. See analysis.
Solar mode ChargeDelay could expire without solar, causing charging-without-solar oscillation — integrated upstream 74e20c8 (master) + 3ab1cee (node-side) with 3 unit tests
MaxSumMains overwrote per-phase Idifference with a sum-of-phases value, causing current fluctuations when the EU capacity limit was configured — integrated upstream a54b07f (fixes #327) with 3 unit tests
upstream a54b07f
RFID, OCPP & Authorization
Improvement
Why
Details
RFID toggle bug fix
Next RFID swipe toggles OFF instead of ON after Tesla disconnect
WebSocket pings keep transport alive but don't prove backend is processing OCPP messages — integrated upstream ecd088b with timing logic extracted to pure C and 10 unit tests
upstream ecd088b
Atomic connector lock decision
Upstream ocppLoop() briefly flipped OcppForcesLock false→true within one iteration, causing actuator unlock/relock jitter — integrated upstream 05c7fc2 with the decision extracted to pure C and 11 unit tests
upstream 05c7fc2
OCPP Finishing-before-Available sequence
CSMS missed the Finishing state because OccupiedInput went false immediately after StopTx — integrated upstream afd72a8 with the decision extracted to pure C (ocpp_should_report_occupied) and 6 unit tests
upstream afd72a8
Cable disconnect detection under PAUSE
CP sampling timer left disabled after STATE_B single-shot fired — integrated upstream e6110b1 (timerAlarmEnable in STATE_A/STATE_C1 paths)
upstream e6110b1
Public charging station LED scheme
Public-charger color semantics (available/connected/charging/reserved/faulted) selectable via new LedMode menu — integrated upstream 3679fe3 with the decision tree extracted to pure C (led_public_compute()) and 14 unit tests. Fork keeps MENU_LEDMODE=51 to avoid renumber cascade.
Adopts the substance of upstream bd2475a ("get rid of settingsCache because most of NVS's page is already in RAM"). The former 50-field settingsCache struct had to be declared, populated on boot and kept in step by hand, and a duplicated name between a value and its cache field silently disabled change detection for that key.
esp32.cpp
Write scheduling extracted to pure C
Upstream keeps the policy inline in a templated ShadowPreferences class over std::map<String, Entry>. The fork puts the timing decision in nvs_shadow.c so it is unit-testable, and uses a fixed table instead of heap-allocated map entries.
nvs_shadow.c, 10 tests
All NVS writes stay in the main loop
Upstream's reg() opens its own Preferences handle and may write from the web-server task while loop() writes from the main task. The fork keeps marking (a flag) separate from writing (main loop only), so no concurrent NVS handles exist.
esp32.cpp
Forced flush on LCD menu exit and reboot
Completes the second half of upstream b97d97c; a change made in the menu is no longer lost if power is cut before the 60s window closes
write_settings_now()
Monthly capacity peak written only on change
CapPeak / CapMonth were written on every flush, costing a flash write per minute even when the peak had not moved
esp32.cpp
Versioning & Updates
Improvement
Why
Details
Fork-branded CalVer (bm-YYYY.MM.N)
Both projects previously published vX.Y.Z releases, so a version string alone could not tell a user or a maintainer which repository built a device — a recurring source of confusion in support threads. The two codebases are aligned functionally, not commit-by-commit, so a comparable number would imply a lineage that does not exist.
The updater accepts an update only when both the running and candidate versions parse as this scheme, so it can never silently move a device to another distribution's firmware. Cross-distribution flashing remains available manually from the update page.
fw_version.c, 13 tests
Releases via GitHub assets only
This repository has no S3 credentials and never writes to the reference codebase's bucket; the release workflow's dormant S3 upload was removed
Meter type ID divergence. Upstream f92dce8 placed its Chint DDSU666 (single phase) at EMConfig[] slot 17 and renamed slot 15 to EM_CHINT_3P. This fork has used slots 17 and 18 for EM_ORNO3P / EM_ORNO1P since Plan 05, and the meter type is persisted in NVS and exposed over MQTT and /settings. Reusing 17 would silently reinterpret an existing Orno configuration as a Chint one — wrong register map, wrong readings. The fork therefore appends EM_CHINT_1P at slot 20. Consequence: meter type numbers 17, 18 and 20 mean different things in the two firmwares; cross-flashing rewrites the configured meter.
upstream f92dce8
Modbus broadcast timeout handling
Timeout on a broadcast address would advance the request loop and skip the next legitimate slave response — integrated upstream b104576 (1-line guard)
Findings from the security review (see internal report; most issues are inherited from upstream).
Fix
Why
Details
Unsigned firmware upload rejected at POST /update
Upstream accepts firmware.bin / firmware.debug.bin uploads over unauthenticated HTTP with no signature check → any LAN client can flash arbitrary firmware (unauthenticated RCE). Fork accepts only *.signed.bin — verified via the multi-key RSA validator from PR #125
Security fix C-1
OCPP auth_key never exposed via /settings GET
Upstream returns the plaintext OCPP backend basic-auth key to any client calling GET /settings. Fork emits only auth_key_set: bool and the Web UI shows a placeholder
Security fix C-2
MQTT private password redacted from boot log
Upstream logs the full EC-private-key-hash on every boot. Fork logs a 4-char prefix then [redacted]
Upstream pattern leaves the buffer unterminated when source fills it; subsequent %s walks past end
Security fix H-5
OCPP URL validator rejects SSRF targets
Upstream accepts wss://127.0.0.1/, wss://[::1]/, wss://169.254.x/, wss://user:pass@host/. Fork rejects loopback + link-local + embedded credentials. RFC1918 still allowed for self-hosted CSMS
Security fix H-4
Full partition erase on signature failure
Upstream erases only the first ENCRYPTED_BLOCK_SIZE (4 KB) on sig-fail, leaving >4 KB of attacker bytes in flash. Fork erases the entire partition
Security fix M-4
Hash buffer zeroed before free in validate_sig
Hygiene: keeps debug-memory dumps from trivially revealing the firmware fingerprint
Security fix M-3
Opt-in HTTP auth layer (AuthMode NVS setting)
Upstream has no authentication on any HTTP endpoint. Fork adds AuthMode (0=Off legacy default, 1=Required) with a pure-C decision helper, LCD-PIN reuse, 30-min idle session timeout, Origin-based CSRF, and applied to every mutating endpoint + GET /settings. Default 0 on upgrade so no install is bricked. Integrations (HA / custom scripts) continue to work unchanged; security-conscious users explicitly opt in
Plan 16 — Phase 1 — closes C-3, H-1, H-2, H-3, H-7, M-1, M-2, M-8 when enabled
Local patches to vendored third-party code
SmartEVSE-3/src/mongoose.c is a vendored amalgamation, not a managed
dependency, so patches to it are silently reverted by dropping in a newer
copy. Each one is marked in the source with a SMARTEVSE LOCAL PATCH
comment, and make check-mongoose-patches (run in CI) fails if one goes
missing. Re-apply them after any Mongoose update.
Patch
Why
mg_tls_init() (mbedTLS backend) calls mbedtls_ssl_set_hostname() on both TLS paths, not only when a CA is configured
Upstream treats the hostname purely as something to verify against, so it is skipped when verification is off. But SNI is also how a shared frontend selects which certificate to present. ocpp.road.io (Cloudflare) answers a ClientHello with no SNI with handshake_failure (alert 40), so every wss:// OCPP backend URL failed to connect. Worth offering upstream — it is a bug in Mongoose, not a fork preference
Contributing changes to the reference codebase
When porting a change to the reference codebase
(dingo35/SmartEVSE-3.5):
Submit small, focused PRs — the upstream maintainer is
conservative about large changes.
Do not bundle test infrastructure with feature changes — the
reference codebase has a different test approach; bundling slows
review.
Follow the reference codebase's conventions — naming, commit
style, and code organisation differ.
Rewrite the commit message in plain technical English — strip
AI-generated tone (no "Co-Authored-By: Claude" trailers, no
multi-paragraph rationale unless the maintainer asks).
Never modify other-owned repos without explicit user approval.