← Back to User Guide

PigStick API Reference

REST API and WebSocket interface for programmatic access to PigStick devices.

Base URL: All endpoints are served from the device at http://10.0.0.1 or http://pigstick-XXXX.local. All responses are application/json unless otherwise noted.

Table of Contents

1. Overview

The PigStick exposes a REST API over HTTP and two WebSocket endpoints for real-time streaming. The API requires no authentication—access is controlled by WiFi connectivity to the device's network.

Conventions

Quick Reference

MethodEndpointDescription

2. Device & Status

GET /api/v1/device

Returns device identity, firmware version, and hardware capabilities.

Response

{
  "device": {
    "serial": "PS2609A0110",
    "name": "pigstick-0110",
    "firmware": "2.0.0",
    "uptime": 3621,
    "bootCount": 47,
    "capabilities": {
      "audio": true,
      "magnetic": true,
      "gps": true,
      "battery": true
    },
    "limits": {
      "maxWsClients": 2,
      "maxEvents": 103
    }
  }
}
FieldTypeDescription
serialstringDevice serial number (PS format)
namestringDevice name (matches WiFi AP SSID)
firmwarestringFirmware version
uptimenumberSeconds since boot
bootCountnumberTotal boot cycles
capabilitiesobjectAvailable hardware features
limits.maxWsClientsnumberMaximum concurrent WebSocket clients
limits.maxEventsnumberMaximum stored events before FIFO eviction
GET /api/v1/status

Comprehensive device state snapshot including recorder, GPS, power, and connectivity.

Response

{
  "status": {
    "timestamp": 1773868800000,
    "device": "pigstick-0110",
    "serial": "PS2609A0110",
    "firmware": "2.0.0",
    "app": "2.0.2",
    "uptime": 3621,
    "storage": {
      "eventCount": 12,
      "total": 10379264,
      "used": 1245184,
      "free": 9134080,
      "evictionCount": 0
    },
    "job": {
      "client": "Acme Pipeline",
      "jobName": "Run-042",
      "operator": "J. Smith"
    },
    "recorder": {
      "state": "armed",
      "eventCount": 12,
      "freeSpace": 9134080,
      "captureProgress": null,
      "holdoffRemainingMs": null,
      "lastTriggerTime": 1773868500000,
      "capturesSinceBoot": 3,
      "autoArm": false,
      "audioEnabled": true
    },
    "gps": {
      "timeSynced": true,
      "fix": true,
      "fixQuality": 1,
      "satellites": 7,
      "lat": 30.267153,
      "lon": -97.743057,
      "altitude": 149.20,
      "speedKnots": 0.04,
      "speedMps": 0.0206,
      "utcEpochMs": 1773868800000
    },
    "power": {
      "voltage": 4.05,
      "percent": 82,
      "charging": false,
      "source": "usb"
    },
    "temperature": 42.5,
    "wifiConnected": false,
    "mqttConnected": false,
    "iotJobsBusy": false,
    "startupBehavior": {
      "armOnBoot": false
    }
  }
}
FieldTypeDescription
timestampnumberCurrent UTC epoch milliseconds
jobobject|nullActive job info, or null if none
recorder.statestringOne of: idle, armed, capturing, writing, holdoff
recorder.captureProgressnumber|null0–100 during capture, null otherwise
recorder.holdoffRemainingMsnumber|nullMilliseconds remaining in hold-off, null otherwise
gps.lat, gps.lonnumberConditional: only present when fix is true
gps.utcEpochMsnumberConditional: only present when GPS time is valid
temperaturenumberMCU temperature in °C (omitted if sensor error)
otaobjectConditional: only present during OTA update
GET /api/v1/power

Battery voltage, charge percentage, and power source.

Response

{
  "power": {
    "voltage": 4.05,
    "percent": 82,
    "charging": false,
    "source": "usb"
  }
}
GET /api/v1/temperature

Internal MCU temperature reading.

Response

{
  "temperatureC": 42.50,
  "temperatureF": 108.50
}

Error Response

{
  "error": "sensorReadFailed"
}
GET /api/v1/time

System clock status. Time is synced from GPS satellites.

Response (synced)

{
  "synced": true,
  "time": "2026-03-17T14:30:00"
}

Response (not synced)

{
  "synced": false
}

3. Recorder Control

The event recorder follows a state machine: IDLE → ARMED → CAPTURING → WRITING → HOLDOFF → ARMED. Configuration changes are rejected while a capture is in progress.

GET /api/v1/recorder/status

Current recorder state, capture statistics, and active job context.

Response

