Skip to content

Latest commit

Β 

History

180 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ›΄ Ninebot Scooter Blocker

Lightweight Android companion for Ninebot / Segway scooters

Fast BLE reconnect Β· Live telemetry Β· Ride sessions Β· Activity calendar Β· One-tap lock Β· Persistent notification Β· Full-charge alert

Android Release Build

⬇️ Download latest APK


Why I built this

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.

πŸ”— Killer feature #1 β€” keep your own BLE connection alive

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.

πŸ” Killer feature #2 β€” software lock as a second layer

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.


What is it?

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

✨ Statistics at a glance

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 β†’


Quick start

  1. Download the latest APK from Releases.
  2. Install it on Android.
  3. Allow Bluetooth permissions.
  4. Select your scooter once.
  5. Leave Auto connect enabled.
  6. 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.


Dashboard

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 / πŸ”“ UNLOCK button;
  • 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.


Ride sessions

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.


Day / Week / Month history

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.


Live notification

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 & full-charge alert

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.


Compatibility

βœ… Hardware-tested baseline

Ninebot Max G30

The known-working G30 BLE transport, SHU-compatible authentication and lock/unlock byte sequences are intentionally kept as the compatibility baseline.

πŸ§ͺ Experimental compatibility

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.


Telemetry

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.


BLE & reconnect behavior

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.


Architecture

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 owns BluetoothGatt.

Statistics database

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 permissions

  • 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 connectedDevice service type

No location permission is requested on Android 12+.


Battery and size

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.


What this project deliberately does NOT do

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.


Build

Requirements: JDK 17 Β· Android SDK 35 Β· Gradle 8.9

gradle :app:assembleDebug
gradle :app:assembleRelease

Pull 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.


Protocol references

The project's own working G30 implementation remains the primary compatibility source.


Contributing and security

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.

License

Licensed under the MIT License.


Connect Β· monitor Β· lock Β· ride Β· remember the shift.

About

Open-source Android BLE companion for Ninebot and Segway scooters: telemetry, ride tracking, auto-reconnect, charging alerts, and one-tap lock. Tested on Ninebot Max G30.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages