Spark Spark Platform Architecture
← Back to Console
End-to-end system architecture

Spark EV Gateway Platform

From the AVR power stage inside a charger, up through the gateway on the wire, across the MQTT broker and cloud backend, into the database and every operator console — the complete data & control path, built ground‑up.

Charger node grizzle 3.3.1:45 ESP32 gateway 9.1.12‑tls‑eth RPi gateway 2.6.39‑tls HiveMQ Cloud MQTTS 8883 Node backend + Supabase
Telemetry / data — up to cloud
Control / commands — down to field
BLE / offline fallback
live / bidirectional
Chapters
1
Onboarding — BLE pairing
How a phone provisions a brand-new site, no cables, no cloud.

BLE pairing — the phone provisions a gateway

Do this first. Over Bluetooth, the operator picks the site Wi-Fi (scanned on the phone — they only type the password) and the target chargers; the gateway then hops charger-to-charger to hand out the credentials and confirms back. No cables, no cloud. Press play.

PHONE SITE Wi-Fi (inHand) TARGET CHARGERS ×5 scan Wi-Fi BLE · 12345678-… Wi-Fi AP hop Spark Box Ready tap play mobile app inHand Router SSID: inHand-Spark 0 / 5 joined Gateway provisioning mode Charger 1Spark_0001 Charger 2Spark_0002 Charger 3Spark_0003 Charger 4Spark_0004 Charger 5Spark_0005
Step 0 / 7
2
A real site — past, present & future
The same yard as a native OCPP charger, with the Spark gateway, and with LoRa energy.

A real site — press play

Same chargers, same inHand router — but watch what the cloud path looks like before the Spark gateway box (the charger’s stock OCPP link) versus with it. The offline modes are the whole point: native OCPP dies when the internet drops; the gateway keeps charging over BLE.

