# MQTT Lock / Unlock API

HTTP API on the Node server to lock and unlock scooters via the OMNI MQTT protocol (Real-Time Control §2.3.1).

**Protocol reference:** [`EngOMNI-Protocal-MQTT.JSON-V1.18_251111-3.pdf`](../docs/EngOMNI-Protocal-MQTT.JSON-V1.18_251111-3.pdf) — section **2.3.1 Real Time Control**

**Related:** [MQTT.md](./MQTT.md) (bridge overview) · [SOCKET-SCOOTER-LOCATION.md](./SOCKET-SCOOTER-LOCATION.md) (live telemetry)

---

## Base URL

| Environment | URL |
|-------------|-----|
| Local | `http://127.0.0.1:5055/mqtt` |
| Production | `http://65.109.35.85:5055/mqtt` or `https://beebbeeb.sa.com:5055/mqtt` |

IMEI in the path is the device serial (`scooters.serial_number` in the database).

---

## Authentication

When `MQTT_API_TOKEN` is set in `.env`, every request must include:

```
X-Mqtt-Token: {MQTT_API_TOKEN}
```

Or pass `?token={MQTT_API_TOKEN}` as a query parameter.

---

## Protocol flow

Unlock is a **multi-step sequence**. MQTT ack `code: 200` alone does **not** mean the scooter is physically unlocked.

**Device polarity note:** On this fleet firmware, `vehicleLock` is **inverted** vs the OMNI PDF / Laravel defaults:

| API action | `vehicleLock` sent | `accCTL` | Expected lock-event |
|------------|--------------------|----------|---------------------|
| Unlock | `1` | `1` | `operateType: 0`, `lockType: 1`, `operateResult: 1` |
| Lock | `0` | `0` | `operateType: 1` (or telemetry locked) |

```mermaid
sequenceDiagram
    participant Client as HTTP_Client
    participant Node as Node_MQTT_Bridge
    participant Device as Scooter_IoT

    Client->>Node: POST unlock
    Node->>Device: send-rto vehicleLock=1 accCTL=1
    Device->>Node: res send-rto code=200
    Node->>Device: set-ecu lockMotor=0
    Node->>Device: send-query scooterInfo=1
    Device->>Node: lock-event operateType=0 operateResult=1
    Node->>Client: verified=true only if physically unlocked
```

| Step | Topic | Purpose |
|------|-------|---------|
| 1 | `om/server/req/send-rto/{IMEI}` | Vehicle unlock + throttle on |
| 2 | `om/server/req/set-ecu/{IMEI}` | Disable motor lock (`lockMotor: 0`) |
| 3 | `om/server/req/send-query/{IMEI}` | Request fresh `scooter-info` |
| Ack | `om/client/res/send-rto/{IMEI}` | Command received (not physical unlock) |
| Confirm | `om/client/data/lock-event/{IMEI}` | Physical result |

### lock-event fields

| Field | PDF meaning | This firmware (Node API) |
|-------|-------------|--------------------------|
| `operateType` | `0` = lock, `1` = unlock | Inverted: `0` correlates with physical unlock, `1` with lock |
| `lockType` | `1` = vehicle, `2` = cable, `3` = battery, `4` = hub | Same |
| `operateResult` | `1` = success, `0` = failed | Same |
| `errorCode` | `1` = controller comm fail, `2` = improper lock position | Same |

**Success criteria (unlock):** lock-event with `operateType=0`, `lockType=1`, `operateResult=1` OR telemetry `lockSta=0` / `vehicleState=1`.

---

## Endpoints

### `GET /mqtt/health`

Check bridge and broker status.

**Response 200:**

```json
{
  "enabled": true,
  "connected": true,
  "broker": "65.109.35.85:1883",
  "topic_prefix": "om",
  "response_timeout_seconds": 15
}
```

---

### `POST /mqtt/scooters/:imei/unlock`

Unlock the scooter (vehicle lock off, throttle on, motor lock disabled).

**OMNI payload (server → device, minimal — inverted polarity for this firmware):**

```json
{
  "operationId": 1234,
  "accCTL": 1,
  "lockCTL": { "vehicleLock": 1 },
  "timestamp": "1599206433"
}
```

Only include `batLock` / `cableLock` / etc. when explicitly passed — PDF states disable commands are **invalid** for battery/cable locks.

**Optional JSON body / query:**

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `wait` | boolean | `true` | Wait for MQTT acks |
| `verify` | boolean | `true` | Require physical unlock confirmation |
| `batLock` | integer | omitted | Battery lock override (advanced) |
| `cableLock` | integer | omitted | Cable lock override |
| `operationId` | integer | random | Correlation id (0–65535) |

**Example:**

```bash
curl -X POST "http://127.0.0.1:5055/mqtt/scooters/862499071894209/unlock" \
  -H "Content-Type: application/json" \
  -H "X-Mqtt-Token: YOUR_TOKEN"
```

**Success 200 (physically verified):**

