Skip to content

Bluetooth Interface Documentation

Service Overview

The scooter exposes several Bluetooth LE services for control and monitoring. All services use the base UUID: xxxx-6e67-5d0d-aab9-ad9126b66f91

Device Information

  • Device Name: "unu Scooter"
  • Company ID: 0xE50A
  • Advertising: 1 second interval, 3 minute duration
  • Connection Interval: 20-75ms
  • Security: MITM enabled, LESC enabled, display-only pairing (6-digit PIN), bonding supported

What a client can do without bonding

Reads and writes require an encrypted, MITM-protected link on every characteristic except two: 9a59a021 (Reset Reason) and 9a59a022 (Reset Count), which are open. Everything else, including both control characteristics, is protected. There is no unbonded path to scooter:state unlock or to any power command.

An unbonded client can still:

  • see the advertisement: device name and manufacturer data (company 0xE50A, payload = the first two bytes of the BLE MAC, so an app can tell scooters apart before connecting)
  • connect and discover the full GATT structure: every service, characteristic and its properties
  • read the two reset counters above

Advertising modes

Advertising follows the vehicle state, and it filters connections rather than changing the advertisement. The payload is byte-identical in both modes.

Vehicle state Mode
parked open, accepts new centrals
off, stand-by, updating, shutting-down, hop-on, ready-to-drive whitelist-only (bonded peers)

ready-to-drive is whitelist-only rather than off, so a bonded phone can reconnect mid-ride while unknown phones are ignored. Consequence when testing: a locked scooter advertises and is visible to a scanner, but will not accept a connection from an unbonded client. Unlock it (parked) to pair a new one.

The mode can be overridden at runtime:

redis-cli lpush scooter:bluetooth advertising-restart-no-whitelisting
redis-cli lpush scooter:bluetooth advertising-start-with-whitelisting
redis-cli lpush scooter:bluetooth advertising-stop

The override does not survive the next change of advertising-mode class (parked and anything else), a disconnect, or a bond change, each of which recomputes the mode from the vehicle state. A transition between two whitelist-only states leaves it in place.

Internal UUID ranges (not GATT)

Some UUIDs in this range address messages on the internal MDB to nRF52 link rather than characteristics. They are not part of the GATT table: no client can discover, read or subscribe to them, and they are listed here only so they are not mistaken for a service that failed to appear.

Range Carries
9a590120-9a590123 Aux charger (LTC4020) control
9a590200-9a590202 Accelerometer wake-up from suspend / hibernation
9a590800+ Power management handshake (the client-visible state is 9a5900a1)

Control Service (9a590000)

Primary control interface for scooter functions

Characteristic Description Values
9a590001 Control Input - "scooter:state lock"
- "scooter:state unlock"
- "scooter:seatbox open"
- "scooter:blinker right"
- "scooter:blinker left"
- "scooter:blinker both"
- "scooter:blinker off"
9a590002 Power Control - "hibernate"
- "wakeup"
- "reboot" (soft reboot via pm-service)
- "hard-reboot" (power-cycle all rails via nRF52)

hard-reboot is only accepted during normal operation (stand-by, suspend, parked, ready-to-drive). It's rejected during active hibernation entry or if a hard reboot is already in progress.

Status Service (9a590020)

Provides scooter state information

Characteristic Description Values
9a590021 Operating State - "stand-by"
- "off"
- "parked"
- "shutting-down"
- "ready-to-drive"
- "updating"

The set above is the full set BLE clients ever observe. When the scooter is in the internal hop-on state, the firmware cloaks the characteristic as "stand-by" so mobile apps with no concept of hop-on still see a locked scooter and offer their unlock affordance. hop-on-learning is collapsed to "parked" upstream (in bluetooth-service) and never reaches the firmware as a distinct value. See Vehicle States. | 9a590022 | Seatbox State | - "open"
- "closed"
- "unknown" | | 9a590023 | Handlebar Lock | - "locked"
- "unlocked" |

Battery Service (9a590040)

Auxiliary battery monitoring

Characteristic Description Values
9a590041 AUX Voltage 0-15000mV
9a590043 Charge Status - "absorption-charge"
- "not-charging"
- "float-charge"
- "bulk-charge"
9a590044 Charge Level 0-100% in 25% steps

