OpenLeaf is a hardware-agnostic telemetry engine for the Nissan Leaf. It ingests vehicle data from different transports (synthetic, OBD2, CAN, playback) and exposes a unified state via a simple HTTP API so any UI can render or act upon it.
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -e .[dev]
python main.pyFor quick local testing you can use the provided scripts (they create .venv, install dependencies, and background the processes):
bash start.sh server # default: starts just the FastAPI server (log: openleaf_server.log)
bash start.sh ui # launches the Kivy UI (log: openleaf_ui.log)
bash start.sh all # spins up both
bash stop.sh server # stop server
bash stop.sh ui # stop UI
bash stop.sh all # stop bothThen query the API:
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/stateAn early touchscreen dashboard lives under openleaf/ui/kivy. It polls the same backend API, so
start the FastAPI server (synthetic mode is fine) and in a separate shell run:
pip install -e .[ui]
python openleaf/ui/kivy/main.pyThe UI targets 1024x600 touch displays and shows SOC/SOH gauges plus pack metrics, updating every
0.5s. A second screen (use the arrow buttons along the bottom) renders a LeafSpy-style cell graph
using the configured cell_count, and a third screen provides a DTC viewer with placeholder “pull”
and “clear” actions. Clear commands already hit the FastAPI endpoint; full DTC retrieval will land
once we implement it in the backend. A fourth “Debug” screen lists recent OBD transport TX/RX lines
so you can verify ELM327 chatter from the UI.
Runtime configuration lives in config.yaml. The default ships with the synthetic transport
so you can get immediate feedback without hardware.
transport:
type: "synthetic"
update_interval_sec: 0.5
logging:
enabled: false
path: "./leaf.log"
vehicle:
year: 2013
generation: "ZE0"
model: "SV"
pack_kwh: 24.0
cell_count: 96
# 2011-2015 ZE0 24 kWh -> 96 cells
# 2016-2017 ZE0 30 kWh -> 96 cells
# 2018-2021 ZE1 40 kWh -> 96 cells
# 2019-2024 ZE1 e+ 62 kWh -> 96 cellsThe vehicle block lets you describe the battery pack you’re targeting. cell_count feeds the
synthetic transport (and future UIs) so graphs like the per-cell view know how many modules to
render. Later transports (OBD, CAN, playback) will honor these settings to parse the correct
signals for each Leaf generation.
Preset configs live in configs/:
configs/synthetic.yaml— default synthetic mode.configs/leaf_2013_24kwh.yaml— OBD for a 2013 ZE0 (24 kWh, 96 cells).configs/leaf_2018_40kwh.yaml— OBD for a 2018 ZE1 (40 kWh, 96 cells).
Point the server at any config via CLI or env:
python main.py configs/leaf_2013_24kwh.yaml
bash start.sh server configs/leaf_2013_24kwh.yaml
OPENLEAF_CONFIG=configs/leaf_2018_40kwh.yaml python main.pySet logging.enabled: true in your config (defaults to ./logs) to emit:
logs/server.logfor app/server eventslogs/connection.logfor all transport TX/RX plus per-adapter files (logs/connection_<adapter>.log)logs/ui.log(stdout/err) andlogs/kivy_ui_*.txtwhen using the helper scripts (UI logging)
Adjust logging.level for verbosity (e.g., DEBUG for maximum detail).
- Pair your ELM327 adapter over Bluetooth on the target device (e.g., Raspberry Pi) and note
the rfcomm device path (commonly
/dev/rfcomm0). - Update
config.yaml:
transport:
type: "obd"
serial_port: "/dev/rfcomm0"
baudrate: 115200
timeout_sec: 1.0
pid_path: "pids/leaf.yaml"
update_interval_sec: 0.5- Start the server (
python main.py). The OBD transport will:- Open the serial port and run an ELM327 handshake (
ATZ,ATI,ATL1,ATH1,ATS1,ATAL,ATSP6). - For each PID defined in
pids/leaf.yaml, set the CAN header (ATSH), send the request (for example02 21 01), assemble multi-frame ISO-TP replies, and decode signals into the shared state store. - If the adapter or vehicle drops, the loop will close the port, sleep briefly, and re-run the initialization sequence on reconnect.
- Open the serial port and run an ELM327 handshake (
PID definitions live in pids/leaf.yaml so you can tweak or add signals without code changes.
Each entry declares the request/response IDs, service/data identifier, and per-signal bit
positions plus scaling/offset. The defaults mirror LeafSpy-style requests to the BMS (21 01
for pack metrics and 21 61 for SOH).
Transports (synthetic/OBD/CAN/playback) --> StateStore --> FastAPI HTTP API --> UI clients
Transports feed decoded Leaf signals into a shared StateStore. The FastAPI server exposes
/state, /health, and command endpoints for control flows. Different UIs (web, Kivy, etc.)
can poll or subscribe to the state endpoints. Logging and playback transports will make it
possible to capture and replay real drives.
- Synthetic transport and HTTP API (this phase)
- OBD/Bluetooth ELM327 transport
- CAN/SocketCAN transport
- Playback/logging engine
- Touch-friendly UI built on top of the state API
OpenLeaf is distributed under the terms of the GNU General Public License v3.0 or
later. See LICENSE for details.