Electric scooters and e-bikes with a controller are not just devices with a BLE chip. A typical stack: a BLDC motor controller (Sabvoton, Kelly, Votol) communicates with a display or BMS via UART/RS485 (often proprietary), while a BLE module (Nordic nRF52840, ESP32) listens to the bus and relays data to the mobile app. Developing an app without understanding this chain ends with an app that "connected" but doesn't know what to do with the byte stream. Our team has 5+ years of experience in this niche and over 30 successfully launched projects. We guarantee a stable BLE connection and correct data handling from any controllers.
How we solve the problem of missing controller documentation
Most controller manufacturers (especially Chinese) do not publish protocols. The process: remove the original display, connect a USB-UART analyzer (FTDI232, CP2102) in parallel to the bus, and capture traffic logs. Tools: PulseView with UART decoder, or simply log to a file via minicom/CoolTerm.
A typical Xiaomi M365 protocol frame (as an open example):
[0x55][0xAA][len][addr][cmd][data...][crc_lo][crc_hi] The frame starts with 0x55 0xAA, followed by payload length, recipient address (0x20 — controller, 0x21 — BMS, 0x3E — display), command, data, CRC16. For less popular brands, CRC is computed differently — XOR, Modbus CRC, sometimes just a sum of bytes with a mask.
class ScooterFrameParser { private val buffer = ByteArrayOutputStream() fun feed(byte: Byte): ScooterFrame? { buffer.write(byte.toInt()) val bytes = buffer.toByteArray() // Look for frame start val start = findStart(bytes) ?: return null if (bytes.size - start < 4) return null val len = bytes[start + 2].toInt() and 0xFF val totalLen = len + 6 // header(2) + len(1) + addr(1) + cmd(1) + crc(2) - 1 if (bytes.size - start < totalLen) return null val frame = bytes.copyOfRange(start, start + totalLen) buffer.reset() if (start + totalLen < bytes.size) { buffer.write(bytes, start + totalLen, bytes.size - start - totalLen) } return if (verifyCRC(frame)) parseFrame(frame) else null } } Why BLE connection stability is critical for rides
On Android, BLE works via BluetoothGatt. The main pain is onConnectionStateChange with status = 133 (GATT_ERROR) when connecting, especially on Android 12+ with Bluetooth Permission enabled. Remedy: retry with 500–1000 ms delay, maximum 3 attempts, then show the user an instruction to reconnect Bluetooth.
class ScooterBLEManager(private val context: Context) { private var gatt: BluetoothGatt? = null private var retryCount = 0 fun connect(device: BluetoothDevice) { gatt = device.connectGatt(context, false, object : BluetoothGattCallback() { override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { when { newState == BluetoothProfile.STATE_CONNECTED -> { retryCount = 0 g.discoverServices() } status == 133 && retryCount < 3 -> { retryCount++ g.close() Handler(Looper.getMainLooper()).postDelayed({ connect(device) }, 800) } else -> notifyConnectionFailed() } } override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { frameParser.feed(value) } }, BluetoothDevice.TRANSPORT_LE) } } On iOS, CBPeripheral is more stable, but the CoreBluetooth session does not survive app restart — we save peripheral.identifier (UUID) in UserDefaults and restore via retrievePeripherals(withIdentifiers:). A platform comparison shows that Android BLE requires more retry mechanisms, increasing development time by 10–15% relative to iOS.
| Characteristic | Android | iOS |
|---|---|---|
| Connection stability | Lower (status 133) | High |
| Retry logic | 3 attempts | Not required |
| BLE layer development time | ~2 weeks | ~1 week |
| Background operation | Limited (Background limits) | Good |
Dashboard: what we display
Standard data set from a scooter/bike controller:
- Speed (km/h) — actual from wheel sensor or calculated from RPM + tire circumference
- Battery charge (%) — from BMS, rarely voltage-based estimation
- Battery voltage/current — important for monitoring regeneration
- Controller and motor temperature — critical for heavy climbs
- Mileage — odometer, total and per trip
- Riding mode — Eco/Normal/Sport or D1–D5
- Brake status (if sensors are connected to the controller)
We highlight speed, battery charge, and temperature as key indicators — their updates should be as fast as possible. A speed graph during the trip is mandatory. Render via MPAndroidChart (Android) or Swift Charts (iOS 16+). Data is written to Room/Core Data every 500 ms — a 30 km trip at this interval yields ~3600 points, which is not a problem.
Details on controller protocols
Beyond Xiaomi, there are protocols with frames 10–20 bytes long, where CRC is computed as XOR of all bytes, or Modbus RTU. We have analyzed Votol controllers (EM-30, EM-100) — there the frame starts with 0xAA, command 0xB1 for data, CRC16 Modbus. The parsing algorithm is universal: find the preamble, read length, check CRC.Controlling modes and controller settings
Some controllers allow reprogramming parameters: maximum current, speed limit, regenerative braking power. We send a write command to the Notify Characteristic. Important: changing controller parameters requires user warning and confirmation — incorrect current can damage the motor or drain the battery in one trip.
For sharing services (fleet of scooters), a server part is added: MQTT or WebSocket, trip history on the backend, geofencing, remote lock. This is a separate level of complexity.
Process
- Analysis of the controller protocol and BLE module specification (1–2 weeks).
- Connection prototype: receive and send commands, verification (1 week).
- UI/UX development: dashboard, trip screen, settings (2–3 weeks).
- BLE layer implementation, parser, data recording (2 weeks).
- Testing on real rides (1–2 weeks).
- Publishing to App Store and Google Play, passing review (1 week).
Timelines: 6–8 weeks for a single platform, 3–4 months for a cross-platform solution (Flutter) with support for multiple protocols. Cost is calculated individually after analyzing your specific device model and the availability of protocol documentation. Contact us for a project assessment.
What is included
After project completion, you receive:
- Source code of the app (native or Flutter) with documentation.
- Build and deployment instructions.
- Controller protocol documentation (if reverse engineering was performed).
- Access to the repository and CI/CD tools.
- Support during app store publishing.
- Fleet administrator training (if a sharing project).
We guarantee stable BLE connection and correct data parsing. We continuously update the app for new iOS and Android versions.
Save time: order turnkey app development and get a product tested on real devices. Write to us — we will help.