Connectivity Battery Service (9a590060)

CBB status monitoring

Characteristic Description Values
9a590061 Charge Level 0-100%
9a590063 Remaining Capacity Integer
9a590064 Full Capacity Integer
9a590065 Cell Voltage mV
9a590072 Charge Status - "not-charging"
- "charging"
- "unknown"

Power State Service (9a5900a0)

System power state monitoring

Characteristic Description Values
9a5900a1 Power State - "booting"
- "running"
- "suspending"
- "suspending-imminent"
- "hibernating-imminent"
- "hibernating"

(cf. State Diagram)

Main Battery Service (9a5900e0)

Primary battery monitoring

Characteristic Description Values
9a5900e2 Primary State - "unknown"
- "asleep"
- "active"
- "idle"
9a5900e3 Primary Present 0/1
9a5900e6 Primary Cycles Integer
9a5900e9 Primary SoC 0-100%
9a5900ee Secondary State - "unknown"
- "asleep"
- "active"
- "idle"
9a5900ef Secondary Present 0/1
9a5900f2 Secondary Cycles Integer
9a5900f5 Secondary SoC 0-100%

Power Mux Service (9a590100)

Characteristic Description Values
9a590101 Selected Input (read) Active battery source

Extended Commands Service (9a590400)

Unified extensible command/response channel for phone app interaction

Characteristic Description Values
9a590401 Command Write-only, up to 128 bytes. Null-terminated string command (MITM-protected)
9a590402 Response Read+notify, up to 512 bytes. Response string from iMX (MITM-protected)

Supported commands:

Command Description Response
nav:dest lat,lon[,name] Set navigation destination nav:ok
nav:clear Clear navigation destination nav:ok
nav:fav:add lat,lon,name Save a location nav:fav:added:<id>
nav:fav:delete <id> Delete saved location nav:ok
nav:fav:navigate <id> Navigate to saved location nav:ok
nav:fav:list List saved locations nav:fav:count:<n> then nav:fav:<id>:lat,lon,name per entry
usb:ums Enter USB Mass Storage mode usb:ok
usb:normal Exit USB Mass Storage mode usb:ok
keycard:list List keycards Response via keycard-service
keycard:count Count keycards Response via keycard-service
keycard:add:<uid> Add keycard Response via keycard-service
keycard:remove:<uid> Remove keycard Response via keycard-service
time:set <unix_timestamp> Set system clock time:ok
config:apn <value> Set cellular APN config:ok
config:hibernate-timer <seconds> Set hibernation timeout config:ok
config:update-channel <channel> Set OTA update channel (stable/testing/nightly) config:ok
config:auto-standby-seconds <seconds> Set auto-lock idle timeout when parked (0=disabled, 0-3600) config:ok
alarm:enable Enable alarm system alarm:ok
alarm:disable Disable alarm system alarm:ok
alarm:arm Arm alarm (runtime, bypasses enabled setting) alarm:ok
alarm:disarm Disarm alarm alarm:ok
alarm:start / alarm:start:<N> Trigger alarm; duration in seconds, default from settings alarm:ok
alarm:stop Stop active alarm alarm:ok
ltc:enable Safe-enable the LTC4020 auxiliary charger ltc:ok
ltc:disable Disable the LTC4020 auxiliary charger ltc:ok
ltc:force-enable Force-enable the auxiliary charger, bypassing the safety check ltc:ok
ltc:force-disable Force-disable the auxiliary charger ltc:ok
ltc:status Query the LTC4020 charger state ltc:status:on or ltc:status:off
pm:hibernate-for <duration> Hibernate for the given duration (Go syntax: 30s, 10m, 8h); nRF52 wakes the iMX6 after the duration pm:ok
pm:hibernate-cancel Cancel a pending hibernate-for and disarm the wake timer pm:ok
status:maps-available Query if offline maps are installed status:maps-available:true or false
status:navigation-available Query if routing engine is available status:navigation-available:true or false
status:version:mdb Query the installed MDB OS image version status:version:mdb:<version>
status:version:dbc Query the installed DBC OS image version status:version:dbc:<version>
ble:forget Ask the scooter to forget the phone sending the command, so an app's "forget this scooter" clears both halves of the bond. Ends the connection it arrives on. Needs nRF v2.8.0-ls or later, so probe cap:ble first ble:forget:ok, or ble:error:unsupported
cap:list Enumerate supported capability categories cap:count:<n> then cap:<name> per category
cap:<category> List commands for a category cap:<category>:count:<n> then cap:<category>:<command> per command
get:<key> Read a schema-declared setting get:<key>:<value>
set:<key>:<value> Set a writable schema-declared setting set:ok:<key> or set:error:<reason>

