Fast BLE reconnect Β· Live telemetry Β· Ride sessions Β· Activity calendar Β· One-tap lock Β· Persistent notification Β· Full-charge alert
I made this app because I wanted two very simple things from my scooter that were awkward to get from the usual apps: keep my own phone connected to the scooter in the background and have a fast software lock that is always one tap away.
With Persistent connection enabled, the app tries to keep the authenticated BLE connection to the remembered scooter alive and automatically reconnect when the scooter wakes up again.
That is useful for more than just live telemetry. On the G30, the active control connection is normally occupied by one client at a time, so keeping your own phone connected also reduces the opportunity for another nearby phone/app to establish the control connection while yours is active.
This is not a cryptographic anti-theft guarantee and should not be treated as one. It is simply another useful layer on top of the scooter's normal behavior.
The software lock is not supposed to make a scooter impossible to steal. Think of it as additional protection / a deterrent.
When locked, the scooter cannot simply be ridden away normally and becomes much more awkward to move. That makes it useful together with a physical lock: even if somebody cuts or breaks the physical lock, they still cannot just jump on the scooter and ride away immediately. To somebody who does not know what happened, the scooter may simply look disabled or broken.
For a very short stop β for example stepping into a shop for a couple of minutes while you stay nearby β the quick lock is also convenient because it removes the easiest theft scenario: someone casually riding away on an unlocked scooter.
But there is an important limit: software lock does not stop somebody from lifting the scooter, folding it, carrying it away or putting it into a car. If the scooter is going to be left unattended, use a proper physical lock as well. This app is an extra layer, not a replacement for physical security.
In short:
physical lock = stops easy removal
software lock = stops easy riding
persistent BLE = keeps your own control connection active
Everything else in the app β telemetry, ride sessions, the activity calendar, charge alerts and statistics β grew around those two original ideas.
Ninebot Scooter Blocker is a tiny daily-use Android companion built around the BLE/authentication implementation hardware-tested on the Ninebot Max G30.
It focuses on the things useful during everyday riding and courier work instead of becoming a firmware-tuning toolbox: reliable background BLE, live telemetry, quick software lock/unlock, a compact notification, charging alerts and persistent ride statistics.
| Feature | |
|---|---|
| π | Automatic BLE connection to the remembered scooter |
| π | Reconnect with battery-friendly backoff and stale-GATT protection |
| π | One-tap software lock / unlock |
| π | Battery %, voltage, current, power and charging detection |
| πΆ | Live BLE signal strength / RSSI bars |
| π | Live speed, odometer and estimated remaining range |
| π | Movement-based ride sessions that survive short stops and reconnects |
| π | Tappable 17-week GitHub-style distance activity calendar |
| π | Day / Week / Month summaries with date navigation and ride history |
| πΎ | Local SQLite history with JSON statistics import / export |
| π‘οΈ | Controller and battery temperatures when available |
| π | Persistent notification with current ride distance, signal and quick lock control |
| π | Optional short double-beep when a monitored charge reaches 100% |
| π | Home-screen lock shortcut |
| β‘ | No permanent wake lock and no aggressive continuous BLE scanning |
The Statistics screen is built around a GitHub-style activity calendar. Every square is one day; heavier riding days get a stronger fill.
recent 17 weeks β
Mon Β· β β β β β β β Β· β β β β β Β· β β
Tue β β β β β β β Β· β β β β β β β β β
Wed Β· β β β β β β β Β· β β β β β β β β
Thu β β β β β β β β β Β· β β β β β β β
Fri β β β β β β β β β β Β· β β β β β β
Sat β β β β β β β β β β β β β β β β β
Sun β β Β· β β β β β β Β· β β β β β β β
Less Β· β β β β More distance
Tap a day to inspect it. Switch between Day / Week / Month, move backward or forward through dates, and see distance, ride count, connected time, max speed, battery used/charged and the rides belonging to that period.
The heatmap is relative to recent riding history, so it remains readable for both occasional rides and long courier shifts.
Read the full statistics documentation β
- Download the latest APK from Releases.
- Install it on Android.
- Allow Bluetooth permissions.
- Select your scooter once.
- Leave Auto connect enabled.
- Enable Persistent notification if you want background connection, session continuity and quick lock control.
Android 13+ requires notification permission for the live notification and full-charge alert.
The compact dashboard shows:
- detected scooter/model and BLE state;
- live BLE signal strength in bars and dBm;
- battery percentage, voltage, current and power;
- charging state when detected;
- live speed;
- Session distance tracked independently from the scooter's temporary trip counter;
- remaining range and total odometer;
- temperatures;
- one large
π LOCK/π UNLOCKbutton; - persistent notification, auto-connect and full-charge switches;
- direct entry into the statistics screen.
Swipe left from the dashboard or tap Statistics βΊ. The Statistics screen uses normal native vertical scrolling; use the visible back control or Android back gesture/button to return.
A ride is defined by real movement, not by Bluetooth uptime.
The app watches odometer deltas and speed. A session starts when movement is observed and remains open through short stops. If there is no real movement for the configured timeout, that ride closes automatically even if BLE remains connected the entire time.
Choose how long a short stop should still count as the same ride:
20 min Β· 30 min Β· 60 min
That makes a quick shop/restaurant stop part of the same ride, while parking for several hours and riding again creates a new session automatically.
Distance is calculated from total odometer deltas, not the scooter's volatile trip register, so a scooter sleep/reconnect does not reset the app session. Fresh reconnect samples are treated as a baseline to avoid artificial distance jumps.
End ride closes a session immediately. Continue previous is available when two sessions should intentionally be joined.
Statistics are stored locally and separated by scooter serial number, with BLE MAC as the fallback identity.
The redesigned Statistics screen provides:
- Day β inspect one selected calendar day;
- Week β Monday through Sunday around the selected date;
- Month β the whole calendar month;
- left/right date navigation;
- period-specific ride lists;
- distance, rides, connected time and max speed;
- observed battery percentage used and charged;
- a 17-week activity heatmap for fast visual browsing.
Ride samples stay live in memory and are persisted in small batches rather than writing SQLite on every telemetry packet.
Export JSON creates a portable local backup. Import JSON restores statistics without replacing scooter BLE settings.
The notification deliberately gives limited horizontal space to values useful while riding:
π΄ 73% Β· 12.4 km
27.3 km/h Β· π’ Connected [ π LOCK ]
The distance is the app's current ride/session, not the scooter's temporary trip counter. BLE signal bars are also shown in the custom notification layout.
If the scooter sleeps or disconnects, the current session value is retained while reconnecting:
π΄ 73% Β· 12.4 km
π΄ Reconnecting [ β» RECONNECT ]
The disconnected notification remains available for the configured inactivity/grace window. A successful reconnect refreshes the notification immediately instead of waiting for the normal telemetry refresh throttle.
Charging detection uses sustained BMS current while the scooter is stationary instead of trusting one isolated current sample. This avoids treating short regenerative-braking current as a charge session.
While charging, the UI/notification alternates a lightweight π / β‘ indicator.
Enable Full-charge sound alert to get a dedicated notification when an observed charge reaches 100%. The app plays a short, low-key double-beep from the phone notification audio stream and fires the alert once per observed charge session.
Reliable background charge monitoring needs the foreground connection, so enabling the alert also keeps the persistent connection active.
Ninebot Max G30
The known-working G30 BLE transport, SHU-compatible authentication and lock/unlock byte sequences are intentionally kept as the compatibility baseline.
Discovery also accepts compatible Ninebot / Segway BLE devices instead of hard-coding one advertised name. Some older scooters share the same legacy Proto2 register family and may work with the current transport.
This is not SHU-level universal compatibility. Newer families can use Encryption2/Encryption3 authentication, different board routing or different register layouts. A model is not claimed as tested until it is verified on hardware.
| Value | Source / behavior |
|---|---|
| Battery % | ESC |
| Voltage | BMS |
| Current | BMS |
| Power | calculated from voltage Γ current |
| Charging | conservative stationary-current detector |
| BLE signal | Android connected-GATT RSSI |
| Speed | ESC |
| Odometer | ESC |
| Remaining range | ESC |
| Controller temperature | ESC |
| Battery temperature | BMS |
| Lock state | read-only Ninebot status register |
| App ride/session | persisted from movement + odometer deltas |
Unsupported values stay empty instead of being fabricated. BLE writes and RSSI reads are serialized, and slower telemetry is interleaved with faster values.
DISCONNECTED β SCANNING β CONNECTING β CONNECTED β READY
β RECONNECTING / ERROR
Reconnect backoff:
1 s β 2 s β 5 s β 10 s β 30 s
Most reconnect attempts use direct GATT to the remembered BLE address. Every third failed cycle can use an 8-second balanced scan fallback. Connection and scan generations protect a new session from late Android callbacks belonging to an old GATT/scan, and a watchdog resets stuck attempts.
READY and user-visible state transitions are serialized on the main queue so an older Bluetooth callback cannot downgrade an already authenticated connection back to CONNECTED / RECONNECTING.
Connected telemetry uses one small request per scheduler tick: roughly 1 request/second while stopped and up to 2/second while moving. RSSI is sampled much less frequently and uses the same serialized GATT path. There is no continuous BLE scan while connected.
ScooterBleManager
β
NinebotBleClient
β
G30Protocol
β
ScooterRepository βββ RideStatsTracker βββ RideStatsStore / SQLite
β β β β
MainActivity StatsActivity ScooterService
β
RideHeatmapView
NinebotBleClientβ Nordic UART GATT transport, crypto/authentication, fragmentation, RSSI reads and serialized writes.ScooterBleManagerβ scanning and connection coordination with stale-callback generation guards.G30Protocolβ register knowledge and telemetry parsing.ScooterRepositoryβ connection, identity, reconnect, telemetry and commands.RideStatsTracker/RideStatsStoreβ movement-based ride lifecycle and local statistics database.RideHeatmapViewβ compact tappable 17-week distance calendar.ScooterServiceβ foreground lifetime, live notification and full-charge alert.MainActivity/StatsActivityβ presentation only; neither ownsBluetoothGatt.
Statistics schema v2 stores movement time separately from general telemetry activity. This fixes the old behavior where a parked scooter could keep one ride alive for hours simply because BLE remained connected.
Legacy open v1 rides are closed once during migration so an old telemetry timestamp cannot silently merge into the next real ride.
For implementation details, period semantics and backup behavior, see docs/STATISTICS.md.
- Android 12+ β
BLUETOOTH_SCAN,BLUETOOTH_CONNECT - Android 13+ β
POST_NOTIFICATIONS - Android 11 and older β location only because legacy Android BLE scanning requires it
- foreground connection uses the
connectedDeviceservice type
No location permission is requested on Android 12+.
The project deliberately avoids heavyweight runtime dependencies: it uses Android framework Bluetooth, SQLite, JSON, drawing and UI APIs directly; JUnit exists only in the test configuration and is not packaged in the APK.
Published APKs are release builds, optimized by R8 with code shrinking/obfuscation and Android resource shrinking. CI also builds a debug APK and reports debug vs release size so accidental binary growth is visible.
Runtime work is kept small with no permanent wake lock, reconnect backoff, serialized GATT operations, adaptive polling, throttled notification refresh, batched statistics persistence and no endless aggressive scan.
It does not include firmware flashing, region/serial changing, motor power tuning or speed-limit modification. Telemetry requests are read-only; lock/unlock uses the existing working scooter command path.
Requirements: JDK 17 Β· Android SDK 35 Β· Gradle 8.9
gradle :app:assembleDebug
gradle :app:assembleReleasePull requests run release lint, unit tests, and debug + optimized release builds without access to signing secrets. Every push to main additionally verifies the official signature, uploads the signed APK artifact, and publishes the GitHub Release.
The signing keystore is intentionally not stored in this repository. Official APKs are signed in GitHub Actions using an encrypted repository secret. Local builds use the standard Android debug signing behavior unless a private keystore is configured.
- Ninebot-PROTOCOL
- py9b
- ninebot-ble
- M365-Rokid-HUD β Android architecture reference
- Segway / Ninebot BLE documentation
The project's own working G30 implementation remains the primary compatibility source.
Bug reports and focused pull requests are welcome. See CONTRIBUTING.md before submitting a change. Please report security-sensitive issues privately as described in SECURITY.md.
Licensed under the MIT License.
Connect Β· monitor Β· lock Β· ride Β· remember the shift.