Boat Position Tracker

A WordPress plugin that receives live GPS positions from a boat, stores them in a database, and provides a live map view and historical logbook.

Pages

Live map — /boat-position/map

Logbook — /boat-position/history

REST API endpoints

POST /wp-json/boat-position/v1/ingest — Ingest position

Called by the router on the boat. Requires API key authentication.

POST fields:

FieldTypeDescription
apikeystringSecret key — set in Settings → Boat Position or as PCIO_BOAT_POSITION_API_KEY in wp-config.php
latfloatLatitude (−90 to 90)
lonfloatLongitude (−180 to 180)
speedfloatSpeed in knots (≥ 0)
coursefloatHeading in degrees (optional, defaults to 0.0)
gps_timestringGPS timestamp (ISO 8601 / UTC)

Response: 200 OK with {"status":"ok","id":N} on success, or a WP REST error object on failure.

GET /wp-json/boat-position/v1/latest — Current position

Returns the most recent position record as JSON.

{
  "lat": 55.123,
  "lon": 12.456,
  "speed": 5.2,
  "course": 270.0,
  "time": 1716400000
}

GET /wp-json/boat-position/v1/trips — Trip data

RouteDescription
GET /tripsAll closed trips, newest first
GET /trips/active-datesArray of YYYY-MM-DD strings that have at least one trip (used to highlight calendar days)
GET /trips/{id}/pointsAll legs with waypoints for a trip (used to draw the route on the map)
POST /trips/{id}/harboursUpdate harbour names for a trip (requires WP editor role)
POST /trips/mergeMerge two trips into one (requires WP editor role)
GET /harboursAll known harbours with coordinates and radius
GET /legs/{id}/pointsRaw waypoints for a single leg

Trip engine state machine

The class-pcio-bp-trip-engine class processes raw positions (where leg_id IS NULL) and assigns them to trips and legs.

Trip engine state machine diagram

Tuning constants (define in wp-config.php to override defaults):

ConstantDefaultDescription
PCIO_BP_SPEED_UNDERWAY_KN1.5Knots above which the boat is considered moving
PCIO_BP_STOP_CONFIRM_COUNT3Consecutive slow readings needed to close a trip
PCIO_BP_NO_DATA_GAP_SECS180Gap (seconds) that triggers an estimated leg
PCIO_BP_TRIP_END_GAP_SECS28800Gap (seconds) that closes the trip entirely (8 h)
PCIO_BP_BATCH_SIZE500Max positions to process per call

Database tables

All tables use the WordPress table prefix (default wp_).

TableDescription
wp_boat_positionsRaw GPS positions (lat, lon, speed, course, gps_time_utc, leg_id)
wp_boat_tripsOne row per sailing trip (started_at, ended_at, distance_nm, harbour_start, harbour_end, state)
wp_boat_legsIndividual legs within a trip; estimated legs are flagged for display as dashed lines
wp_boat_harboursKnown harbours used to label trip start/end points

Configuration

The API key can be set in Settings → Boat Position, or hard-coded in wp-config.php:

define( 'PCIO_BOAT_POSITION_API_KEY', 'your-secret-key' );

When the constant is defined, the settings field is shown as read-only and the database option is ignored.

Pages (summary)

Both URLs are registered automatically on plugin activation. If they return 404, go to Settings → Permalinks and click Save Changes to flush the rewrite rules.

Testing the ingest endpoint

Send a single test position from a command prompt (Windows):

curl.exe -X POST https://<your-site>/wp-json/boat-position/v1/ingest ^
  -d "apikey=<secret-key>" ^
  -d "lat=55.10" ^
  -d "lon=10.10" ^
  -d "speed=7.1" ^
  -d "course=182.4" ^
  -d "gps_time=2026-05-19T18:30:00Z"

A successful response is {"status":"ok","id":N}. Any other response will include a message field describing the problem.

Configuring the Wifi router

The router reads GPS data and POSTs it to the ingest endpoint once per minute via a cron job.

1. Enable GPS on the router

Connect to the router admin UI on the local Wi-Fi and navigate to Services → GPS. Enable GPS and confirm a fix is acquired before continuing.

To verify from the command line (step 2 first):

ubus call gpsd position

The response includes a fix_curr_mode field. A value of 2 means a 2D fix; 3 means a 3D fix. Values below 2 mean no valid fix — wait until this is ≥ 2 before relying on positions.

2. SSH into the router

ssh root@192.168.1.1

3. Create the sendgps.sh script

vi /home/root/sendgps.sh

Paste the following content (replace the site URL and API key):

#!/bin/sh

GPS=$(ubus call gpsd position)

LAT=$(echo "$GPS"    | jsonfilter -e '@.latitude')
LON=$(echo "$GPS"    | jsonfilter -e '@.longitude')
SPEED=$(echo "$GPS"  | jsonfilter -e '@.speed_vtg_knots')
COURSE=$(echo "$GPS" | jsonfilter -e '@.angle')
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
FIX=$(echo "$GPS"    | jsonfilter -e '@.fix_curr_mode')

if [ "$FIX" -lt 2 ]; then
    echo "No valid GPS fix (mode=$FIX)"
    exit 1
fi

curl -X POST https://<your-site>/wp-json/boat-position/v1/ingest \
  -d "apikey=<secret-key>" \
  -d "lat=$LAT" \
  -d "lon=$LON" \
  -d "speed=$SPEED" \
  -d "course=$COURSE" \
  -d "gps_time=$TIME"

4. Make the script executable

chmod +x /home/root/sendgps.sh

5. Test the script manually

Run it once and confirm you get {"status":"ok",...} from the server:

sh /home/root/sendgps.sh

6. Add a cron job

crontab -e

Add this line to run the script every minute:

*/1 * * * * /home/root/sendgps.sh

Save and exit. The router will now POST a position every minute whenever it has a valid GPS fix and a cellular/internet connection.

vi quick reference

CommandAction
iEnter insert mode
Esc then :wq + EnterSave and exit
Esc then :q! + EnterExit without saving