{
  "recorder": {
    "state": "armed",
    "eventCount": 12,
    "freeSpace": 9134080,
    "captureProgress": null,
    "holdoffRemainingMs": null,
    "lastTriggerTime": 1773868500000,
    "capturesSinceBoot": 3,
    "autoArm": false,
    "audioEnabled": true,
    "activeJob": {
      "client": "Acme Pipeline",
      "jobName": "Run-042",
      "clientDir": "Acme_Pipeline",
      "jobDir": "Run-042",
      "createdAt": 1773782400,
      "notes": ""
    }
  }
}
GET /api/v1/recorder/config

Current trigger, audio, and speed estimation configuration.

Response

{
  "config": {
    "trigger": {
      "mode": "zeroCrossing",
      "thresholdPercent": 50,
      "holdoffMs": 5000,
      "preDurationMs": 10000,
      "postDurationMs": 10000
    },
    "audio": {
      "enabled": true,
      "sampleRate": 8000
    },
    "speed": {
      "intervalWindow": 5,
      "segmentLengthFt": 40,
      "pigThreshold": 30
    },
    "autoArm": false,
    "activeJob": { ... }
  }
}
FieldTypeDescription
trigger.modestringzeroCrossing, threshold, or combined
trigger.thresholdPercentnumber0–100, trigger level for threshold mode
trigger.holdoffMsnumberMinimum time between captures (ms)
trigger.preDurationMsnumberPre-trigger buffer duration (ms)
trigger.postDurationMsnumberPost-trigger capture duration (ms)
audio.enabledbooleanWhether audio is captured with events
speed.segmentLengthFtnumberPipeline segment length in feet
POST /api/v1/recorder/config

Update recorder configuration. All fields are optional—only provided fields are changed.

Request Body

{
  "triggerMode": "threshold",
  "threshold": 40,
  "holdoffMs": 3000,
  "audioEnabled": true,
  "autoArm": false
}
FieldTypeDescription
triggerModestringzeroCrossing, threshold, or combined
thresholdnumber0–100 percent of full scale
holdoffMsnumberHold-off time in milliseconds
preDurationMsnumberPre-trigger buffer (ms)
postDurationMsnumberPost-trigger capture (ms)
audioEnabledbooleanEnable/disable audio capture
autoArmbooleanArm on boot
intervalWindownumberSpeed estimation window
segmentLengthFtnumberSegment length in feet
pigThresholdnumberSpeed detection threshold
browserTimenumberEpoch seconds—sets device clock if GPS not synced

Response

{"status": "ok"}

Errors

Broadcast: Sends {"event":"configChanged"} to all WebSocket clients.
POST /api/v1/recorder/arm

Arm or disarm the event recorder.

Request Body

{"armed": true}

Response

{
  "recorder": {
    "state": "armed",
    "armed": true
  }
}
Broadcast: Sends {"event":"armState","armed":true} to all WebSocket clients.

4. Events

Events are recorded pig passages containing magnetic waveform data and optional geophone audio. Each event has a numeric ID and is stored on the device's flash filesystem. The device can store up to 103 events; older events are automatically evicted when storage is full.

GET /api/v1/events

List all event metadata for the active job.

Response

{
  "events": [
    {
      "id": 1,
      "timestamp": 1773868500000,
      "triggerSample": 2000,
      "magSamples": 4000,
      "magRate": 200,
      "audioSamples": 160000,
      "audioRate": 8000,
      "peakMagnetic": 1842,
      "triggerMode": 0,
      "threshold": 50,
      "latitude": 30.267153,
      "longitude": -97.743057,
      "hasAudio": true,
      "magDataSize": 8000,
      "audioDataSize": 40000
    }
  ],
  "total": 1,
  "freeSpace": 9134080
}
FieldTypeDescription
idnumberEvent ID
timestampnumberUTC epoch milliseconds at trigger
triggerSamplenumberSample index of trigger point
magSamplesnumberTotal magnetic samples
magRatenumberMagnetic sample rate (Hz)
audioSamplesnumberTotal audio samples (0 if no audio)
audioRatenumberAudio sample rate (Hz)
peakMagneticnumberPeak magnetic reading (raw ADC)
triggerModenumber0 = zero crossing, 1 = threshold, 2 = combined
hasAudiobooleanWhether audio data is available
magDataSizenumberMagnetic data size in bytes
audioDataSizenumberADPCM audio data size in bytes
GET /api/v1/events/{id}

Single event metadata with additional fields.

Response

{
  "id": 1,
  "timestamp": 1773868500000,
  "triggerSample": 2000,
  "magSamples": 4000,
  "magRate": 200,
  "audioSamples": 160000,
  "audioRate": 8000,
  "peakMagnetic": 1842,
  "triggerMode": 0,
  "threshold": 50,
  "latitude": 30.267153,
  "longitude": -97.743057,
  "hasAudio": true,
  "magDataSize": 8000,
  "audioDataSize": 40000,
  "deviceSerial": "PS2609A0110",
  "agmSiteNumber": "AGM-001",
  "cursorSample": 2150
}
FieldTypeDescription
deviceSerialstringSerial of device that recorded the event
agmSiteNumberstringUser-assigned site identifier
cursorSamplenumber|nullSaved cursor position (sample index), or null

