Telemetry

Send a boat's own data to Skiff

Skiff reads what the boat already puts on its own network rather than going through each maker's cloud. Engine data from Volvo Penta, Yanmar, Mercury SmartCraft, Caterpillar, Cummins and other J1939 engines reaches NMEA 2000 through a gateway, and anything on NMEA 2000 can be read by a Signal K server. That means most boats can feed Skiff without an arrangement with the boat's builder.

1. Create a connection

In the Skiff web app, open the vessel, then Connections, and add the system that is fitted. You get a connection key starting with ct_, shown once.

A connection key is not a personal access key. It authenticates one box on one boat, can only write telemetry for that vessel, and can be rotated or removed on its own. When a boat is sold with its hardware still aboard, removing the connection is enough.

2. Push readings

One request carries up to 200 readings. Send no more than one request every five seconds per connection; hour meters and tank levels do not need to arrive any faster than that.

curl -X POST https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/telemetry \
  -H "Authorization: Bearer ct_..." \
  -H "Content-Type: application/json" \
  -d '{
    "readings": [
      { "metric": "engine_hours", "value": 412.4, "unit": "h", "instance": "port" },
      { "metric": "engine_coolant_temp", "value": 82.4, "unit": "C", "instance": "port" },
      { "metric": "fuel_level_pct", "value": 61 },
      { "metric": "fault_code", "text_value": "SPN 110 FMI 0", "instance": "port" }
    ]
  }'

{ "accepted": 4, "rejected": [], "engine_hours_applied": true }

Unknown metric names are reported in rejected and the rest of the batch is still stored, so adding a sensor never costs you the readings that already worked.

Fields

metric and one of value (number) or text_value are required. unit, instance (port, starboard, 0, house) and recorded_at (ISO 8601, defaults to arrival time) are optional.

Metrics

MetricUnitNotes
engine_hourshRun time. Skiff's hour meter takes the whole hours.
engine_rpmrpmCrankshaft speed.
engine_coolant_tempCCoolant temperature.
engine_oil_pressurekPaOil pressure.
fuel_level_pct%Tank level, 0 to 100.
fuel_ratel/hCurrent burn rate.
fuel_usedlFuel consumed.
battery_voltageVTerminal voltage.
battery_soc_pct%State of charge.
water_level_pct%Fresh water tank.
waste_level_pct%Waste tank.
bilge_water0 or 1Water detected in the bilge.
bilge_pump_cyclescountPump starts since the last reading.
shore_power0 or 1Shore power connected.
position_latdegLatitude.
position_londegLongitude.
depthmDepth below transducer.
speed_over_groundknSpeed over ground.
water_tempCSea water temperature.
fault_codetextSent as text_value, for example SPN 110 FMI 0.

How engine hours are handled

Every reading is kept as sent. The vessel's hour meter is separate: it takes whole hours and only moves forward. A reading of 412.4 sets the meter to 412, a later 412.9 changes nothing, and 413.1 moves it to 413. A reading below the current meter is stored as telemetry but leaves the meter alone, so a replaced engine or a gauge that resets cannot disturb the service schedule. Whenever the meter does move, a logbook entry is written with source telemetry and the due feed recalculates.

Signal K

The signalk-skiffplugin does all of the above for you. Install it from the Signal K app store, paste the connection key, and it maps propulsion, tank, battery and environment paths into the metrics above, converting from Signal K's SI units. Per-engine and per-tank instances are preserved. Position is off by default. Source lives in packages/signalk-skiffin this portal's repository.

Engines that are not on NMEA 2000 yet

Most modern diesels speak J1939 and most outboards speak their maker's own bus. A gateway (Yacht Devices YDEG-04 and similar) translates those onto NMEA 2000, after which a Signal K server reads them like anything else. This is usually about an hour of a maintainer's time and needs no cooperation from the engine maker.

Which systems are supported

The catalog is served live, so it stays in step with what Skiff can actually connect. Providers marked pull need the other side to grant access and cannot be connected yet.

curl https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/telemetry/providers \
  -H "Authorization: Bearer sk_live_..."

Skiff never asks an owner for the password to a maker's app, and the API refuses to store one. If a system has no way to grant access, it is listed as unreachable and owners can register interest instead:

curl -X POST https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/telemetry/brand-requests \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"system_name": "Nimbus Connect", "boat_make": "Nimbus"}'

Cameras

A camera on the boat sends stills, so an owner can look in from anywhere. It uses the same connection key as anything else on the boat: create a connection with the provider onboard_camera and point your camera or script at the frames endpoint. Skiff stores stills, never video, and the app always shows how old the picture is.

Pass a label on the first frame and the camera registers itself, up to four per boat. After that you can send the camera_id that comes back. One frame every ten seconds per camera, 8 MiB each.

curl -X POST https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/cameras/frames \
  -H "Authorization: Bearer ct_..." \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Cockpit",
    "view": "cockpit",
    "mime": "image/jpeg",
    "image_base64": "'"$(base64 < frame.jpg)"'"
  }'

{ "frame_id": "…", "camera_id": "…", "answered_check_in": false }

When an owner taps check in, that queues a request rather than waking the boat. Poll for open requests and answer one by posting a frame, which is also the one case where the ten second limit does not apply. Requests past their window are closed for you, so a boat that was offline never answers a day-old tap and calls the picture current.

curl https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/cameras/pending \
  -H "Authorization: Bearer ct_..."

{ "requests": [ { "id": "…", "camera_id": "…", "label": "Cockpit" } ],
  "poll_after_ms": 30000 }

With a personal access key: GET /vessels/{id}/cameras, GET /cameras/{id}/frames (signed URLs, newest first), and POST /cameras/{id}/check-in.

Reading it back

curl "https://binranoynwqpydttvgbs.supabase.co/functions/v1/public-api/v1/vessels/VESSEL_ID/telemetry?metric=engine_hours&limit=50" \
  -H "Authorization: Bearer sk_live_..."

Connections are managed with a personal access key: GET /vessels/{id}/connections, POST /vessels/{id}/connections (returns the connection key once), and DELETE /connections/{id}.