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.
Live map — /boat-position/map
Logbook — /boat-position/history
/wp-json/boat-position/v1/ingest — Ingest positionCalled by the router on the boat. Requires API key authentication.
POST fields:
| Field | Type | Description |
|---|---|---|
apikey | string | Secret key — set in Settings → Boat Position or as PCIO_BOAT_POSITION_API_KEY in wp-config.php |
lat | float | Latitude (−90 to 90) |
lon | float | Longitude (−180 to 180) |
speed | float | Speed in knots (≥ 0) |
course | float | Heading in degrees (optional, defaults to 0.0) |
gps_time | string | GPS timestamp (ISO 8601 / UTC) |
Response: 200 OK with {"status":"ok","id":N} on success, or a WP REST error object on failure.
/wp-json/boat-position/v1/latest — Current positionReturns the most recent position record as JSON.
{
"lat": 55.123,
"lon": 12.456,
"speed": 5.2,
"course": 270.0,
"time": 1716400000
}
/wp-json/boat-position/v1/trips — Trip data| Route | Description |
|---|---|
GET /trips | All closed trips, newest first |
GET /trips/active-dates | Array of YYYY-MM-DD strings that have at least one trip (used to highlight calendar days) |
GET /trips/{id}/points | All legs with waypoints for a trip (used to draw the route on the map) |
POST /trips/{id}/harbours | Update harbour names for a trip (requires WP editor role) |
POST /trips/merge | Merge two trips into one (requires WP editor role) |
GET /harbours | All known harbours with coordinates and radius |
GET /legs/{id}/points | Raw waypoints for a single leg |
The class-pcio-bp-trip-engine class processes raw positions (where leg_id IS NULL) and assigns them to trips and legs.
Tuning constants (define in wp-config.php to override defaults):
| Constant | Default | Description |
|---|---|---|
PCIO_BP_SPEED_UNDERWAY_KN | 1.5 | Knots above which the boat is considered moving |
PCIO_BP_STOP_CONFIRM_COUNT | 3 | Consecutive slow readings needed to close a trip |
PCIO_BP_NO_DATA_GAP_SECS | 180 | Gap (seconds) that triggers an estimated leg |
PCIO_BP_TRIP_END_GAP_SECS | 28800 | Gap (seconds) that closes the trip entirely (8 h) |
PCIO_BP_BATCH_SIZE | 500 | Max positions to process per call |
All tables use the WordPress table prefix (default wp_).
| Table | Description |
|---|---|
wp_boat_positions | Raw GPS positions (lat, lon, speed, course, gps_time_utc, leg_id) |
wp_boat_trips | One row per sailing trip (started_at, ended_at, distance_nm, harbour_start, harbour_end, state) |
wp_boat_legs | Individual legs within a trip; estimated legs are flagged for display as dashed lines |
wp_boat_harbours | Known harbours used to label trip start/end points |
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.
/boat-position/map — Leaflet map using OpenStreetMap/OpenSeaMap. Polls /latest every 30 seconds and renders a rotating arrow icon when underway, or a dot when stopped./boat-position/history — Calendar sidebar highlights days with recorded trips. Clicking a day loads trip routes on the map. WordPress editors can update harbour names and merge trips inline.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.
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.
The router reads GPS data and POSTs it to the ingest endpoint once per minute via a cron job.
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.
ssh root@192.168.1.1
sendgps.sh scriptvi /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"
chmod +x /home/root/sendgps.sh
Run it once and confirm you get {"status":"ok",...} from the server:
sh /home/root/sendgps.sh
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.
| Command | Action |
|---|---|
i | Enter insert mode |
Esc then :wq + Enter | Save and exit |
Esc then :q! + Enter | Exit without saving |