Errors

GET /api/v1/events/{id}/magnetic

Raw magnetic waveform data as a binary array.

Response

Content-Type: application/octet-stream

Errors

GET /api/v1/events/{id}/audio

Geophone audio data encoded as IMA ADPCM.

Response

Content-Type: application/octet-stream

Errors

POST /api/v1/events/{id}/cursor

Set or clear the cursor position on a recorded event.

Request Body

{"sample": 2150}

Set to null to clear: {"sample": null}

Response

{"ok": true}
Broadcast: Sends {"event":"cursorUpdated","id":1,"sample":2150} to all WebSocket clients.
POST /api/v1/events/refresh

Force rebuild of the event manifest from disk. Use after manual file operations.

Response

Same format as GET /api/v1/events.

DELETE /api/v1/events/{id}

Delete a single event.

Response

{"status": "ok"}
Broadcast: Sends {"event":"eventDeleted","id":1} to all WebSocket clients.
DELETE /api/v1/events

Delete all events in the active job.

Response

{"status": "ok"}
Broadcast: Sends {"event":"eventsCleared"} to all WebSocket clients.

5. Job Management

Events are organized under jobs, grouped by client. One job is active at a time—new events are recorded into the active job. Jobs are stored on-device and persist across reboots.

GET /api/v1/jobs

List all jobs grouped by client, with the active job indicated.

Response

{
  "active": "Acme_Pipeline/Run-042",
  "clients": [
    {
      "client": "Acme_Pipeline",
      "jobs": [
        {
          "jobName": "Run-042",
          "operator": "J. Smith",
          "pipelineSegment": "Segment-A",
          "siteNameTemplate": "AGM-{n:3}",
          "notes": "",
          "createdAt": 1773782400,
          "siteNameCounter": 4
        }
      ]
    }
  ]
}
FieldTypeDescription
activestring|nullPath of active job (Client/Job), or null
clients[].clientstringClient directory name
jobs[].jobNamestringJob directory name
jobs[].operatorstringOperator/technician name
jobs[].siteNameTemplatestringAuto-numbering template (e.g. AGM-{n:3})
jobs[].createdAtnumberEpoch seconds when job was created
jobs[].siteNameCounternumberNext site number for auto-generation
POST /api/v1/jobs

Create a new job. Becomes the active job by default.

Request Body

{
  "client": "Acme Pipeline",
  "jobName": "Run-043",
  "operator": "J. Smith",
  "pipelineSegment": "Segment-B",
  "siteNameTemplate": "AGM-{n:3}",
  "notes": "",
  "setActive": true
}
FieldTypeRequiredDescription
clientstringYesClient or company name
jobNamestringYesJob identifier
operatorstringNoOperator name
pipelineSegmentstringNoPipeline segment
siteNameTemplatestringNoSite number template
notesstringNoJob notes
siteNameCounternumberNoStarting site number
setActivebooleanNoActivate immediately (default: true)

Response

{
  "status": "ok",
  "clientDir": "Acme_Pipeline",
  "jobDir": "Run-043"
}
PUT /api/v1/jobs/{client}/{job}

Update job metadata. All fields are optional—only provided fields are changed.

Request Body

{
  "operator": "A. Jones",
  "notes": "Updated notes"
}

Response

{"status": "ok"}

Errors

POST /api/v1/jobs/{client}/{job}/activate

Switch the active job. New events will be recorded into this job.

Response

{"status": "ok"}

Errors

DELETE /api/v1/jobs/{client}/{job}

Delete a job and all its events. Cannot delete the currently active job.

Response

{"status": "ok"}

Errors

Destructive: All events within the job are permanently deleted.
GET /api/v1/jobs/{client}/{job}/export

Stream the entire job as a ZIP archive.

Response

Content-Type: application/zip

The archive contains:

Errors

6. GPS

GET /api/v1/gps

GPS receiver status, satellite count, and position (when fix is available).

Response (with fix)

{
  "gps": {
    "timeSynced": true,
    "fix": true,
    "fixQuality": 1,
    "satellites": 7,
    "lat": 30.267153,
    "lon": -97.743057,
    "altitude": 149.20,
    "speedKnots": 0.04,
    "speedMps": 0.0206,
    "utcEpochMs": 1773868800000
  }
}

Response (no fix)

{
  "gps": {
    "timeSynced": false,
    "fix": false,
    "fixQuality": 0,
    "satellites": 0
  }
}
FieldTypeDescription
timeSyncedbooleanWhether system clock is GPS-synced
fixbooleanWhether GPS has a position fix
fixQualitynumber0 = no fix, 1 = GPS, 2 = DGPS
satellitesnumberSatellites in use
latnumberLatitude (conditional: fix = true)
lonnumberLongitude (conditional: fix = true)
altitudenumberAltitude in meters (conditional)
speedKnotsnumberSpeed in knots (conditional)
speedMpsnumberSpeed in m/s (conditional)
utcEpochMsnumberGPS UTC time in ms (conditional: time valid)

