# GPS Tracking (GT06) — Operations & Device Configuration

GT06 trackers speak a **raw TCP binary protocol**, not HTTP/HTTPS. This shapes
everything about how the service is deployed and how devices are pointed at it.

---

## 1. Why GPS needs its own dedicated TCP port

- **Not HTTP.** GT06 devices send binary frames
  (`0x78 0x78 | len | protocol | payload | serial | CRC16 | 0x0D 0x0A`).
  An HTTP server receiving these answers `400 Bad Request` and closes.
- **Dedicated port required.** A listening socket is keyed on `(IP, port)`; two
  listeners on the same pair collide (`EADDRINUSE`). The REST API already owns
  its port, so GPS traffic needs a separate one.
- **No hostname routing.** GT06 frames carry no `Host` header and no TLS SNI, so
  L7 proxies (Nginx `http`, AWS ALB, Cloudflare proxy) cannot route them by
  domain. Routing must happen at **Layer 4 (TCP)** by port.
- **DNS carries no port.** An A record maps a name to an IP only. The tracker
  config must state the port explicitly (e.g. `gps.example.com:5023`).

### Operational requirements

| Item | Value |
| --- | --- |
| Protocol | Raw TCP (not HTTP, not TLS) |
| Default port | `5023` (configurable via `GT06_TCP_PORT`) |
| Listener bind | `0.0.0.0` inside the `freight-gps` container |
| Edge terminator | **L4** — AWS NLB or Nginx `stream {}`. **Not** ALB / Cloudflare proxy. |

---

## 2. Port configuration

`5023` is only this project's default — **not** a GT06 protocol requirement. The
listener binds whatever `GT06_TCP_PORT` says, as long as trackers are configured
with the same number.

Host and container ports are decoupled in `docker-compose.yaml`:

```yaml
freight-gps:
  ports:
    - "${GT06_TCP_PORT:-5023}:5023"   # host is configurable; container fixed
  environment:
    GT06_TCP_PORT: "5023"             # pinned inside the container
```

- The **container** always listens on `5023`.
- The **host/public** port is configurable (443, 5023, 9000, …) via the root
  `.env`'s `GT06_TCP_PORT`.
- This split is required because the image runs as a **non-root** user
  (`nestjs`, uid 1001), which cannot bind ports `<1024`. Docker (root) binds the
  host port and forwards to `5023` inside.
- Running **outside Docker** (`pnpm dev:gps`, systemd), `GT06_TCP_PORT` is the
  actual bind port, so `<1024` needs root or `CAP_NET_BIND_SERVICE`.
- **443 is allowed but risky:** GT06 stays raw TCP, not TLS. Middleboxes that
  expect a TLS handshake on 443 may drop the connection.

---

## 3. Deployment topology

The GT06 listener runs as its own process (`dist/main.gps.js`, module
`GpsIngestModule`) — DB + GPS only, no HTTP server. It shares the `edr_freight`
DB with the API; the DB is the seam (ingester writes `gps_devices` /
`gps_positions`, API reads them).

```
freight-api    HTTP :3001   GT06_TCP_PORT=0        (listener off, applies migrations)
freight-gps    TCP  :5023   DB_MIGRATIONS_RUN=false (owns the tracker socket)
```

`DB_MIGRATIONS_RUN=false` keeps the second process from racing migrations.

Horizontal scale: each tracker holds one long-lived TCP connection with
per-socket session state, so N `freight-gps` replicas can run behind an L4 LB —
each device sticks to one replica. `ensureDevice` is safe under concurrency
(unique IMEI).

---

## 4. Device configuration (GT06 side)

Config is done by **SMS to the tracker's SIM**. Commands below are the canonical
Concox/GT06 set — **verify against your unit's sheet**, syntax varies by firmware.
Default command password is usually `123456`.

Prep: data-enabled SIM, SMS on, **SIM PIN off**, know your carrier APN.

```
STATUS#                          # 1. sanity check — returns GSM/GPS/batt/GPRS
APN,<apn>#                       # 2. carrier data APN (add ,user,pass if needed)
SERVER,1,gps.example.com,5023,0# # 3. point at server (1=domain). Port MUST match GT06_TCP_PORT
GPRSON,1#                        # 4. enable data
GPSON,1#                         #    enable GPS
TIMER,10#                        # 5. upload interval, seconds (some use UPLOAD,10#)
RESET#                           # 6. reboot so it reconnects (many cache DNS until reboot)
```

Raw-IP variant of step 3: `SERVER,0,203.0.113.50,5023,0#`
Custom host port (e.g. 443): `SERVER,1,gps.example.com,443,0#`

### Verify from the server

```bash
docker compose logs -f freight-gps | grep -Ei "login|Auto-registering|ingester up"
nc -vz gps.example.com 5023
curl -H "Authorization: Bearer <token>" https://api.example.com/api/gps/positions/latest
```

First login packet **auto-registers** the IMEI (no manual step). `online:true`
only when `lastSeenAt` < 5 min (computed at read time).

### Link a tracker to a vehicle (optional)

Auto-register leaves `vehicleId` null. Attach it (needs `tracking.manage`):

```
PATCH /api/gps/devices/:id   { "vehicleId": "<uuid>", "name": "Truck 03-ET" }
```

### Failure map

| Symptom | Cause |
| --- | --- |
| No SMS reply | SIM PIN on / no signal / wrong number |
| Replies but never connects | APN wrong, or `SERVER` port ≠ `GT06_TCP_PORT` |
| Connects then drops | server not ACKing, or middlebox on 443 expecting TLS |
| Registered but `online:false` | packets blocked by firewall — open inbound TCP |
| Wrong location / `positioned:false` | no GPS fix yet — open sky, cold start ~1–2 min |

---

## 5. Security

- GT06 authenticates with **IMEI only**, which is **spoofable**. Anyone who can
  reach the port can inject fake positions.
- **Do not** expose the port to `0.0.0.0/0`. Restrict at the firewall / security
  group to the SIM provider's **APN / IP range**.
- Trackers must use the same host+port as the server:
  `SERVER,1,gps.example.com,<port>,0#`.

---

## 6. Edge (L4) termination

See [`infrastructure/nginx/gps-stream.conf`](../../../../../infrastructure/nginx/gps-stream.conf)
for an Nginx `stream {}` example, and the AWS NLB notes in the same file.
Reminder: **L4 only** — an HTTP proxy cannot route GT06.
