# Socket Events — Scooter Location Updates

This document describes how scooter GPS location updates flow from the OMNI device through the Node MQTT bridge to Socket.IO clients, and what gets persisted in the database.

**Related:**

- [MQTT.md](./MQTT.md) (broker, topics, DB persistence)
- [SOCKET-MOBILE-ACTIVE-RIDE.md](./SOCKET-MOBILE-ACTIVE-RIDE.md) (active ride screen — map, speed, distance, battery, balance)

---

## Overview

Scooter location is **not** pushed by the mobile app. The IoT device publishes GPS over MQTT; the bridge updates MySQL and emits a Socket.IO event.

```mermaid
sequenceDiagram
    participant Device as OMNI_Device
    participant MQTT as MQTT_Broker
    participant Bridge as Node_Bridge
    participant DB as MySQL_scooters
    participant Socket as Socket_IO
    participant App as Mobile_App

    Device->>MQTT: om/client/data/location/{IMEI}
    MQTT->>Bridge: handlers._handleLocation
    Bridge->>DB: persistScooterState lat/lng/speed
    Bridge->>Socket: emit scooter-location
    Socket->>App: broadcast + scooter rooms
    App->>Socket: enter-scooter scooter_id/imei
```

| Step | Component | Action |
|------|-----------|--------|
| 1 | Device | Publishes `om/client/data/location/{IMEI}` (~every 30s when tracking enabled) |
| 2 | `mqtt/handlers.js` | Parses NMEA/decimal coords, validates GPS |
| 3 | `mqtt/scooter-service.js` | Updates `scooters.lat`, `scooters.lng`, `scooters.speed` |
| 4 | `mqtt/ride-service.js` | If active ride exists, updates `rides` end position / distance / cost |
| 5 | Socket.IO | Emits `scooter-location` to all clients + scooter rooms |

**Source files:**

- [`mqtt/handlers.js`](./mqtt/handlers.js) — `_handleLocation()`, `emitScooterEvent()`
- [`mqtt/scooter-service.js`](./mqtt/scooter-service.js) — `persistScooterState()`
- [`socket/socket.js`](./socket/socket.js) — `enter-scooter` / `exit-scooter` room join

---

## Main event: `scooter-location` (server → client)

Emitted on **every** incoming MQTT location message, whether or not GPS is valid enough for DB save.

### Broadcast targets

From `emitScooterEvent()` in `handlers.js`:

1. **Global** — `io.emit('scooter-location', payload)` (all connected sockets)
2. **Room by ID** — `io.to('scooter:{scooter_id}')` when `scooter_id` is known
3. **Room by IMEI** — `io.to('scooter:imei:{imei}')` always

Clients that only care about one scooter should call `enter-scooter` first (see below) to avoid relying on the global broadcast.

### Database columns updated (when `db_saved: true`)

| Table | Columns |
|-------|---------|
| `scooters` | `lat`, `lng`, `speed`, `updated_at` |
| `rides` | `end_lat`, `end_lng`, `distance_km`, `duration_seconds`, `cost` (only if ride `status` is `active` or `ending`) |

---

## Subscribe to a scooter (client → server)

| Event | Direction | Purpose |
|-------|-----------|---------|
| `enter-scooter` | client → server | Join `scooter:{id}` and/or `scooter:imei:{imei}` rooms |
| `exit-scooter` | client → server | Leave those rooms |

### JavaScript example

```javascript
// After socket connects (with auth token in handshake query)
socket.emit('enter-scooter', { scooter_id: 38 });
// or by device IMEI:
socket.emit('enter-scooter', { imei: '862499071894209' });

socket.on('scooter-location', (payload) => {
  if (!payload.db_saved) {
    console.warn('GPS not saved — invalid lock', payload.gpsNum);
    return;
  }
  updateMapMarker(payload.scooter_id, payload.lat, payload.lng);
  updateSpeed(payload.speed_kmh ?? payload.derived?.gps_speed_kmh);
});

socket.on('disconnect', () => {
  socket.emit('exit-scooter', { scooter_id: 38 });
});
```

`enter-scooter` payload (either field optional, both allowed):

```json
{ "scooter_id": 38 }
```

```json
{ "imei": "862499071894209" }
```

---

## Payload reference — `scooter-location`

Built in `handlers.js` → `_handleLocation()`. All events include base fields from `buildTelemetryEvent()`:

| Field | Type | Description |
|-------|------|-------------|
| `imei` | string | Device IMEI (MQTT topic id) |
| `scooter_id` | number \| null | `scooters.id` (null if not yet in DB) |
| `scooter_code` | string \| null | `scooters.code` (scan QR / admin code) |
| `type` | string | Always `"location"` |
| `received_at` | string | ISO timestamp when bridge received the message |

### Coordinates

| Field | Type | Description |
|-------|------|-------------|
| `lat` | number | Decimal degrees — **use for maps and DB** |
| `lng` | number | Decimal degrees — **use for maps and DB** |
| `LAT` | string | Raw OMNI field (often NMEA `DDMM.mmmm`) |
| `LNG` | string | Raw OMNI field |
| `GEO_NS` | string | `"N"` or `"S"` |
| `GEO_EW` | string | `"E"` or `"W"` |
| `coord_format` | string | `"nmea"` or `"decimal"` |
| `points_count` | number | Number of points in MQTT `location[]` array |

