BLE Transport Layer¶
The Segway-Ninebot BLE protocol runs over standard BLE GATT. Three service profiles are supported, selected based on hardware generation.
Service & characteristic UUIDs¶
Ninebot Custom (primary — modern devices)¶
This is the primary service used by current Segway-Ninebot vehicles.
| Role | UUID |
|---|---|
| Service | 6e400001-0000-0000-006e-696e65626f74 |
| Write (app → device) | 6e400002-0000-0000-006e-696e65626f74 |
| RCTP Write (secondary) | 6e400003-0000-0000-006e-696e65626f74 |
| Notify (device → app) | 6e400004-0000-0000-006e-696e65626f74 |
| Test Write (factory) | 6e400005-0000-0000-006e-696e65626f74 |
| Test Notify (factory) | 6e400006-0000-0000-006e-696e65626f74 |
UUID structure
The UUID suffix 006e-696e65626f74 decodes to \x00ninebot in ASCII. The internal codename for this service is "Hospitality".
Characteristic 0004, not 0003
The notify characteristic is 0004, not 0003. Characteristic 0003 is the RCTP Write channel (a secondary write path), not the notification channel. This is a common source of confusion.
Nordic UART (compatibility)¶
Older devices and some accessories use the standard Nordic UART Service.
| Role | UUID |
|---|---|
| Service | 6e400001-b5a3-f393-e0a9-e50e24dcca9e |
| Write (TX) | 6e400002-b5a3-f393-e0a9-e50e24dcca9e |
| Notify (RX) | 6e400003-b5a3-f393-e0a9-e50e24dcca9e |
HMSoft (legacy)¶
Very old devices may use the HMSoft BLE module profile.
| Role | UUID |
|---|---|
| Service | 0000ffe0-0000-1000-8000-00805f9b34fb |
| Notify | 0000ffe1-0000-1000-8000-00805f9b34fb |
Characteristic roles¶
| Characteristic | Property | Usage |
|---|---|---|
0002 (RTP Write) |
Write | App → device commands |
0003 (RCTP Write) |
Write | App → device (secondary channel) |
0004 (RTCP Notify) |
Notify | Device → app responses |
0005 (Test Write) |
Write | Factory/diagnostic writes |
0006 (Test Notify) |
Notify | Factory/diagnostic reads |
The app writes encrypted frames to characteristic 0002 and subscribes to notifications on characteristic 0004.
MTU & fragmentation¶
BLE packets are fragmented at the GATT MTU boundary:
- Default MTU: 20 bytes (BLE 4.0)
- Negotiated MTU: Up to 512 bytes (BLE 4.2+)
- Modern devices support MTU negotiation (
mtu_negotiation: truein device config)
The protocol layer handles reassembly across multiple GATT writes/notifications using the frame sync bytes (0x5A 0xA5) and the length field. A state machine scans incoming bytes for the sync pattern, reads the length, and accumulates bytes until a complete frame is received.
App must fragment writes too
Fragmentation is bidirectional. The device fragments its notifications automatically, but the app must also split outgoing frames that exceed the MTU payload size (MTU - 3 bytes). At the default MTU of 23, the payload limit is 20 bytes. For example:
- PRE_COMM encrypted frame = 13 bytes — fits in one write
- AUTH encrypted frame = 27 bytes — must be split into two writes (20 + 7)
If the app writes more than MTU - 3 bytes in a single GATT write, the BLE stack may silently truncate the data. The device receives an incomplete frame and never responds. Split large frames into MTU - 3 byte chunks with a small inter-fragment delay (~10 ms).
Frame reassembly state machine¶
IDLE ─── see 0x5A ───► HAVE_HEAD ─── see 0xA5 ───► HAVE_BEGIN ──► accumulate
│ │
└── other ──► IDLE until LENGTH + 13 bytes
│
▼
deliver frame
Connection flow¶
sequenceDiagram
participant App
participant Device
App->>Device: BLE Scan (look for "Ninebot*" / "S1D*" names)
App->>Device: Connect
App->>Device: Discover services
App->>Device: Find write (0002) + notify (0004) characteristics
App->>Device: Subscribe to notifications (0004)
App->>Device: Negotiate MTU
Note over App,Device: Begin authentication handshake
App->>Device: PRE_COMM (encrypted, non-SN mode)
Device->>App: auth_param + serial number
App->>Device: SET_PWD (encrypted, SN mode)
Device->>App: accepted
App->>Device: AUTH (encrypted, SN mode)
Device->>App: authenticated
Note over App,Device: Normal encrypted communication
iOS bonded device behaviour¶
iOS caches BLE state for bonded (previously connected) peripherals. This creates several pitfalls when reconnecting:
Stale CCCD state¶
iOS persists the Client Characteristic Configuration Descriptor (CCCD) across connections. If the app subscribed to notifications (0004) in a previous session, the device may still consider notifications enabled when the next connection starts — but the app hasn't set up its notification handler yet.
Workaround: Toggle CCCD off → wait 300 ms → on before starting the handshake:
Stale notification draining¶
After re-subscribing, the device may flush cached notifications from the previous session. These arrive before the app sends its first command and will confuse the frame parser if consumed as real responses.
Workaround: Start the notification listener immediately (to consume stale data), but don't process frames until a short drain period (~200 ms) has elapsed. Then "activate" the transport for real communication.
Echo detection¶
On some iOS reconnections, the device enters a state where it echoes back whatever the app writes instead of processing commands. The PRE_COMM request comes back verbatim as a notification — the app receives its own encrypted frame instead of the device's response.
Detection: After sending PRE_COMM, check if the response bytes match the request bytes. If they do, the device is echoing.
Recovery: Disconnect, wait 1–2 seconds (escalating delay), and reconnect. The disconnect resets the device's BLE stack. Up to 3 retry attempts with increasing delays (1s, 2s) reliably recovers the connection.
Attempt 1: Connect → PRE_COMM → echo detected → disconnect, wait 1s
Attempt 2: Connect → PRE_COMM → echo detected → disconnect, wait 2s
Attempt 3: Connect → PRE_COMM → real response → continue handshake
System device discovery¶
On iOS, bonded peripherals don't appear in BLE scans. To find them, query the system's connected peripherals using the service UUIDs:
# iOS: find already-connected peripherals by service UUID
connected = FlutterBluePlus.systemDevices([
Guid("6e400001-0000-0000-006e-696e65626f74"), # Ninebot Custom
Guid("6e400001-b5a3-f393-e0a9-e50e24dcca9e"), # Nordic UART
])
These system devices connect instantly (no scan needed) but are more prone to the stale state issues above.
Device discovery¶
Segway-Ninebot vehicles advertise with BLE names that typically follow these patterns:
| Pattern | Example | Device type |
|---|---|---|
S1DXXXXXXXXXX |
S1DCA000000000 |
Mopeds (E-series) |
NBxxxxxxxxxx |
NB-MK046J2345 |
Kick scooters (Max, F-series) |
Ninebot-XXXX |
Ninebot-1234 |
Self-balancing vehicles |
The BLE device name is significant — it is used as key material during the first two phases of the authentication handshake.