Export the BMU-v2-48v motherboard fab set after the routing review (DRC at production baseline, 0 unconnected): - 11 gerbers (Protel ext, UTC dates, soldermask-subtracted) + Excellon PTH/NPTH drills with PDF maps. - BOM 32 lines: LCSC refs merged from the schematic fields (LCSC Part/LCSC/LCSC Alt) plus known basics (INA237 C2864837, 1N4148W, 0603 R/C, LEDs); 12 lines left blank to match at order time (LM5164, TCA9535, L1, precision R, big caps). - CPL 86 top-side placements (TH connectors, solder jumpers, test points excluded). - D5/D9 marked DNP in the root schematic: orphans of the removed MCP16331 circuit (anode dangling), excluded from BOM/CPL.
KXKM Batterie Parallelator
Battery Management Unit for parallel 24-30V packs
Up to 32 batteries · ESP32-S3 · BLE + MQTT + InfluxDB · iOS companion app
Overview
The KXKM Batterie Parallelator is an embedded Battery Management Unit (BMU) that safely parallels multiple battery packs (24-30V) for off-grid stage installations. Each battery is individually monitored (voltage, current, temperature) and can be disconnected in microseconds via MOSFET switches.
Built by L'Electron Rare for KompleX KapharnaüM (Villeurbanne, France) — a live arts company deploying digital scenography in public spaces without grid power.
Key Features
| Feature | Description |
|---|---|
| Per-battery protection | Under/over-voltage, over-current, imbalance detection |
| Auto-reconnection | Exponential backoff with permanent lock after 5 faults |
| Soft-balancing | Duty-cycling with opportunistic R_int measurement |
| Victron integration | BLE Instant Readout + GATT SmartShunt emulation + device scanner |
| Touchscreen UI | 320x240 LVGL display with battery grid, SOH, charts |
| iOS companion app | BLE real-time monitoring, offline cache, role-based access |
| Cloud telemetry | MQTT + InfluxDB + Grafana with offline persistence |
| OTA updates | Dual-partition with automatic rollback |
Quick Start
# ESP-IDF firmware (production)
cd firmware-idf
source ~/esp/esp-idf/export.sh
idf.py build
idf.py -p /dev/cu.usbmodem* flash monitor
# PlatformIO (legacy Arduino)
pio run -e kxkm-s3-16MB --target upload
# Run all tests (no hardware needed)
pio test -e sim-host # 13 PlatformIO tests
cd firmware-idf && idf.py build # 13 ESP-IDF host tests
# Cloud stack (kxkm-ai server)
cd kxkm-api && cp .env.example .env && docker compose up -d
Architecture
┌──────────────────────────┐
│ ESP32-S3-BOX-3 │
│ 320x240 LVGL display │
│ NimBLE + WiFi │
└─────────┬────────────────┘
│ I2C 50kHz
┌───────────────┼───────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ TCA9535 │ │ TCA9535 │ │ ...×8 │
│ 4× MOSFET │ │ 4× LED │ │ │
└─────┬─────┘ └───────────┘ └───────────┘
│
┌─────▼─────┐
│ INA237 ×4 │ ← per TCA (voltage + current)
└───────────┘
┌──────────────────────────────────────────────────────┐
│ Cloud Stack │
│ BMU ──MQTT──► Mosquitto ──► Telegraf ──► InfluxDB │
│ │ │
│ iPhone ◄──BLE──► BMU Grafana ◄───────────┘ │
│ iPhone ◄──REST──► FastAPI ◄─────────────────┘ │
└──────────────────────────────────────────────────────┘
Firmware Components (ESP-IDF, 25 modules)
| Component | Role |
|---|---|
bmu_protection |
State machine: 5 states, 5 criteria, permanent lock |
bmu_balancer |
Soft-balancing duty-cycling + R_int measurement |
bmu_ina237 |
16× INA237 power monitors (2mΩ shunt) |
bmu_tca9535 |
8× GPIO expanders (switches + LEDs) |
bmu_ble |
NimBLE 4 GATT services + Victron SmartShunt |
bmu_ble_victron |
Victron Instant Readout (AES-CTR encrypted ads) |
bmu_ble_victron_scan |
BLE central scanner for Victron devices |
bmu_display |
LVGL: battery grid, pack info, SOH, charts, swipe |
bmu_influx + _store |
InfluxDB client + offline FAT/SD persistence |
bmu_mqtt |
ESP-MQTT with auth credentials |
bmu_vedirect |
Victron VE.Direct UART parser (solar charger) |
bmu_rint |
Internal resistance pulse measurement |
bmu_web |
HTTP + WebSocket with token auth + rate limiting |
bmu_config |
NVS runtime config + Victron device keys |
bmu_storage |
NVS, FAT, SPIFFS, SD card, USB MSC |
bmu_ota |
OTA firmware update with rollback |
Protection Logic
Nb_switch < 5 → Reconnect immediately when condition clears
Nb_switch == 5 → 10-second delay before reconnection
Nb_switch > 5 → Permanent lock (blinking red LED) until reboot
| Threshold | Default | Configurable |
|---|---|---|
| Min voltage | 24,000 mV | via Kconfig + NVS |
| Max voltage | 30,000 mV | via Kconfig + NVS |
| Max current | 10,000 mA | via Kconfig + NVS |
| Imbalance | 1,000 mV | via Kconfig + NVS |
| Reconnect delay | 10,000 ms | via Kconfig |
| Switch limit | 5 | via Kconfig |
LED Behavior
| State | LED |
|---|---|
| Connected | Solid green |
| Disconnected | Solid red |
| Error | Blinking red (~1 Hz) |
| Locked | Solid red |
iOS Companion App
KXKM BMU — SwiftUI + CoreBluetooth
| Feature | Transport |
|---|---|
| Real-time battery dashboard | BLE (2s poll) |
| Battery detail + voltage chart | BLE |
| Switch ON/OFF + reset | BLE (role-gated) |
| Protection config (0.025V/A step) | BLE |
| WiFi config for BMU | BLE |
| Audit trail + filtering | Local SQLDelight |
| Offline mode with cache indicator | Auto-fallback |
| SOH dashboard + R_int | BLE + REST |
| Victron device scanning | BLE passive |
3 roles: Admin (full), Technician (control), Viewer (read-only) — PIN + Face ID.
Code: iosApp/ (Xcode, CoreBluetooth) + kxkm-bmu-app/ (KMP Shared Kotlin)
Victron BLE Integration
The BMU interacts with the Victron ecosystem via three mechanisms:
| Mechanism | Direction | Protocol |
|---|---|---|
| Instant Readout | BMU → VictronConnect | AES-CTR encrypted advertising (PID 0xA389) |
| GATT SmartShunt | BMU → any BLE client | 9 read-only characteristics (V, I, SOC, Ah, TTG, T, alarm) |
| Device Scanner | Victron devices → BMU | Passive BLE scan, AES decrypt, 8 device cache |
Supported Victron record types: Solar (0x01), Battery (0x02), Inverter (0x03), DC-DC (0x04).
Cloud Infrastructure
Docker stack on kxkm-ai server:
| Service | Port | Role |
|---|---|---|
| Mosquitto | 1883, 9001 | MQTT broker (authenticated) |
| InfluxDB 2.7 | 8086 | Time-series storage (org=kxkm, bucket=bmu) |
| Telegraf | - | MQTT → InfluxDB bridge |
| FastAPI | 8400 | REST API (sync, history, audit) |
| Grafana 11 | 3001 | 3 dashboards (live, fleet, solar) |
Setup: cp .env.example .env — fill all secrets (API key, InfluxDB token, MQTT credentials, Grafana password, CORS origins). All secrets via ${...} env vars, chmod 600 .env.
Offline resilience: when WiFi/InfluxDB is unreachable, telemetry is persisted to internal FAT flash (/fatfs/influx/) with 2-file rotation (512KB each). Automatic replay on reconnection.
Testing
26 tests total — no hardware required.
PlatformIO sim-host (13 tests)
pio test -e sim-host
| Suite | Tests | Covers |
|---|---|---|
test_protection |
10 | V/I/imbalance/lock state machine |
test_battery_route_validation |
- | Index bounds + state checks |
test_influx_buffer_codec |
- | InfluxDB line-protocol encoding |
test_web_mutation_rate_limit |
3 | Sliding window + multi-IP |
test_web_route_security |
9 | Constant-time token + Bearer |
test_ws_auth_flow |
7 | Combined auth + rate limit |
test_mqtt_influx_codec |
3 | JSON MQTT + topic parsing |
test_emulation_bench |
- | Emulation benchmark |
ESP-IDF Host Tests (13 tests)
cd firmware-idf && idf.py build
| Suite | Tests | Covers |
|---|---|---|
test_protection |
13 | Full state machine (ESP-IDF) |
test_victron_gatt |
8 | GATT encoding (V, SOC, alarm, TTG, T) |
test_victron_scan |
5 | Payload parsing, expiry, MAC, CID |
test_ble_victron |
- | Battery/solar payload encoding |
test_vedirect_parser |
- | VE.Direct frame parsing |
test_rint |
- | R_int calculation |
test_config_labels |
- | Battery label management |
test_vrm_topics |
- | VRM topic generation |
CI/CD Pipelines
| Pipeline | Trigger | Gate |
|---|---|---|
ci.yml |
push/PR | sim-host 13 tests pass |
sim-host-tests.yml |
push/PR | + S3 build + RAM ≤75% + Flash ≤85% |
esp-idf-ci.yml |
push/PR | + ESP-IDF host tests + flash ≤85% of 2MB OTA |
Current flash usage: 81% (1.69 MB / 2 MB OTA partition) — 19% headroom.
Security Audit
All findings resolved as of 2026-04-07.
Phase 1 — Migration (March 2026): 7/7 resolved
| ID | Finding | Status |
|---|---|---|
| CRIT-001 | Obsolete function calls | ✅ |
| CRIT-002 | Unreliable global Nb_INA | ✅ |
| CRIT-003 | I2C access without mutex | ✅ |
| CRIT-004 | Missing mutex init | ✅ |
| HIGH-005 | WebSocket V/I race | ✅ |
| HIGH-006 | reconnect_time race | ✅ |
| HIGH-007 | Uninitialized globals | ✅ |
Phase 2 — Deep audit (March-April 2026): 13/13 resolved
| ID | Severity | Finding | Fix |
|---|---|---|---|
| CRIT-A | Critical | mV/V unit mismatch | ESP-IDF: mV everywhere |
| CRIT-B | Critical | Imbalance vs config instead of fleet max | bmu_protection |
| CRIT-C | Critical | Deadlock in web switch | bmu_web |
| CRIT-D | Critical | Unauthenticated routes | bmu_web_security |
| HIGH-1 | High | Negative overcurrent ignored | Bidirectional factor |
| HIGH-2 | High | Unauthenticated WebSocket | Token on first frame |
| HIGH-3 | High | XSS in /log | cJSON null check |
| HIGH-4 | High | MQTT plaintext | Mosquitto auth + .env |
| HIGH-5 | High | I2C speed change unguarded | I2CLockGuard |
| HIGH-7 | High | Public battery_voltages[] | state_mutex |
| HIGH-8 | High | Influx task crash on SD fail | influx_store fallback |
| MED-1 | Medium | No permanent lock (F08) | BMU_STATE_LOCKED |
| MED-010 | Medium | mV/V harmonization | mV throughout |
Infrastructure audit (April 2026): 5/5 resolved
| Finding | Fix |
|---|---|
| Mosquitto anonymous access | Auth + password file |
| Hardcoded secrets in docker-compose | All via .env (chmod 600) |
| Default InfluxDB token | Rotated (44-char random) |
| CORS allow_origins=["*"] | Restricted via env var |
| Offline data loss | FAT/SD persistence + replay |
Hardware
PCB Revisions
| Version | Status | Files |
|---|---|---|
| BMU v1 | Manufactured | hardware/PCB/ |
| BMU v2 | Production-ready (ERC 0, BOM 107) | hardware/pcb-bmu-v2/ |
Key ICs
| IC | Package | Role | I2C Address |
|---|---|---|---|
| INA237 | VSSOP-10 | Power monitor (V/I/P) | 0x40-0x4F (16 max) |
| TCA9535 | TSSOP-24 | GPIO expander (switches + LEDs) | 0x20-0x27 (8 max) |
| ISO1540 | SOIC-8 | Galvanic I2C isolator | - |
| IRF4905 | D2PAK | P-channel MOSFET (55V, 74A) | - |
I2C Mapping
TCA_0 (0x20) → INA 0x40-0x43 → Batteries 1-4
TCA_1 (0x21) → INA 0x44-0x47 → Batteries 5-8
TCA_2 (0x22) → INA 0x48-0x4B → Batteries 9-12
TCA_3 (0x23) → INA 0x4C-0x4F → Batteries 13-16
Topology constraint: Nb_TCA × 4 == Nb_INA — mismatch triggers fail-safe (all OFF).
Project Structure
KXKM_Batterie_Parallelator/
├── firmware-idf/ # ESP-IDF 5.4 firmware (25 components)
│ ├── components/ # Modular ESP-IDF components
│ ├── main/ # app_main entry point
│ └── test/ # Unity host tests (13 suites)
├── firmware/ # Legacy PlatformIO/Arduino firmware
│ ├── src/ # Source modules
│ ├── test/ # sim-host tests (8 suites, 13 tests)
│ └── lib/INA237/ # Local INA237 driver fork
├── iosApp/ # iOS SwiftUI app (CoreBluetooth)
├── kxkm-bmu-app/ # KMP shared Kotlin module
│ └── shared/ # Cross-platform domain logic
├── kxkm-api/ # Docker stack (Mosquitto, InfluxDB, FastAPI, Grafana)
├── hardware/ # KiCad schematics + Gerbers (v1 + v2)
├── specs/ # Kill_LIFE gates (S0-S3)
├── docs/ # Specs, plans, governance, research
├── grafana/dashboards/ # 3 Grafana dashboard JSONs
└── scripts/ # CI, QA, ML pipeline
About KompleX KapharnaüM
KompleX KapharnaüM is a live arts company based in Villeurbanne (Lyon, France), active for 20+ years in street arts, live performance, and digital creation. The company develops its own embedded hardware platform (K32, ESP32-based) and maintains open-source tools on GitHub.
License
GNU General Public License v3.0
Built with ESP-IDF, LVGL, NimBLE, SwiftUI, FastAPI, InfluxDB, and Grafana.
Developed by L'Electron Rare for KompleX KapharnaüM