### GPS quality

| Field | Type | Description |
|-------|------|-------------|
| `gpsNum` | number | Satellite count (`0` = no fix) |
| `hdop` | string | Horizontal dilution of precision |
| `altitude` | number \| null | Parsed altitude (meters) |
| `altitude_raw` | string | Raw OMNI value e.g. `"628.2,M"` |
| `timestamp` | string | Device timestamp (Unix string) |

### Speed and distance

| Field | Type | Source |
|-------|------|--------|
| `speedKmh` | number \| null | Last ECU report (`scooter-info`) |
| `speed_kmh` | number \| null | Alias of `speedKmh` |
| `singleRideMile` | number \| null | ECU trip distance (meters) |
| `trip_distance_m` | number \| null | Alias |
| `runTotalMile` | number \| null | ECU total distance (meters) |
| `total_distance_m` | number \| null | Alias |
| `remainingMileage` | number \| null | ECU remaining range (×10 m) |
| `remainingMileage_m` | number \| null | Remaining range in meters |
| `runTime` | number \| null | ECU driving time (seconds) |
| `run_time_s` | number \| null | Alias |
| `gps_speed_kmh` | number \| null | Top-level alias of `derived.gps_speed_kmh` |
| `segment_m` | number \| null | Distance since last GPS point (m) |
| `session_distance_m` | number \| null | Cumulative GPS distance since bridge session start (m) |

### `derived` object (computed by bridge, not in OMNI packet)

| Field | Type | Description |
|-------|------|-------------|
| `derived.gps_speed_kmh` | number \| null | Instantaneous speed from last two GPS points |
| `derived.segment_m` | number | Meters between last two points |
| `derived.session_distance_m` | number | Session cumulative distance (m) |

`derived` is `null` when GPS was not persistable (no motion tracking for that packet).

### Persistence flags

| Field | Type | Meaning |
|-------|------|---------|
| `db_saved` | boolean | `true` if `scooters` row was updated |
| `ride_saved` | boolean | `true` if an active `rides` row was updated |
| `ride_id` | number \| null | Ride id when `ride_saved` is true |

---

## When is `db_saved: false`?

GPS must pass `isLocationPersistable()` in `mqtt/telemetry.js`:

- `lat` and `lng` parsed successfully
- Raw `LAT` and `LNG` strings are non-empty
- `gpsNum > 0` (at least one satellite locked)

If validation fails:

- Socket event **`scooter-location` is still emitted** (with last known telemetry)
- `db_saved` is `false`
- `scooters.lat` / `lng` / `speed` are **not** updated for that packet
- Log line: `LOCATION skipped DB persist for {imei} — invalid GPS`

---

## Example payload (live device)

IMEI `862499071894209`, valid GPS:

```json
{
  "imei": "862499071894209",
  "scooter_id": 38,
  "scooter_code": "862499071894209",
  "type": "location",
  "received_at": "2026-07-13T13:47:55.277Z",
  "LNG": "04642.401366",
  "LAT": "2442.586246",
  "GEO_NS": "N",
  "GEO_EW": "E",
  "gpsNum": 16,
  "hdop": "1.55",
  "altitude": 628.2,
  "altitude_raw": "628.2,M",
  "timestamp": "1783950474",
  "lat": 24.709770766666665,
  "lng": 46.70668943333333,
  "coord_format": "nmea",
  "points_count": 1,
  "speedKmh": null,
  "singleRideMile": null,
  "runTotalMile": null,
  "remainingMileage": null,
  "remainingMileage_m": null,
  "runTime": null,
  "derived": {
    "gps_speed_kmh": 0.21,
    "segment_m": 1.74,
    "session_distance_m": 885.77
  },
  "db_saved": true,
  "ride_saved": false,
  "ride_id": null,
  "speed_kmh": null,
  "trip_distance_m": null,
  "total_distance_m": null,
  "gps_speed_kmh": 0.21,
  "segment_m": 1.74,
  "session_distance_m": 885.77
}
```

---

## Important: `updateLocation` is NOT for scooters

The generic Socket event `updateLocation` in `socket/socket.js` is for **user/delegate** position tracking (delivery-style features). It:

- Updates `lat` / `lng` on the authenticated user row (`users`, `providers`, etc.)
- Emits `track-info` to room `delegate:{user_id}`

It does **not** update scooter coordinates. For scooter maps, listen to **`scooter-location`** only.

---

## Related socket events (no GPS coordinates)

| Event | Contains lat/lng? | Notes |
|-------|-------------------|-------|
| `scooter-sign` | No | Device check-in, battery, signal |
| `scooter-heartbeat` | No | Lock state, battery, CSQ (~4 min) |
| `scooter-vehicle-info` | No | ECU speed, trip mileage, `QRCode` |
| `scooter-lock-event` | No | Lock/unlock events |
| `scooter-alarm` | No | Alarm reports |

---

## Verify in logs and SQL

**Log file:** `node/logs/mqtt-scooter.log`

```
[MQTT] LOCATION 862499071894209
[MQTT][DB] Updated scooter id=38 imei=862499071894209
```

**SQL:**

```sql
SELECT id, code, serial_number, lat, lng, speed, updated_at
FROM scooters
WHERE serial_number = '862499071894209';
```

**MQTT topic pattern:**

```
om/client/data/location/{IMEI}
```
