Part of the Librescoot open-source platform.
ecu-service bridges a supported Bosch motor controller from SocketCAN to the
Librescoot Redis IPC surface. It is intended to run on the vehicle, alongside
Redis and the CAN interface connected to the controller. On the unu Scooter Pro, the
Bosch ECU is a licensed, white-labeled Lingbo LBMC controller with CAN support.
- Decodes ECU status, configuration, gear, and diagnostic frames.
- Publishes motor telemetry, controller-reported configuration, and fault state.
- Applies boost and regenerative-braking (KERS) settings from Redis.
- Gates KERS on vehicle readiness, vehicle speed, and the temperature/state of active batteries.
- Reconnects its CAN socket after it becomes stale, such as across suspend and resume.
The service reads the Bosch ECU from the configured CAN interface and maintains
the engine-ecu hash. Its fields include motor voltage/current, speed, RPM,
power, energy counters, temperature, odometer, brake and throttle states,
controller KERS/boost state, gear information, firmware details, and decoded
configuration when the ECU reports it.
The energy:consumed and energy:recovered fields are cumulative integer
mWh totals integrated from motor voltage and current. At process startup the
service restores existing totals asynchronously from Redis; a missing field
starts at zero. While reads fail or either value is invalid, the service logs and
retries, continues processing CAN frames, and defers both energy fields without
overwriting retained values. Energy integrated while waiting is added once to
the restored totals. Sub-mWh remainders and energy while the service is stopped
are not retained. The existing Redis connection health monitor restarts the service
on a detected disconnect. Continuity across a Redis restart or power loss depends
on Redis persistence; the service does not checkpoint energy to disk.
The engine-ecu pub/sub channel carries field names for selected events,
including throttle, odometer, kers, kers-reason-off,
regen-available, and fault. Consumers should read the hash after a
notification rather than treating the notification as a value payload.
Fault presence is stored in engine-ecu:fault; changes are also appended to
events:faults with group engine-ecu. The service watches vehicle.state,
battery:0, and battery:1 to determine KERS eligibility. Battery state is
considered active only when its state field is active.
The additive engine-ecu:motion channel publishes complete direct-value JSON
observations on every motion frame, including unchanged values. See the
versioned contract and isolated integration test. The existing
hash/change-notification interface remains unchanged.
One timestamp-aware CAN receive pump feeds a 64-entry ordered dispatcher queue. Legacy Redis publication cannot block physical reception; an overflow discards queued work and invalidates motion continuity. Receive timestamps, rather than callback time, establish controller liveness. Expired buffered traffic cannot refresh it.
One asynchronous TX worker shares that same socket. Control code submits bounded batches without performing socket writes under the ECU state mutex or in a receive callback. Batches expire after 100 ms; power/connection generation and current transmit permission are checked before writes. Power-off/reconnect invalidate pending commands. Queue-full and write failures leave assertions eligible for reconciliation; startup gear sequences retry after incomplete transmission. Sent flags update only after successful batch completion. A completed socket write is distinct from the controller-reported KERS/boost/configuration values. Other services' CAN access is unchanged.
While vehicle[state] is parked, drive-start-status values waiting-ecu,
release-throttle, and stop-vehicle request full ECU status snapshots at most
once per second. This runs after the eight-second power-on grace, requires both
power rails, and pauses during a communication fault. A recent watchdog probe
also counts toward the request cooldown. No ride-start probes run in
ready-to-drive or after a start timeout.
These requests do not establish freshness by themselves. Vehicle-service waits
for actual engine-ecu:motion reports before allowing drive entry. Other CAN
status traffic can keep communication healthy without providing a current
throttle sample, so the start requests are independent of silence detection.
Compatible TUNU native guards expose accepted/active state, pack eligibility,
remaining allowance, cooldown and inhibition reasons separately from the legacy
boost acknowledgement. The engine-ecu:boost JSON key/channel has a 500 ms
observation-based expiry. See the versioned contract. Stock
control IDs and the existing engine-ecu hash fields remain unchanged.
The service does not immediately assert KERS and boost state when ECU power is commanded on. It waits for the controller's initial boot period, then retries that assertion from the watchdog until an incoming CAN frame confirms the controller is listening. This prevents unacknowledged control frames from being retransmitted while the controller is still booting. State changes made while ECU power is off are not queued; the current commanded state is asserted when the controller becomes reachable.
Runtime configuration is provided by command-line flags. Run
bin/ecu-service -help after building for the authoritative flag list. The
main deployment choices are the Redis address and port, the CAN interface
(default can0), log level, and optional Bosch gear ratios.
The following fields in the Redis settings hash are watched at startup and on
change:
engine-ecu.boostengine-ecu.kersengine-ecu.kers-powerengine-ecu.kers-power-dualengine-ecu.kers-voltage
KERS treats disabled (and the legacy value false) as off; an unset value is
enabled. Current and voltage values are passed to the controller, so restrict
write access to these settings to trusted vehicle-management components.
The Makefile uses Go 1.25.7 for its build and test targets.
make build # Linux ARMv7 binary: bin/ecu-service
make build-host # local-development binary: bin/ecu-service
make test
make lint # requires golangci-lintThe Yocto layer ships librescoot-ecu.service, which requires Valkey and
starts after the vehicle and battery services. The runtime requires a reachable
Redis-compatible datastore and permission to open the configured SocketCAN
interface.
The service handles SIGINT and SIGTERM. Loss of ECU communication while the
controller is powered is reported as the synthetic E20 fault; investigate the
CAN path and controller power state before clearing or acting on that report.
After eight seconds of power-on grace, one second of silence starts a liveness
check regardless of cached speed. The watchdog sends at most two status requests
per uninterrupted outage, at least three seconds apart. E20 requires more than
three seconds of silence and 1.5 seconds without a reply after the second request
(normally about 5.5 seconds total). Failed sends count as attempts, not replies.
A known unavailable receive socket or a recent CAN bus-off/stopped/sleeping sample
raises E20 directly after startup grace, with the transport cause logged separately.
An isolated frame does not clear E20: recovery requires traffic spanning one second without a receive gap exceeding one second, or a reply to a separate, rate-limited recovery probe. No requests are sent while unpowered or during startup grace. Power-off ends monitoring rather than establishing recovery. Configuration replies prove communication only; they do not refresh speed or other telemetry fields.
E20 invalidates cached ECU faults and pending diagnostic transitions. After communication recovers, ECU faults require a fresh Status2 report and normal debouncing; unrelated CAN traffic cannot restore a cached fault.
This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Made with ❤️ by the Librescoot community