Error responses follow the pattern <prefix>:error:<details>. See Generic settings for the complete contract.

Generic settings

get:<key> and set:<key>:<value> use the schema published by settings-service in settings:schema. Reads and writes reject unknown keys; writes also reject read-only keys and schema-invalid values. A value begins after the first colon, so a value may itself contain colons. get:list and get:list:<prefix> return get:count:<n> followed by exactly n get:<key> notifications.

All extended commands, including generic settings, require the encrypted MITM-protected bonded connection described above. The protocol has no per-phone authorization: any bonded phone with extended-command access can read schema-declared setting values and change any writable schema setting. Treat bonding as the authorization boundary.

OTA Transfer Service (9a590500)

Firmware bundle transfer from the phone to the scooter (MDB and DBC updates over BLE). Windowed, resumable, integrity-checked; the nRF forwards these characteristics verbatim to bluetooth-service. See BLE OTA Firmware Transfer for the full protocol.

Characteristic Description Values
9a590501 OTA Data (write w/o response, max 244 B) [offset:u32 LE][chunk ≤ 240 B]
9a590502 OTA Control (write, max 128 B) START / COMPLETE / ABORT / STATUS_REQ messages
9a590503 OTA Status (read+notify, max 64 B) START_ACK / ACK / COMPLETE_ACK / INSTALL_PROGRESS / ABORT_ACK / ERROR messages

Scooter Info Service (9a59a040)

System-level telemetry and configuration

Characteristic Description Values
9a59a041 iMX Software Version Version string (read-only)
9a59a042 Mileage/Odometer Signed 32-bit little-endian integer, meters (read-only)
9a59a043 System Time Unix timestamp string (write-only)
9a59a044 Navigation Active 0 = no destination, 1 = destination set (read+notify)
9a59a045 UMS Status 0 = normal, 1 = USB Mass Storage mode (read+notify)

System Info Service (9a59a000)

Characteristic Description Values
9a59a001 nRF Version Version string (e.g. "v2.2.1-ls")
9a59a021 Reset Reason Nordic RESETREAS register value (see below). Readable without bonding
9a59a022 Reset Count Integer. Readable without bonding

RESETREAS (Reset Reason Register)

Reset reason register. Cumulative unless cleared. Fields are cleared by writing '1'. If no reset sources are flagged, indicates reset from on-chip reset generator (power-on-reset or brownout reset).

Field Bit(s) Access Description Value Meaning
RESETPIN 0 RW Reset from pin-reset detected 0
1
Not detected
Detected
DOG 1 RW Reset from watchdog detected 0
1
Not detected
Detected
SREQ 2 RW Reset from soft reset detected 0
1
Not detected
Detected
LOCKUP 3 RW Reset from CPU lock-up detected 0
1
Not detected
Detected
OFF 16 RW Reset due to wake up from System OFF mode (DETECT signal from GPIO) 0
1
Not detected
Detected
LPCOMP 17 RW Reset due to wake up from System OFF mode (ANADETECT signal from LPCOMP) 0
1
Not detected
Detected
DIF 18 RW Reset due to wake up from System OFF mode (debug interface mode entry) 0
1
Not detected
Detected
NFC 19 RW Reset due to wake up from System OFF mode (NFC field detect) 0
1
Not detected
Detected
VBUS 20 RW Reset due to wake up from System OFF mode (VBUS rising into valid range) 0
1
Not detected
Detected

Register layout has:

  • Low bits (0-3): Basic reset sources
  • High bits (16-20): System OFF wake-up sources