librescoot-ecu (ecu-service)¶
Description¶
The ECU service (ecu-service) provides a unified interface for communicating with electric scooter motor controllers (ECUs) via CAN bus. It monitors motor speed, RPM, voltage, current, temperature, throttle state, KERS (kinetic energy recovery system) status, odometer, power metrics, and fault codes. The service publishes this data to Redis for consumption by the dashboard and other services, and manages KERS functionality based on battery temperature and vehicle state.
Command-Line Options¶
Usage of ecu-service:
-can_device string
CAN device name (default "can0")
-gear_ratios string
Bosch ECU gear ratios (comma-separated values 1-3, each 1-255, e.g. '100,150,200')
-log int
Log level (0=NONE 1=ERROR 2=WARN 3=INFO 4=DEBUG) (default 3)
-redis_port int
Redis server port (default 6379)
-redis_server string
Redis server address (default "127.0.0.1")
-version
Print version and exit
Redis Operations¶
Hash: engine-ecu¶
Fields written:
motor:voltage- Motor voltage in mVmotor:current- Motor current in mA (signed, negative during regenerative braking)rpm- Motor RPMspeed- Vehicle speed in km/h (calibrated)raw-speed- Raw speed before calibrationthrottle- Throttle state ("on", "off")brake- Brake state ("on", "off")temperature- ECU temperature in °Cfault:code- Current fault code (32-bit, 0 when no fault)fault:description- Active fault description (empty string when no fault)odometer- Total distance traveled in meterskers- KERS (regenerative braking) state ("on", "off")boost- Boost mode state ("on", "off")gear- Current gear (1-3, 0 if unknown)power- Instantaneous power in mWenergy:consumed- Cumulative energy consumed in mWhenergy:recovered- Cumulative energy recovered via regenerative braking in mWhkers-reason-off- Reason KERS is disabled ("none", "cold", "hot")kers-accepted-voltage- EBS regen voltage cap the ECU accepted, in mV (Bosch only; 0 otherwise). The ECU echoes this after clamping the EBS Set command; it is the stored config, not a live measurement. Distinct from the commandedengine-ecu.kers-voltagesetpoint.kers-accepted-current- EBS regen current limit the ECU accepted, in mA (Bosch only; 0 otherwise). Distinct from the commandedengine-ecu.kers-powersetpoint.regen-available- Derived ("on"/"off"): whether regen can happen right now.regen-reason- Derived: why regen is unavailable ("none"/"cold"/"hot"/"off"/"standstill"/"full"). "standstill" means wheel RPM is below the ECU's regen engage deadband (~90 wheel RPM, ~7 km/h). "full" means the pack is at its voltage cap. Empirically derived envelope.regen-expected- Derived: expected regen current envelope in mA.motor:currentremains the real-measurement source for actual regen.ecu-status- ECU enable state ("enabled"/"disabled"), decoded from the Status4 paired enable/disable bitsboost-status- Boost enable state ("enabled"/"disabled"), same Status4 signal asboostgear-mode- Gear-mode enable state ("enabled"/"disabled"), from Status4fw-version- ECU firmware identification word, 8 hex digits (written once the ECU has reported it)warranty-date- Unknown word from the same frame, 8 hex digits. Always zero on every controller observed so far, and the key is only written when it is non-zero, so in practice it does not appear.motor:rated-power-kw- Motor rated power in kWmotor:max-speed-kmh- Motor maximum speed in km/hfw:base-version- Base software version (e.g. "4.0")fw:app-version- Application software revisiongear:high-current-ratio,gear:mid-current-ratio,gear:low-current-ratio- Per-gear current scaling in percentgear:high-torque-ratio,gear:mid-torque-ratio,gear:low-torque-ratio- Per-gear torque scaling in percentconfig:ov-threshold-mv- ECU-reported over-voltage threshold in mVconfig:uv-threshold-mv- ECU-reported under-voltage threshold in mVconfig:speed-limit-ratio- ECU-reported speed limit in percentconfig:wheel-circumference-cm- ECU-reported wheel circumference in cmconfig:max-phase-current-ma- ECU-reported peak phase current in mAconfig:startup-phase-current-ma- ECU-reported startup phase current in mA
The config:* fields are deleted from the hash rather than written as 0 while the ECU has not reported them, so a missing field means "not reported" and not a real zero.
Published channels:
All notifications go out on the engine-ecu channel, with the name of the field
that changed as the payload:
throttle- Published when throttle state changeskers- Published when KERS state changesodometer- Published when odometer updateskers-reason-off- Published when KERS disable reason changesregen-available- Published when derived regen availability or reason changesfault- Published when a fault is raised or cleared
Hash: settings¶
Fields read:
engine-ecu.kers- KERS enable/disable ("enabled"/"disabled"; default: enabled)engine-ecu.kers-power- KERS regenerative braking current in mA for single-battery operation (default: 10000)engine-ecu.kers-power-dual- KERS regenerative braking current in mA when both batteries are active (optional; when unset,kers-poweris used for both cases)engine-ecu.kers-voltage- KERS target voltage in mV (Bosch only; default: 56000, valid range 42000-58000)engine-ecu.boost- Boost mode enable/disable ("true" enables, any other value disables)
Loaded on startup; updated live via settings pub/sub.
Pub/Sub Subscriptions¶
The service subscribes to the following channels to monitor system state:
vehicle- Listens for vehicle state changes (e.g., "ready-to-drive")battery:0- Monitors battery 0 state and temperaturebattery:1- Monitors battery 1 state and temperaturesettings- Listens for KERS setting changes
Redis Operations for Fault Management¶
Set: engine-ecu:fault
- Contains the active fault code (SADD when a fault is raised; the key is DELeted when the fault clears)
Stream: events:faults
- Publishes fault events with group="engine-ecu"; a raised fault carries
code,descriptionandseverity, a cleared fault carries the samecodenegated, so the two can be paired up - Limited to 1000 entries (MAXLEN)
Published channel for faults: engine-ecu with payload "fault"
Hardware Interfaces¶
CAN Bus¶
- CAN device: Configurable via
-can_deviceoption (default:can0) - Interface type: SocketCAN
- Connected to: Bosch motor controller/ECU
The service uses standard Linux SocketCAN to communicate with the motor controller.
ECU Communication¶
The service talks to a Bosch ECU over CAN.
CAN Message IDs:
0x7E0- Status1: Voltage, current, RPM, speed, throttle state0x7E1- Status2: Temperature, fault codes0x7E2- Status3: Odometer0x7E3- Status4: KERS enabled status0x4E0- Control message (sent to ECU for KERS control)0x4E2- EBS settings (sent to ECU for KERS voltage/current)
Speed Calibration:
- Applies calibration factor: 1.03
- Applies tolerance factor: 1.155556
- Formula:
speed = raw_speed * 1.03 * 1.155556
Odometer Calibration:
- Odometer values multiplied by 1.07 and converted from 0.1km to meters
KERS Control:
- Sends voltage/current settings (56V, 10A) via CAN ID 0x4E2
- Sends enable/disable commands via CAN ID 0x4E0
- Supports gear mode and boost mode flags
Configuration¶
Systemd Unit¶
- Unit file:
/usr/lib/systemd/system/librescoot-ecu.service - Started by: systemd at boot
- Restart policy: Always
CAN Bus Setup¶
The bus runs at 125 kbit/s. The interface must be configured before the service starts:
Two things do this, and they have to agree. librescoot-netconfig.service
(a oneshot from the mdb-netconfig recipe) runs /usr/sbin/librescoot-netconfig,
which sets the bitrate and raises the link. systemd-networkd then reconfigures
can0 from /etc/systemd/network/20-can0.network, which carries the same
BitRate=125000 along with RestartSec=100ms. The restart setting has to be in
the networkd file as well as the script, because networkd's reconfigure resets
what the script set: without it the controller latches bus-off and stays there,
since anything the MDB sends while the ECU is unpowered goes unacknowledged and
drives the TX error counter to 256.
Observable Behavior¶
Startup Sequence¶
- Parse command-line options (CAN device, Redis settings, log level, gear ratios)
- Validate the log level and the
-gear_ratiosstring - Connect to Redis server with timeout (5s connection timeout)
- Initialize components:
- Battery monitor (tracks 2 battery slots)
- IPC TX (Redis publisher)
- Write default state to Redis (all zeros/off)
- Start Redis health check goroutine (30s interval)
- KERS manager (with 1.5s ready-to-drive delay timer)
- Diagnostics manager (fault tracking)
- Initialize CAN bus interface
- Create the Bosch ECU instance
- Set up IPC RX subscriptions:
- Subscribe to
vehiclechannel - Subscribe to
battery:0andbattery:1channels - Read initial states from Redis
- Register CAN frame handler
- Start CAN message publishing loop (background goroutine)
- Begin processing CAN frames and Redis subscriptions
- Wait for SIGINT or SIGTERM to shut down
Runtime Behavior¶
CAN Message Processing¶
- Continuously receives CAN frames from ECU
- Parses relevant CAN IDs for motor data
- Updates Redis hash on value changes
- Publishes to
engine-ecuchannel on updates
Speed Monitoring¶
- Speed reported in km/h
- Typical range: 0-45 km/h
- High update frequency (multiple times per second)
- Dashboard polls
engine-ecu:speedwhen vehicle is ready-to-drive
KERS (Regenerative Braking)¶
The service manages KERS based on battery temperature and vehicle state:
- KERS Enable Conditions:
- Active battery temperature is "ideal"
- Vehicle is in "ready-to-drive" state
- 1.5 second delay after vehicle becomes ready-to-drive
-
Vehicle is stopped (speed = 0)
-
KERS Disable Reasons (
kers-reason-offvalues): "none"- KERS is enabled or should be enabled"cold"- Active battery temperature is too cold-
"hot"- Active battery temperature is too hot -
Battery Temperature Monitoring:
- Monitors both battery slots (battery:0 and battery:1)
- Uses temperature state of whichever battery is marked "active"
-
If no battery is active the temperature state is unknown and KERS state is not updated; when both are active the more restrictive of the two temperature states wins
-
State Machine:
- Subscribes to
vehiclechannel for state changes - Subscribes to
battery:0andbattery:1for temperature updates - Updates KERS state only when vehicle is stopped
- Applies 1.5s delay when transitioning to ready-to-drive state
Odometer¶
- Total distance in meters
- Monotonically increasing
- Persisted by ECU (survives power cycles)
Fault Detection¶
The service monitors ECU fault codes and manages them through Redis. The ECU reports one fault at a time as a sequential ordinal, not as a bit mask, and the service maps it to a description and a severity:
| Code | Fault | Severity |
|---|---|---|
| 1 | Battery over-voltage | critical |
| 2 | Battery under-voltage | critical |
| 3 | Motor short-circuit | critical |
| 4 | Motor stalled | critical |
| 5 | Hall sensor abnormal | critical |
| 6 | MOSFET check error | critical |
| 7 | Motor open-circuit | critical |
| 10 | Power-on self-check error | critical |
| 11 | Over-temperature | critical |
| 12 | Throttle abnormal | critical |
| 13 | Motor temperature protection | warning |
| 14 | Throttle active at power-up | warning |
| 16 | Internal 15V abnormal | critical |
| 20 | ECU communication lost | critical |
Code 20 (E20) is synthetic. It is raised by the comm-lost watchdog when the ECU should be powered but has gone quiet, and is not a code the ECU reports. A code with no entry in the table is logged once as unknown rather than being reported as a healthy vehicle.
Code 15 is missing from the table on purpose. The ECU uses it to report that the
engine brake line is asserted, which the vehicle does itself in every state
except drive as the interlock that stops a parked scooter riding off. It never
reaches fault:code; the service logs its edges instead, so it does not toast
the rider every time they park.
Fault Handling:
- Active faults added to
engine-ecu:faultset - Fault events published to
events:faultsstream - Notification published to
engine-ecuchannel with "fault" payload - When the fault clears, the
engine-ecu:faultkey is deleted and the raised code is logged to the stream negated (a fault to fault transition removes the outgoing code from the set and logs its clear before the new raise)
Power Metrics¶
The service calculates and tracks power consumption and recovery:
- Instantaneous Power:
power = (voltage_mV * current_mA) / 1000(in mW) - Energy Integration: Power integrated over time to calculate energy in mWh
- Consumed Energy: Tracked when power is positive (motor driving)
- Recovered Energy: Tracked when power is negative (regenerative braking)
- Maximum Delta: 2 second maximum between updates (prevents large accumulation when ECU is off)
Log Output¶
The service logs to journald (or stdout when not running under systemd). Common log patterns:
- Startup:
- Service version
- Redis connection status
- Component initialization (Battery, IPC TX/RX, KERS, Diagnostics, ECU)
-
Default Redis state written
-
Runtime:
- Vehicle state changes (ready-to-drive, etc.)
- Battery state updates (active, temperature-state)
- KERS state changes and reason updates
- Fault code set/cleared events
- CAN health check status
-
Redis connection issues
-
Detailed Logging (at INFO/DEBUG levels):
- Battery subscription messages
- Vehicle subscription messages
- KERS update decisions
- Power metrics calculations
Use journalctl -u librescoot-ecu to view logs.
Dependencies¶
- CAN device - Must have configured SocketCAN interface (e.g., can0)
- ECU/Motor controller - Must be powered and responsive on CAN bus
- Redis server - At specified host:port (default: 127.0.0.1:6379)
- Battery service - For temperature state monitoring (publishes to battery:0 and battery:1)
- Vehicle state manager - For ready-to-drive state (publishes to vehicle channel)
Related Documentation¶
- Redis Operations - Engine ECU hash fields
- Dashboard Redis - How dashboard displays ECU data
- States - Vehicle states that affect motor operation