7. WiFi

GET /api/v1/wifi

WiFi mode, access point info, and station connection status.

Response

{
  "wifi": {
    "mode": "AP+STA",
    "ap": {
      "ssid": "pigstick-0110",
      "channel": 6,
      "clients": 1
    },
    "sta": {
      "connected": true,
      "ssid": "FieldOffice-5G",
      "rssi": -52,
      "ip": "192.168.1.105"
    }
  }
}
GET /api/v1/wifi/scan

Scan for available WiFi networks. This is a blocking operation that takes a few seconds.

Response

{
  "networks": [
    {
      "ssid": "FieldOffice-5G",
      "rssi": -52,
      "secure": true
    },
    {
      "ssid": "OpenNetwork",
      "rssi": -78,
      "secure": false
    }
  ]
}
GET /api/v1/wifi/networks

List saved WiFi networks. Passwords are not returned.

Response

{
  "networks": [
    {
      "ssid": "FieldOffice-5G",
      "priority": 0
    }
  ]
}
POST /api/v1/wifi/networks

Save a WiFi network. The device stores up to 5 networks.

Request Body

{
  "ssid": "FieldOffice-5G",
  "password": "secretpass"
}

Response

{"success": true}

Errors

DELETE /api/v1/wifi/networks

Delete a saved WiFi network.

Request Body

{"ssid": "FieldOffice-5G"}

Response

{"success": true}

Errors

8. System

GET /api/v1/system/info

Firmware version and storage statistics.

Response

{
  "system": {
    "firmware": "2.0.0",
    "storage": {
      "total": 10379264,
      "used": 1245184,
      "free": 9134080
    },
    "uptime": 3621
  }
}
POST /api/v1/system/restart

Restart the device. The restart occurs after a 1-second delay to allow the response to be sent.

Response

{
  "restart": {
    "scheduled": true,
    "delayMs": 1000
  }
}
Warning: The device will be unreachable for several seconds during restart. Reconnect to the WiFi network after the device reboots.

9. WebSocket Streams

The PigStick provides two WebSocket endpoints for real-time sensor data. Both support up to 2 concurrent clients. Connect using standard WebSocket protocol.

WS /ws/audio

Real-time geophone audio stream.

Binary Frames (device → client)

Text Commands (client → device)

FieldTypeDescription
freqnumberTest tone frequency (Hz)
ampnumberTest tone amplitude (0–100%)
preampnumberHardware pre-amp gain multiplier
armednumber1.0 = arm, 0.0 = disarm
thresholdnumberTrigger threshold (0–100%)

Send as JSON text frame, e.g.: {"preamp": 4.0}

WS /ws/magnetic

Real-time magnetic sensor stream with JSON event notifications.

Binary Frames (device → client)

Each frame contains batched samples with the following per-sample structure:

struct MagneticSample {
    int16_t voltage;    // Raw ADC reading
    int16_t envelope;   // Peak envelope value
};  // 4 bytes per sample

JSON Event Frames (device → client)

Text frames carry real-time event notifications:

EventDescription
triggerTrigger detected: {"event":"trigger","peak":1842,"time":1773868500000}
recordingStartedCapture began: {"event":"recordingStarted","eventId":5}
recordingCompletedEvent saved: {"event":"recordingCompleted","eventId":5, ...}
armStateArm state changed: {"event":"armState","armed":true}
configChangedRecorder config updated
cursorUpdatedCursor set on event: {"event":"cursorUpdated","id":1,"sample":2150}
eventDeletedEvent removed: {"event":"eventDeleted","id":1}
eventsClearedAll events deleted
jobCreatedNew job: {"event":"jobCreated","client":"...","job":"..."}
jobActivatedActive job switched
jobUpdatedJob metadata changed
jobDeletedJob removed

10. Error Handling

HTTP Status Codes

CodeMeaning
200Success
400Bad request (invalid input, missing fields)
404Resource not found (event, job, file)
409Conflict (e.g., config change during capture)
413Payload too large
500Internal server error

Error Response Format

All errors return a JSON object with an error field:

{
  "error": "Description of what went wrong"
}

Common Errors

ErrorEndpointCause
Event not foundEventsInvalid event ID
Job not foundJobsInvalid client/job path
Config rejected (capturing in progress)Recorder configCannot change config while capturing
Invalid triggerModeRecorder configMust be zeroCrossing, threshold, or combined
Maximum networks reachedWiFiAlready 5 saved networks
No audio dataEvent audioEvent was captured without audio