● GATEWAY ONLINE
FIELD SITE Ethernet GATEWAY BOX MQTT CLOUD Ethernet MQTTS 8883 OTA .bin IoT · broker-direct BLE · AUTH:key Gateway ESP32-S3 / Raspberry Pi ▪ buffering → SD/DB HiveMQ BrokerMQTTS 8883 Cloud VMNode backend SupabasePostgres + Auth Admin Consoleweb · kiosk · mobile Firmware StoreS3 buckets · FTP Spark Box Mobile · IoT/BLE BLE ACTIVE local UI :8080 1 pick a bay → pay & start Kiosk HMI on-site touchscreen · RPi Payment tap · card · app ⚡ dynamic power sharing LoRa RX LoRa · long range Energy Meter+ LoRa TX Site Power Panel NATIVE OCPP · NO GATEWAY BOX OCPP 1.6J · wss (internet) OCPP CSMScentral system (cloud) OCPP Dashboardoperator web OCPP User Appdriver app ✕ No local control — cloud is the only path Charger 1 Charger 2 Charger 3 Charger 4 Charger 5 ESP32 + AVR · OCPP · :80 TP-Link EAP225 Outdoor AP inHand Site router
OCPP online — the charger’s stock cloud path

    Tip: hover any box on the map for details. Switch Past / Present / Future above, and toggle online / offline to see what survives an internet outage.

    3
    The full data path
    The seven-layer signal path — press Play to build it stage by stage.

    The exact seven-layer signal path — one charge session from a charger in the ground to your browser. Hit Play and it builds itself stage by stage. Cyan streams carry telemetry up; red streams carry commands down.

    Stage 0 / 7
    Field · the charger

    EV Charger Node  · Soneil-EV Spark (ChargeLab “grizzle” fork)

    Dual-MCU charger: ESP32 brain + AVR/STM power board

    AVR / STM board

    Pilot & relay control, current setting, live metering (V·A·Wh·W), safety trips (GFCI, ground, over-current/voltage/temp).

    UART · L:<cmd>:<val> · 38400

    ESP32 controller

    WiFi (AP+STA), local HTTP API on port 80, OCPP 1.6J client, logging, config, OTA. Flashes the AVR firmware.

    HTTP :80OCPP 1.6J · wss
    LAN API (open, plaintext): /chargerInfo · /avr_logs · /GetMeterValue · /wifi_status · /charger_activation_state · /logs (text stream) · /Start_charging · /Stop_charging · /set_Current=A · /max_current_limit=A · /back_end_ocpp_url= · /authorize= …

    Two independent control planes: a cloud OCPP link to a CSMS, and a local HTTP path the site gateway drives — so charging keeps working even with no CSMS.

    ▲ poll (4 APIs / cycle) · HTTP GET :80 over LAN · ▼ control
    Site · LAN

    Site router / LAN

    Chargers & gateway are DHCP hosts on the same /24

    The gateway discovers chargers by scanning the subnet (TCP :80 probe → fingerprint /chargerInfo by serial_Number), then caches serial→IP so reboots skip the sweep.

    Ethernet (ESP32-S3) / eth0 (RPi)
    Edge · the gateway

    ESP32-S3 Gateway

    Waveshare ESP32-S3-ETH · W5500 wired

    Bare-metal Arduino/FreeRTOS. Core 0 polls the LAN; Core 1 keeps MQTT alive. TLS via SSLClient/BearSSL. Offline buffer on SD card (JSONL).

    9.1.12-tls-ethDHCPSD bufferBLE

    Raspberry Pi Gateway

    Python asyncio · systemd services

    Same MQTT contract as the ESP32. Polls every 2 s, buffers offline to SQLite (auto-replay at QoS 1), serves a local UI + LAN API, OTA with crash-loop rollback.

    2.6.39-tlseth0SQLite bufferBLE + AP

    Kiosk HMI + Payment

    On-site touchscreen · Raspberry Pi

    Pick a bay, tap to pay, start charging — a local touchscreen wired to the RPi. Works with no internet.

    local UI :8080tap · card · appoffline-capable

    one gateway per site · interchangeable · identical broker contract

    MQTTS · TLS 8883   ▼ telemetry · status · heartbeat · offline_data · response  /  ▲ cmd · ota
    Cloud · transport

    HiveMQ Cloud  

    Managed MQTT broker · TLS 8883 · topic tree sparkcharger/gateway/…

    Every gateway and every client is just another authenticated MQTT client here. Retained status + heartbeat give any newcomer instant fleet state; last-will marks a gateway offline.

    …/telemetry/<serial>/data · /info …/status…/heartbeat …/response…/offline_data …/cmd…/ota
    MQTTS 8883 — one always-on subscriber per client
    Cloud · services & fat clients

    Console Backend · GCP VM

    Node.js · pm2 · Caddy TLS · sparkadmin.duckdns.org

    The only MQTT client for the web console — it holds the broker credentials, keeps one always-on connection (no cold-launch node-numbering race), fans commands/OTA out, and mirrors traffic to the DB. Also runs the auth-key rotation and validates logins via Supabase.

    only broker clientJWT sessionkey rotationWS → browser

    Operator Kiosk · C# WPF

    Direct-to-infra fat client

    Talks straight to the broker (holds creds itself) with three transports and automatic fallback: MQTTBLE-OpAP HTTP.

    MQTT directBLE AUTH:keyAP :8089

    Mobile App  migration planned

    Today: talks straight to the broker · Planned: routed through the backend

    The phone app currently connects to the broker directly (same pattern as the kiosk). The roadmap is to point it at the console backend instead — so broker credentials live in exactly one place and the mobile client becomes a thin, backend-authenticated consumer, just like the web console.

    now: Mobile ⇄ Broker   →   planned: Mobile ⇄ Backend ⇄ Broker
    HTTPS · PostgREST + Auth — backend writes/reads
    Cloud · state

    Supabase  · project “spark-gateway-logs”

    Postgres + Auth — the system of record

    Every MQTT in/out message (except the high-rate telemetry stream) is captured for backtracking; login is validated against Supabase Auth; auth-key issuance and console settings are persisted here.

    mqtt_eventsauth_key_eventsapp_settingsAuth · users
    HTTPS /api · WSS /ws — JWT-gated · ▼ live state / ▲ actions
    Client · operator

    Spark Admin Web Console

    Browser SPA — never touches the broker directly

    Login-gated fleet view & control from anywhere: live gateways/nodes, per-node start/stop/set, gateway reboots & factory resets, OTA rollout, live MQTT log, the Supabase database browser, and BLE auth-key rotation — all through the backend over HTTPS + WebSocket.

    talks only to backendlive WS statefleet controlOTA · logs · DB browser
    4
    Layer by layer
    The facts behind each box — hardware, interfaces, resilience.

    The facts behind each box

    Charger node

    Hardware
    • ESP32 (WiFi AP+STA) + AVR/STM power board, linked over UART with an ASCII L:<cmd>:<val> line protocol at 38400.
    • ESP flashes the AVR from an embedded image; AVR owns relay, pilot, current & safety.
    Interfaces
    • Open HTTP API on port 80 (no auth — LAN-isolated), incl. /logs as a continuous text stream.
    • OCPP 1.6J WebSocket to a CSMS + fully-offline “dumb”/plug-and-charge modes.
    • Identity = serial number (flash partition); AP SSID Spark_<serial> @ 192.168.4.1.

    Gateways (ESP32 & RPi)

    Discovery & poll
    • Subnet TCP :80 scan → fingerprint /chargerInfo; cache serial→IP; ownership/claim model.
    • Per-cycle sweep of /wifi_status · /charger_activation_state · /avr_logs · /GetMeterValue (ESP ~sub-second, RPi 2 s).
    Resilience
    • Offline buffer — ESP SD/JSONL, RPi SQLite — replayed on reconnect via offline_data.
    • BLE operational fallback (AUTH:<key>) + OTA (TLS download, verify, swap, rollback).
    • Site Wi-Fi coverage extended with a TP-Link EAP225-Outdoor (Omada AC1200) outdoor AP so every charger bay stays reachable.

    Broker & backend

    HiveMQ Cloud
    • MQTTS 8883, topic root sparkcharger/gateway/…; retained status/heartbeat + last-will.
    Node backend (GCP VM)
    • Sole broker client for the console; holds creds; always-on so it sees every publish from both gateway types.
    • Fans out commands/OTA, mirrors traffic to Supabase, rotates BLE auth keys, issues JWT sessions.

    Clients

    Web console
    • Browser SPA over HTTPS + WebSocket; never touches the broker — the backend is the only path.
    Kiosk (C# WPF)
    • Direct-to-infrastructure fat client, 3 transports with fallback (MQTT → BLE-Op → AP HTTP).
    Mobile (Flutter)
    • Two transports — IPT (broker-direct) online + BLE offline. Broker-direct today; planned to route through the backend.

    Firmware artifacts (OTA source)

    Where the .bin lives
    • Amazon S3 buckets host the gateway firmware images — ESP32 .bin and RPi .tar.gz release bundles.
    • The charger-node firmware file is served from an FTP server.
    How it's pulled
    • An operator triggers OTA from the console/kiosk → backend publishes {"url":"…"} to …/ota.
    • The gateway downloads directly from S3 over TLS (Amazon Root CA pinned), verifies, flashes, and reports progress on the status topic; the node pulls its image over FTP via /esp_OTA / AvrUpdate.
    5
    Two paths, one system
    Online & offline are first-class — plus the auth-key bridge & LoRa roadmap.

    Online & offline are first-class

    Online path (MQTT)

    1. Gateway polls chargers over LAN HTTP and publishes telemetry/status.
    2. Broker fans it to every subscriber; the backend mirrors it into Supabase.
    3. Console/kiosk render live; operators send commands/OTA back down the same tree.
    charger → gateway → broker → backend → DB → console

    Offline path (BLE / SD / AP)

    1. Broker unreachable → gateway buffers to SD/SQLite and keeps polling.
    2. A phone/kiosk operates it locally over BLE (or WiFi-AP), authorized by the rotating auth key.
    3. On reconnect the gateway replays the buffer (QoS 1) so no history is lost.
    gateway ⇄ SD/SQLite · BLE AUTH:<key> · replay on reconnect

    Auth-key rotation — the bridge between online & offline

    The backend generates a fresh random key per online gateway on an interval, pushes set_auth_key=<key> over MQTT, and records ok/fail in Supabase. That same key is what BLE-Op and the AP HTTP API validate — so when a gateway drops offline, a phone can still operate it locally with the last key it successfully received, until it’s back online and rotates again.

    Roadmap · LoRa energy management & dynamic power sharing

    A LoRa module at the site power panel streams the building’s live energy-meter readings to a LoRa receiver on the gateway. LoRa’s long range covers the whole site — any LoRa meter or sensor is reachable with no extra wiring.

    1. The gateway uses the live readings for dynamic power sharing — allocating charger current against the site / building capacity in real time, so the panel is never overloaded.
    2. Energy packets are appended to the existing MQTT stream, so the same cloud path (broker → VM → Supabase → console) gains full live energy visibility with no new pipeline to build.
    3. The setup ships with a live energy-monitoring view for the operator out of the box.
    4. Future: AI models that adapt charging to live electricity price, demand & available capacity — smart, cost-aware energy management across the whole fleet.
    6
    MQTT topic contract
    One namespace, spoken identically by every ESP32 and Raspberry Pi gateway.
    TopicDirectionRetainedPurpose
    sparkcharger/gateway/statusgw → cloudretainedGlobal fleet roster & online/offline status
    sparkcharger/<mac>/telemetry/<serial>/datagw → cloudPer-charger live telemetry
    sparkcharger/<mac>/telemetry/<serial>/infogw → cloudPer-charger static/identity info
    sparkcharger/<mac>/heartbeatgw → cloudGateway liveness
    sparkcharger/<mac>/responsegw → cloudCommand replies & chunked bodies
    sparkcharger/<mac>/offline_datagw → cloudBuffered SD replay after reconnect
    sparkcharger/<mac>/cmdcloud → gwControl commands (QoS 1)
    sparkcharger/<mac>/otacloud → gwTargeted firmware OTA
    sparkcharger/gateway/otacloud → fleetretainedFleet-wide firmware OTA