```json
{
  "key": "success",
  "status": 200,
  "data": {
    "action": "unlock",
    "imei": "862499071894209",
    "topic": "om/server/req/send-rto/862499071894209",
    "request": {
      "operationId": 48291,
      "timestamp": "1721049600",
      "lockCTL": { "vehicleLock": 1 },
      "accCTL": 1
    },
    "mqtt_ack": { "code": 200, "operationId": 48291, "timestamp": "1721049601" },
    "set_ecu": { "topic": "om/server/req/set-ecu/862499071894209", "response": { "code": 200 } },
    "lock_event": {
      "operateType": 0,
      "lockType": 1,
      "operateResult": 1,
      "errorCode": 0,
      "is_unlock": true,
      "is_success": true,
      "is_vehicle_lock": true
    },
    "verified": true,
    "verification_source": "lock_event",
    "lock_sta": 0,
    "vehicle_state": 1,
    "lock_state": "1111",
    "waited": true
  }
}
```

**502 — ack OK but still locked:**

```json
{
  "key": "fail",
  "message": "Unlock command acknowledged but device remains locked (no vehicle unlock confirmation)",
  "status": 502,
  "data": {
    "action": "unlock",
    "mqtt_ack": { "code": 200 },
    "verified": false,
    "lock_sta": 1
  }
}
```

---

### `POST /mqtt/scooters/:imei/lock`

Lock the scooter (vehicle lock on, throttle off).

**OMNI payload (server → device):**

| Field | Value |
|-------|-------|
| `lockCTL.vehicleLock` | `0` (lock — inverted polarity) |
| `accCTL` | `0` (throttle off) |

Same optional body fields as unlock (default `accCTL` is `0`).

**Example:**

```bash
curl -X POST "http://127.0.0.1:5055/mqtt/scooters/862499071894209/lock" \
  -H "Content-Type: application/json" \
  -H "X-Mqtt-Token: YOUR_TOKEN"
```

**Success 200:** Same shape as unlock with `"action": "lock"`.

---

## Fire-and-forget mode

Skip waiting for the device response (legacy behavior):

```bash
curl -X POST "http://127.0.0.1:5055/mqtt/scooters/862499071894209/unlock?wait=false" \
  -H "X-Mqtt-Token: YOUR_TOKEN"
```

Response includes `"waited": false` and `"response": null`.

---

## Error responses

| HTTP | When | Example `message` |
|------|------|-------------------|
| `400` | Invalid IMEI | `IMEI must be 10–20 digits` |
| `401` | Missing/invalid token | `Unauthorized` |
| `502` | Ack OK but lock still engaged / unlock not verified | `Unlock command acknowledged but device remains locked` |
| `503` | MQTT bridge not connected | `MQTT client is not connected` |
| `504` | No device response within timeout | `OMNI MQTT response timed out for IMEI ...` |

Timeout is controlled by `MQTT_RESPONSE_TIMEOUT` in `.env` (default **15** seconds).

Device error example:

```json
{
  "key": "fail",
  "message": "OMNI device command failed with code 500",
  "status": 502,
  "device_response": {
    "code": 500,
    "operationId": 1234,
    "timestamp": "1721049600"
  }
}
```

---

## Socket event (async)

When the physical lock state changes, the device may report:

| Event | When |
|-------|------|
| `scooter-lock-event` | `om/client/data/lock-event/{IMEI}` |

Subscribe via `enter-scooter` room. See [SOCKET-SCOOTER-LOCATION.md](./SOCKET-SCOOTER-LOCATION.md).

---

## Integration test

```bash
cd public_html/node
node scripts/test-mqtt-lock-unlock.js
```

With device offline, simulate MQTT device ack (validates API + `publishAndWait` pipeline):

```bash
TEST_SIMULATE_DEVICE=1 node scripts/test-mqtt-lock-unlock.js
```

Environment variables:

| Variable | Default |
|----------|---------|
| `NODE_HOST` | `127.0.0.1` |
| `NODE_PORT` | `5055` |
| `MQTT_API_TOKEN` | from `.env` |
| `TEST_SCOOTER_IMEI` | `862499071894209` |

---

## Implementation files

| File | Role |
|------|------|
| `mqtt/routes.js` | HTTP endpoints |
| `mqtt/commands.js` | `lockScooter()` / `unlockScooter()` |
| `mqtt/bridge.js` | `publishAndWait()` + pending response map |
| `mqtt/handlers.js` | Handles `res:send-rto`, `res:set-ecu`, `res:send-query`, `lock-event` |
| `mqtt/telemetry.js` | `parseLockEvent()`, `isPhysicallyUnlocked()` helpers |
| `mqtt/config.js` | `MQTT_RESPONSE_TIMEOUT`, `MQTT_API_TOKEN` |

---

## Notes

- This API controls **hardware only**. It does not deduct wallet balance, change ride status, or start/end rides.
- Laravel ride `unlock` uses [`ScooterDeviceService.php`](../app/Services/Scooter/ScooterDeviceService.php) with the same OMNI payload.
- Admin panel lock/unlock uses the same Laravel service; this Node API is for direct HTTP/MQTT control and automation.
