Skip to main content

Industrial IoT Monitoring Platform

A real-time MQTT pipeline serving live battery telemetry from globally distributed plants. Dynamically subscribes to 100,000+ topics with zero hardcoded schema, delivered sub-second to operators at batterywali.com.

Open live dashboard
  • Backend Architecture
  • Real-time MQTT Pipeline
  • Dynamic Topic Resolution
  • Edge Gateway Integration
batterywali · production
uptime99.98%p95 latency180msregionglobal
MQTT topics
100K+
▲ discovered dynamically
Plants online
12/12
● all healthy
Battery packs
4.8k
▲ +12 today
Messages / sec
32k
● steady
100k+
MQTT topics handled live
12
Plants across the globe
<200ms
End-to-end p95 latency
0
Hardcoded topic names

The problem

Industrial battery fleets generate cell-level telemetry — voltage, temperature, and state-of-charge for every cell in every pack — from plants distributed across continents. The data has to land on an operator screen fast enough to act on, and the system has to keep working as new plants, racks, and packs come online without redeploying the backend every time the topic tree grows.

Hardcoding 100,000+ MQTT topics is not a solution. Neither is exposing the broker directly to the browser. The pipeline needs to discover the schema, isolate tenants, and push updates over a stable contract.

Architecture

Each plant runs a Raspberry Pi acting as the edge gateway — it bridges the on-site PLCs and battery management buses to MQTT, publishing per-cell telemetry over QoS 1 to a central Mosquitto broker. A topic resolver on the backend discovers the topic tree at runtime, normalizes it, and fans out updates to subscribed clients over a thin WebSocket layer.

telemetry-pipeline · live
throughput32k msg/sp95180msleak0
Plants
12 sites · global
Pi gateway
edge · QoS1
Mosquitto
100k+ topics
Topic resolver
dynamic · zero-config
WebSocket / REST
sub-second push
per-cell V · per-cell °C · pack SoC·no hardcoded topic names·tenant-isolated

What it does

  • Dynamic topic discovery
    Backend subscribes with wildcards and infers the (plant/rack/pack/cell) hierarchy from incoming messages. New packs onboard with zero config changes.
  • Sub-second push to UI
    WebSocket fan-out keeps the operator dashboard live; p95 from device publish to pixel update sits under 200ms.
  • Tenant isolation
    Each plant lives in its own topic namespace, ACLs are enforced at the broker, and the API only ever returns rows scoped to the caller. Zero cross-tenant leak.
  • Edge resilience
    Pi gateways buffer locally when the link drops and resume at the broker without duplicates. Operator dashboards reconnect transparently.
  • Schema as data, not code
    Adding a new metric (e.g. internal resistance) is a publish, not a deploy. The frontend renders whatever the resolver maps in.
  • Observable end-to-end
    Every hop has structured logs and counters — broker connections, resolver hits, websocket subscribers, latency histograms.

Per-pack live view

Drilling into a single pack shows per-cell voltage, temperature, and SoC updating in real time. The resolver maps every cell from the raw topic namespace into a stable JSON contract the dashboard renders against.

pack · IN-MH-01 / rack-04 / pack-12
soc87%v52.8Vi-3.1A
#0178%
v3.28
t28°
#0289%
v3.29
t35°
#0382%
v3.31
t33°
#0493%
v3.32
t31°
#0586%
v3.33
t29°
#0679%
v3.34
t36°
#0790%
v3.36
t34°
#0883%
v3.37
t32°
#0994%
v3.38
t30°
#1087%
v3.40
t28°
#1180%
v3.41
t35°
#1291%
v3.42
t33°
#1384%
v3.44
t31°
#1495%
v3.45
t29°
#1588%
v3.28
t36°
#1681%
v3.29
t34°
16 cells · 1Hz · live nominal warm low-v

Plants, globally

A dozen plants spread across continents publish into the same topic tree. Plant identity stays anonymised on the public page; in production each pin maps to a customer-specific tenant.

One broker, every site

Every Pi at every plant talks to the same Mosquitto cluster. Topic namespaces keep tenants isolated; ACLs make it enforceable.

History, on demand

Live telemetry is durable — every sample is persisted so operators can replay yesterday at the same resolution they get today. SoC and pack voltage trends are exposed over the same REST surface the dashboard uses.

trend · 24h · pack IN-MH-01/r04/p12
window24hresolution1h
SoC % Pack V
00:0006:0012:0018:00now

Dynamic topic resolution

The resolver is the load-bearing piece. It subscribes once with a wildcard, derives the hierarchy from each incoming topic, and keeps an in-memory schema that the WebSocket layer reads against. No topic ever appears as a string literal in application code.

resolver/topic.pypython
# topics arrive as: tenants/<tenant>/plants/<plant>/racks/<rack>/packs/<pack>/cells/<n>/<metric>
PATTERN = re.compile(
    r"^tenants/(?P<tenant>[^/]+)"
    r"/plants/(?P<plant>[^/]+)"
    r"/racks/(?P<rack>[^/]+)"
    r"/packs/(?P<pack>[^/]+)"
    r"/cells/(?P<cell>\d+)"
    r"/(?P<metric>[\w_]+)$"
)

def on_message(client, _userdata, msg):
    m = PATTERN.match(msg.topic)
    if not m:
        return
    key = SchemaKey(**m.groupdict())
    schema.upsert(key)              # grow the live topic tree
    fanout.publish(key, msg.payload)  # push to subscribed websockets

Stack, by layer

Every layer has one job and a tight contract with the one above it. Swap Mosquitto for HiveMQ or SQLite for Postgres — nothing else moves.

stack · five layers · one contract
boundaryphysical → pixel
  1. 05layer
    Operator
    Browser dashboard, live updates, drill-down per cell.
    • React
    • WebSockets
    • D3
  2. 04layer
    Serving
    REST snapshots + WebSocket deltas on the same contract.
    • FastAPI
    • Pydantic
    • Uvicorn
  3. 03layer
    Backend
    Dynamic topic resolver, schema discovery, fan-out.
    • Python 3.12
    • paho-mqtt
    • PostgreSQL
    • Redis
  4. 02layer
    Transport
    Central pub/sub with per-tenant ACLs.
    • Mosquitto
    • MQTT 3.1.1
    • QoS 1
  5. 01layer
    Edge
    Raspberry Pi bridges PLCs/BMS to MQTT at each plant.
    • Raspberry Pi OS
    • systemd
    • modbus
    • C/C++ daemon
edge → broker → backend → API → UI·contract stays stable as the topic tree grows

How a sample travels

  • 01 · edge

    Pi gateway at the plant

    Reads the BMS bus, publishes per-cell V/°C/SoC to MQTT at QoS 1. Buffers locally if the uplink drops.

  • 02 · transport

    Mosquitto broker

    Central pub/sub. ACLs scope each tenant to their own namespace; no cross-tenant subscription is possible.

  • 03 · resolver

    Dynamic topic resolver

    Subscribes with a wildcard, derives the schema from incoming topics, persists samples, and fans out to websockets.

  • 04 · serving

    REST + WebSocket API

    WebSocket pushes deltas live, REST serves snapshots and historical windows. Same contract, no broker exposure.

  • 05 · operator

    Live dashboard

    Renders plant / rack / pack / cell views with sub-second updates. Public read at batterywali.com:9000.

Outcome

The platform is live and serving operators across all 12 plants. Onboarding a new pack or even a new metric no longer requires a backend deploy — the resolver picks it up the moment a Pi starts publishing. Zero data leaks, sub-second latency, and a topic surface that has grown more than 5× since launch without a single line of new schema code.