Preparing search index...

    Radar API

    The Signal K server Radar API provides a unified interface for viewing and controlling marine radar equipment from any manufacturer. The API is (web)app-friendly: clients can build dynamic UIs that automatically adapt to any radar's capabilities without hardcoding support for specific brands or models.

    This is version v3.1.0 of the API. The version will use semver for version updates.

    Radar functionality is provided by "provider plugins" that handle the interaction with radar hardware and stream spoke data to connected clients.

    Requests to the Radar API are made to HTTP REST endpoints rooted at /signalk/v2/api/vessels/self/radars or the Signal K websocket stream at /signalk/v1/stream.

    Like signalk-server vis-a-vis the Signal K specification there is a reference implementation for this API, which may very well remain the only implementation of the server side of the API, at https://github.com/MarineYachtRadar/mayara-server. However, like Signal K itself, there is no reason it needs to remain the only implementation. In particular it would be ultra cool if some manufacturer of marine hardware would implement this API -- even though this is very unlikely.

    This API uses a self-describing schema pattern that benefits both radar provider developers and client/chartplotter developers.

    Build a single, adaptive UI that works with any radar—now and in the future—without hardcoding brand-specific logic.

    How it works:

    1. Fetch capabilities once when a radar connects — this tells you what the radar can do
    2. Generate UI widgets from the schema:
      • dataType: "number" → Slider with min/max/step
      • dataType: "enum" with descriptions → Dropdown or button group
      • dataType: "string" → Text input field
      • dataType: "button" → Action button
      • dataType: "sector" → Angle range selector (start/end angles)
      • dataType: "zone" → Guard zone editor (angles + distances)
      • dataType: "rect" → Rectangular exclusion zone (two corners + width)
      • isReadOnly: true → Display-only label
    3. Subscribe to updates for current values — the schema tells you what to expect
    4. Connect to websocket for spoke data - receive the binary spoke data stream

    Example: Rendering a Gain Control

    $ curl -s http://10.56.0.1:6502/signalk/v2/api/vessels/self/radars/nav1034A/capabilities | jq '.controls.gain'
    {
    "category": "base",
    "dataType": "number",
    "description": "How sensitive the radar is to returning echoes",
    "hasAuto": true,
    "hasAutoAdjustable": false,
    "id": 4,
    "maxValue": 100.0,
    "minValue": 0.0,
    "name": "Gain",
    "stepValue": 1.0
    }
    $ curl -s http://10.56.0.1:6502/signalk/v2/api/vessels/self/radars/nav1034A/controls/gain
    {"auto":false,"value":58}

    Your UI renders:

    • Mode toggle: [Auto] [Manual]
    • Value slider: 0 ----[58]---- 100 (disabled or hidden when mode=auto)

    Whether it's a Furuno DRS4D-NXT with 20, a Navico HALO with 40 controls or a basic radar with 5 controls, the same client code handles both.

    Different manufacturers have vastly different hardware capabilities, control sets, value ranges, and operating modes. Instead of clients hardcoding knowledge about each model, your provider plugin declares what the radar can do:

    1. Capabilities — hardware capabilities (Doppler, dual-range, no-transmit zones, supported ranges)
    2. Controls — schema for each control (type, valid values, modes, read-only status)
    Category Description Examples
    base Available on all radars power, range, gain, sea, rain
    targets Target tracking settings targetExpansion, targetTrails
    guardZones Guard zone configuration guardZone1, guardZone2
    trails Trail display settings trailsTime, clearTrails
    advanced Model-specific features dopplerMode, beamSharpening, interferenceRejection
    installation Setup/configuration settings antennaHeight, bearingAlignment, noTransmitSector1
    info Read-only information serialNumber, firmwareVersion, transmitTime

    Read-only information (serialNumber, firmwareVersion, operatingHours) is exposed as controls with isReadOnly: true. Some controls are dynamically read-only when a particular mode is set. This is handled with an optional allowed: <bool> field in the control value.

    Some further considerations as how to show controls:

    • Within each category, all controls have a numeric id field which may be used for ordering.
    • The advanced and especially the installation categories could be shown in a different panel.
    • In particular the installation controls are typically configured once.
    • The power and range controls are used often and should be easy to be controlled.
    • The gain, sea and rain controls are usually represented graphically on a PPI window.
    /signalk/v2/api/vessels/self/radars
    ├── GET                              → List all active radars
    ├── /interfaces
    │   └── GET                          → List network interfaces and listener status
    └── /{radar_id}
        ├── /capabilities GET            → Get radar capabilities and control definitions
        ├── /controls
        │   ├── GET                      → Get all control values
        │   └── /{control_id}
        │       ├── GET                  → Get single control value
        │       └── PUT                  → Set single control value
        ├── /spokes                      → WebSocket (spoke data in binary format)
        └── /targets
            ├── GET                      → List tracked targets
            ├── POST                     → Acquire target manually
            └── /{target_id}
                └── DELETE               → Cancel target tracking
    
    /signalk/v1/stream                   → WebSocket (control values and targets for all radars)
    

    Retrieve all available radars with their current info:

    HTTP GET "/signalk/v2/api/vessels/self/radars"
    

    Response:

    {
    "nav1034A": {
    "brand": "Navico",
    "model": "HALO",
    "name": "HALO 034A",
    "radarIpAddress": "192.168.1.50",
    "spokeDataUrl": "ws://192.168.1.100:8080/signalk/v2/api/vessels/self/radars/nav1034A/spokes",
    "streamUrl": "ws://192.168.1.100:8080/signalk/v1/stream"
    },
    "nav1034B": {
    "brand": "Navico",
    "model": "HALO",
    "name": "HALO 034B",
    "radarIpAddress": "192.168.1.50",
    "spokeDataUrl": "ws://192.168.1.100:8080/signalk/v2/api/vessels/self/radars/nav1034B/spokes",
    "streamUrl": "ws://192.168.1.100:8080/signalk/v1/stream"
    }
    }

    Check which network interfaces are available and which radar brands are listening:

    HTTP GET "/signalk/v2/api/vessels/self/radars/interfaces"
    

    Response:

    {
    "brands": ["Navico", "Furuno", "Raymarine"],
    "interfaces": {
    "en0": {
    "status": "Ok",
    "ip": "192.168.1.100",
    "netmask": "255.255.255.0",
    "listeners": {
    "Navico": "Active",
    "Furuno": "No match for 172.31.255.255",
    "Raymarine": "Listening"
    }
    },
    "en1": {
    "status": "WirelessIgnored"
    }
    }
    }

    This endpoint is useful for diagnosing network configuration issues when radars are not being detected.

    The capability manifest describes everything a radar can do. Clients should fetch this at the beginning of a session. The contents do not change during radar operation.

    HTTP GET "/signalk/v2/api/vessels/self/radars/{radar_id}/capabilities"
    

    Response:

    {
    "maxRange": 74080,
    "minRange": 50,
    "supportedRanges": [
    50, 75, 100, 250, 500, 750, 1000, 1500, 2000, 3000, 4000, 6000, 8000, 12000,
    16000, 24000, 36000, 48000, 64000, 74080
    ],
    "spokesPerRevolution": 2048,
    "maxSpokeLength": 1024,
    "pixelValues": 16,
    "hasDoppler": true,
    "hasDualRadar": false,
    "hasDualRange": true,
    "hasSparseSpokes": false,
    "noTransmitSectors": 2,
    "controls": {
    "gain": {
    "id": 4,
    "name": "Gain",
    "description": "How sensitive the radar is to returning echoes",
    "category": "base",
    "dataType": "number",
    "minValue": 0.0,
    "maxValue": 100.0,
    "stepValue": 1.0,
    "hasAuto": true,
    "hasAutoAdjustable": false
    }
    },
    "legend": {
    "lowReturn": 1,
    "mediumReturn": 8,
    "strongReturn": 13,
    "targetBorder": 17,
    "dopplerApproaching": 18,
    "dopplerReceding": 19,
    "historyStart": 20,
    "pixelColors": 16,
    "pixels": [
    { "type": "normal", "color": { "r": 0, "g": 0, "b": 0, "a": 0 } },
    { "type": "normal", "color": { "r": 0, "g": 0, "b": 51, "a": 255 } }
    ]
    }
    }

    Capability fields:

    1. hasDoppler - if true, the radar can detect boats or objects approaching or receding and emits separate pixel colors for these.
    2. hasDualRadar - if true, the physical radome reports itself as two independent radars that can be set to different ranges and modes. Currently only Navico 4G and HALO support this.
    3. hasDualRange - mutually exclusive with hasDualRadar, indicates a more limited form of supporting two ranges with one device.
    4. minRange and maxRange - define what ranges the radar supports (in meters).
    5. supportedRanges - list of all discrete range values the radar supports (in meters).
    6. maxSpokeLength and spokesPerRevolution - define how many pixels the radar produces each revolution.
    7. noTransmitSectors - how many sectors the radar can stop transmitting to avoid obstacles like masts.
    8. pixelValues - number of distinct pixel intensity values.
    9. hasSparseSpokes - if true, the radar produces fewer spokes per revolution than spokesPerRevolution indicates (see Spoke skipping).

    All spokes are sent with one byte per pixel. The legend explains what each byte value represents.

    {
    "lowReturn": 1,
    "mediumReturn": 8,
    "strongReturn": 13,
    "dopplerApproaching": 18,
    "dopplerReceding": 19,
    "historyStart": 20,
    "pixelColors": 16,
    "pixels": [
    { "type": "normal", "color": "#00000000" },
    { "type": "normal", "color": "#0000ffff" },
    { "type": "dopplerApproaching", "color": "#ff00ffff" },
    { "type": "dopplerReceding", "color": "#00ff00ff" },
    { "type": "history", "color": "#454545ff" }
    ]
    }

    The lowReturn, mediumReturn, and strongReturn indicate offsets in the array, typically used for smoothing algorithms.

    If the radar doesn't implement Doppler, the dopplerApproaching and dopplerReceding fields will be null. If the provider doesn't implement target trails, historyStart will be null.

    There are two different ways that radars handle "dual" ranges.

    Navico radars implement this by acting as if both radars are full independent, to the point where both radars use different ports and IP addresses. They can be seen to be dependent in that if you change some controls they also change on the other radar. The NoTransmitZones are examples of such controls. These radars therefore also show up as two radars in the API. As long as clients listen to updates to controls, which they should do anyway to be able to function in a setting where there is for instance a MFD device, they can assume that all controls can be set.

    Furuno radars do this in a way where the second range shares as many control settings as possible. Currently there is no support for Furuno dual range yet and its impact on the API is unknown.

    The controls object in capabilities lists all controls the radar supports. Control data types:

    dataType Description
    number Numeric value with min/max/step
    enum Discrete set of values with descriptions
    string Text value
    button Action trigger (no value)
    sector Angle range (start/end)
    zone Guard zone (angles + distances)
    rect Rectangular exclusion zone
    1. number
    {
    "id": 47,
    "name": "Transmit time",
    "description": "How long the radar has been transmitting over its lifetime",
    "category": "info",
    "dataType": "number",
    "isReadOnly": true,
    "minValue": 0.0,
    "maxValue": 3599996400.0,
    "stepValue": 3600.0,
    "units": "s"
    }

    The units field indicates the unit of measurement for the control value. A conforming server implementation sends only SI units to clients:

    Category SI Unit Abbreviation
    Distance Meters m
    Speed Meters per second m/s
    Angle Radians rad
    Rotational speed Radians per second rad/s
    Duration Seconds s

    Note how in the above example the server has converted a value in hours (3600 seconds) to seconds to conform to the above, but the client can convert the value back to hours for representation to a human.

    A conforming API server will allow the following units to be specified when receiving values from a client:

    Category Unit Abbreviation
    Distance Meters m
    Distance Kilometers km
    Distance Nautical miles nm
    Speed Meters per second m/s
    Speed Knots kn
    Angle Radians rad
    Angle Degrees deg
    Rotational speed Radians per second rad/s
    Rotational speed Rotations/minute rpm
    Duration Seconds s
    Duration Minutes min
    Duration Hours h
    1. enum
    {
    "id": 0,
    "name": "Power",
    "description": "Radar operational state",
    "category": "base",
    "dataType": "enum",
    "minValue": 0.0,
    "maxValue": 3.0,
    "stepValue": 1.0,
    "descriptions": {
    "0": "Off",
    "1": "Standby",
    "2": "Transmit",
    "3": "Preparing"
    },
    "validValues": [1, 2]
    }

    The validValues array indicates which values can be set by clients. The power control guarantees that at least these values can be set across all radars: 1 (Standby) and 2 (Transmit).

    1. string
    {
    "id": 53,
    "name": "Custom name",
    "description": "User defined name for the radar",
    "category": "advanced",
    "dataType": "string"
    }
    1. button

    A button triggers an action without needing a value:

    {
    "id": 15,
    "name": "Clear trails",
    "description": "Clear target trails",
    "category": "trails",
    "dataType": "button"
    }
    1. sector
    {
    "id": 35,
    "name": "No Transmit sector",
    "description": "First no-transmit sector",
    "category": "installation",
    "dataType": "sector",
    "hasEnabled": true,
    "minValue": -3.141592653589793,
    "maxValue": 3.141592653589793,
    "stepValue": 0.0017453292519943296,
    "units": "rad"
    }

    A sector defines a start and end angle from -π to +π radians, plus an enabled flag. The value for start is transmitted in value and the end in endValue.

    $ curl -s http://localhost:6502/signalk/v2/api/vessels/self/radars/nav1034A/controls/noTransmitSector1
    {"enabled":true,"value":-1.5533,"endValue":-1.2217}
    1. zone
    {
    "id": 16,
    "name": "Guard zone",
    "description": "First guard zone for target detection",
    "category": "guardZones",
    "dataType": "zone",
    "hasEnabled": true,
    "minValue": -3.141592653589793,
    "maxValue": 3.141592653589793,
    "maxDistance": 100000.0,
    "units": "rad"
    }

    A zone defines five attributes: start angle, end angle, start distance, end distance, and enabled.

    $ curl -s http://localhost:6502/signalk/v2/api/vessels/self/radars/nav1034A/controls/guardZone1
    {"enabled":true,"value":-0.5585,"endValue":1.7104,"startDistance":100.0,"endDistance":232.0}
    1. rect
    {
    "id": 60,
    "name": "Exclusion zone",
    "description": "Rectangular exclusion zone",
    "category": "guardZones",
    "dataType": "rect",
    "hasEnabled": true,
    "maxValue": 100000.0
    }

    A rect defines a rectangular zone using two corners and a perpendicular width. The corners (x1, y1) and (x2, y2) define one edge of the rectangle in meters relative to the radar position (positive X is starboard, positive Y is ahead). The width extends perpendicular to this edge.

    $ curl -s http://localhost:6502/signalk/v2/api/vessels/self/radars/nav1034A/controls/exclusionZone1
    {"enabled":true,"x1":-50.0,"y1":100.0,"x2":50.0,"y2":100.0,"width":200.0}

    Controlling the radar can be done via HTTP REST requests or via the stream websocket.

    HTTP GET "/signalk/v2/api/vessels/self/radars/{radar_id}/controls"
    

    Response:

    {
    "gain": { "auto": false, "value": 50 },
    "sea": { "auto": true, "autoValue": 25, "value": 30 },
    "range": { "value": 3000 }
    }
    HTTP GET "/signalk/v2/api/vessels/self/radars/{radar_id}/controls/{control_id}"
    

    Response:

    { "auto": false, "value": 50 }
    
    HTTP PUT "/signalk/v2/api/vessels/self/radars/{radar_id}/controls/{control_id}"
    

    Simple numeric control:

    { "value": 75 }
    

    Control with auto mode:

    { "auto": false, "value": 75 }
    

    or just change auto mode:

    { "auto": true }
    

    Control with auto adjustment (e.g., Sea on HALO):

    When in auto mode, some controls accept an adjustment value:

    { "auto": true, "autoValue": -20 }
    

    Sector control:

    { "enabled": true, "value": -1.5533, "endValue": -1.2217 }
    

    Zone control:

    {
    "enabled": true,
    "value": -0.5585,
    "endValue": 1.7104,
    "startDistance": 100.0,
    "endDistance": 500.0
    }

    Button control:

    For buttons, send an empty body or {} - the PUT request itself triggers the action.

    Control values contain different fields depending on the control's dataType (defined in the capability schema).

    Common fields:

    Field Description
    value The control value (numeric or string) (if dataType is not rect)
    auto Whether automatic mode is enabled (if hasAuto is true)
    autoValue Adjustment when auto=true (if hasAutoAdjustable is true)
    timestamp ISO 8601 timestamp when value was last changed

    dataType-specific fields:

    Field dataType Description
    enabled sector, zone, rect Whether the control is enabled
    endValue sector, zone End angle (radians)
    startDistance zone Inner radius (meters)
    endDistance zone Outer radius (meters)
    x1 rect First corner X (meters, starboard +)
    y1 rect First corner Y (meters, ahead +)
    x2 rect Second corner X (meters, starboard +)
    y2 rect Second corner Y (meters, ahead +)
    width rect Perpendicular width (meters)

    The Radar API defines ARPA (Automatic Radar Plotting Aid) target tracking with CPA/TCPA calculations and SignalK notification integration.

    mayara-server fully supports both ARPA and MARPA, but this is an optional part of the API. When a server does not support it it shall return HTTP status 501.

    If the radar is a dual-radar device then mayara-server has a CLI option --merge-targets, when this is used targets will be shared between both ranges and move from one radar to another.

    HTTP GET "/signalk/v2/api/vessels/self/radars/{id}/targets"
    

    Response:

    [
    {
    "id": 1,
    "status": "tracking",
    "position": {
    "bearing": 0.789,
    "distance": 1852,
    "latitude": 52.3702,
    "longitude": 4.8952
    },
    "motion": {
    "course": 3.14159,
    "speed": 3.34
    },
    "danger": {
    "cpa": 150,
    "tcpa": 324
    },
    "acquisition": "auto",
    "sourceZone": 1,
    "firstSeen": "2025-01-15T10:25:00Z",
    "lastSeen": "2025-01-15T10:30:00Z"
    }
    ]

    Units: All distances are in meters. All angles (bearing, course) are in radians [0, 2π). Speed is in m/s. Time values (tcpa) are in seconds.

    Optional fields: Sub-structures are omitted when data is not yet known or not applicable:

    • motion: Omitted when motion is not yet computed (target still acquiring). Present with speed: 0 and course: 0 for confirmed stationary targets (buoys, anchored vessels).
    • danger: Omitted when vessels are diverging (no CPA exists) or own-ship motion unavailable
    • position.latitude/longitude: Omitted when radar position is unavailable
    • sourceZone: Omitted for manually acquired targets or Doppler-detected targets
    HTTP POST "/signalk/v2/api/vessels/self/radars/{id}/targets"
    

    Request body:

    {
    "bearing": 0.785,
    "distance": 2000
    }
    HTTP DELETE "/signalk/v2/api/vessels/self/radars/{id}/targets/{targetId}"
    

    There are two types of websocket:

    1. Control Stream: Signal-K-formatted JSON messages containing control information to and from radars, as well as targets.
    2. Spoke Data Stream: High-volume radar spoke data in binary format (up to 1 MB/s).

    The JSON data websocket provides real-time control value updates for all radars via the standard Signal K stream.

    The URI is found in the radar response as streamUrl or can be constructed as:

    ws://{host}:{port}/signalk/v1/stream
    

    This websocket endpoint works identical to a Signal K stream, as documented in https://signalk.org/specification/1.5.0/doc/streaming_api.html

    In short:

    • By default you are described to all paths
    • Query parameters subscribe=none can be used to start without any subscriptions and sendCachedValues=false to disable sending all currently cached values.
    • Subscriptions and desubscriptions can be made for paths. You can use '*' for all radars including radars still to be discovered.
    • When first connected all radar meta data will be sent.
    • When a new radar is discovered all existing streams will also be sent the meta data for the new radar.

    The recommended way of connecting is to either send subscribe=none and then a subscribe to all controls, as in the example below, with a policy of instant. The number of updates after the initial cache dump is low, about 2 messages per second.

    "subscribe": [
    {
    "path": "radars.*.controls.*",
    "period": 1000
    },
    ]

    To receive real-time ARPA target updates, subscribe to the targets path:

    {
    "subscribe": [
    {
    "path": "radars.*.targets.*",
    "policy": "instant"
    }
    ]
    }

    You can subscribe to both controls and targets simultaneously:

    {
    "subscribe": [
    { "path": "radars.*.controls.*", "period": 1000 },
    { "path": "radars.*.targets.*", "policy": "instant" }
    ]
    }

    Example of received meta-data:

    {
    "updates": [
    {
    "$source": "mayara",
    "timestamp": "2026-02-23T18:15:26.409454084Z",
    "meta": [
    {
    "path": "radars.nav1034A.controls.guardZone1",
    "value": {
    "id": 13,
    "name": "Guard zone",
    "description": "First guard zone for target detection",
    "category": "guardZones",
    "dataType": "zone",
    "hasEnabled": true,
    "minValue": -3.141592653589793,
    "maxValue": 3.141592653589793,
    "units": "rad",
    "maxDistance": 100000.0
    }
    },
    {
    "path": "radars.nav1034A.controls.firmwareVersion",
    "value": {
    "id": 48,
    "name": "Firmware version",
    "description": "Version of the radar firmware",
    "category": "info",
    "dataType": "string",
    "isReadOnly": true
    }
    }
    ]
    }
    ]
    }

    Example of received data:

    {
    "updates": [
    {
    "$source": "mayara",
    "values": [
    {
    "path": "radars.nav1034A.controls.spokes",
    "value": { "value": 2048 }
    }
    ]
    }
    ]
    }

    Example of setting a control:

    {
    "path": "radars.nav1034A.controls.guardZone1",
    "value": {
    "value": 0.735,
    "endValue": 3.1415,
    "startDistance": 0,
    "endDistance": 500,
    "enabled": true
    }
    }

    Target updates are sent whenever a target's position, motion, or status changes:

    {
    "updates": [
    {
    "$source": "mayara",
    "timestamp": "2025-01-15T10:30:00Z",
    "values": [
    {
    "path": "radars.nav1034A.targets.1",
    "value": {
    "id": 1,
    "status": "tracking",
    "position": {
    "bearing": 0.789,
    "distance": 1852,
    "latitude": 52.3702,
    "longitude": 4.8952
    },
    "motion": {
    "course": 3.14159,
    "speed": 3.34
    },
    "danger": {
    "cpa": 150,
    "tcpa": 324
    },
    "acquisition": "auto",
    "sourceZone": 1,
    "firstSeen": "2025-01-15T10:25:00Z",
    "lastSeen": "2025-01-15T10:30:00Z"
    }
    }
    ]
    }
    ]
    }

    Targets are created either automatically (ARPA) or manually (MARPA, via a REST or stream message.) In all cases the targets go through the following states: acquiring -> tracking -> lost.

    When a target is deleted (either because it has been in status lost for a while or a client explicitly deletes it), a final null value is sent:

    {
    "updates": [
    {
    "$source": "mayara",
    "timestamp": "2025-01-15T10:32:00Z",
    "values": [
    {
    "path": "radars.nav1034A.targets.1",
    "value": null
    }
    ]
    }
    ]
    }

    Because radars can produce up to 4 megabytes of data per rotation, this data is transmitted on a separate websocket per radar and is in a binary format. The data is encoded using Protocol Buffers (protobuf), Google's language-neutral binary serialization format. Protobuf provides compact encoding and fast parsing, with official implementations available for most programming languages including JavaScript, Python, Java, C++, Go, and Rust.

    The message schema is stable and will not change within a major version (per semver):

    syntax = "proto3";
    
    /*
     * The data stream coming from a radar is a series of spokes.
     * The number of spokes per revolution is different for each type of
     * radar and can be found in the capabilities at
     * .../v2/api/vessels/self/radars/{id}/capabilities as 'spokesPerRevolution'.
     * The maximum length of each spoke is also defined there, as well as the legend that provides
     * a lookup table for each byte of data in the spoke.
     *
     * The angle and bearing fields below are in terms of spokes, so
     * range from [0..spokesPerRevolution>.
     *
     * Angle is a mandatory field and tells you the rotation of the spoke
     * relative to the front of the boat, going clockwise. 0 means directly
     * ahead, spokesPerRevolution / 4 is to starboard, spokesPerRevolution / 2 is directly astern, etc.
     *
     * Bearing, if set, means that either the radar or the radar server has
     * enriched the data with a true bearing, e.g. 0 is directly North,
     * spokesPerRevolution / 4 is directly West, spokesPerRevolution / 2 is South, etc.
     *
     * Likewise, time and lat/lon indicate the best effort when the spoke
     * was generated, and the lat/lon of the radar at the time of generation.
     *
     */
    message RadarMessage {
        message Spoke {
            uint32 angle = 1; // [0..spokesPerRevolution>, angle from bow
            optional uint32 bearing = 2; // [0..spokesPerRevolution>, offset from True North
            uint32 range = 3; // [meters], range in meters of the last pixel in data
            optional uint64 time = 4; // [millis since UNIX epoch] Time when spoke was generated or received
            optional double lat = 6; // Location of radar at time of generation
            optional double lon = 7; // Location of radar at time of generation
            bytes data = 5;
        }
        repeated Spoke spokes = 2;
    }
    

    The URL is found in the radars REST response as spokeDataUrl or can be constructed as:

    /signalk/v2/api/vessels/self/radars/{radar_id}/spokes
    

    This a Javascript example how to set up the connection to receive spokes:

    // Fetch radars
    const response = await fetch('/signalk/v2/api/vessels/self/radars/')
    const data = await response.json()

    // Choose a radar_id from the returned radars
    const radarId = Object.keys(data)[0]
    const radar = data[radarId]

    // Connect to spoke data stream
    const wsUrl =
    radar.spokeDataUrl ??
    `ws://${location.host}/signalk/v2/api/vessels/self/radars/${radarId}/spokes`

    const socket = new WebSocket(wsUrl)
    socket.binaryType = 'arraybuffer'

    socket.onmessage = (event) => {
    const spokeData = new Uint8Array(event.data)
    // Process binary spoke data...
    }

    Every spoke contains spoke_len bytes. The radar API always uses one byte per pixel, with every byte representing a value explained by the legend contained in the capabilities.

    The legend provides a lookup table mapping each byte value to its meaning and suggested display color:

    • Byte values 0 to pixelColors - 1: Normal radar returns, ranging from no echo (0) to strongest echo. The lowReturn, mediumReturn, and strongReturn fields indicate thresholds within this range, useful for smoothing or color gradient algorithms.
    • Byte value at targetBorder: Indicates the edge of a tracked ARPA target.
    • Byte value at dopplerApproaching: Object moving toward the radar (requires Doppler-capable radar).
    • Byte value at dopplerReceding: Object moving away from the radar (requires Doppler-capable radar).
    • Byte values from historyStart onward: Historical trail data showing where targets were in previous rotations.

    The pixels array provides the complete mapping from byte value to RGBA color. Clients can use this directly for rendering, or implement their own color scheme based on the semantic pixel types (normal, targetBorder, dopplerApproaching, dopplerReceding, history).

    If the radar doesn't support a feature, the corresponding legend field will be absent or null (e.g., dopplerApproaching and dopplerReceding are absent for non-Doppler radars).

    In a later API release it is likely that the legend will be expanded to contain color mappings for different palettes.

    Some radars have a high value for spokesPerRevolution but actually only produce fewer spokes per each revolution. This is true for Furuno radars but not the other supported radars from Garmin, Navico and Raymarine. The Furuno radars set hasSparseSpokes in the capabilities struct to true.

    A conforming GUI must allow for this and either implement some way to expand missing spokes or to reconsider the width of spokes to be from the angle/bearing from the received spoke to the previously received spoke.

    A typical value for Furuno is to have spokesPerRevolution = 8192 but the actual # of spokes will be ~ 900. Weirdly enough it is not a "round" figure like 1440, 2048, 512 or 250 like the other radars.

    interface RadarsResponse {
    version: string
    radars: Record<string, RadarInfo>
    }

    interface RadarInfo {
    name: string
    brand: string
    model?: string
    radarIpAddress: string
    spokeDataUrl: string
    streamUrl: string
    }
    interface Capabilities {
    maxRange: number
    minRange: number
    supportedRanges: number[]
    spokesPerRevolution: number
    maxSpokeLength: number
    pixelValues: number
    hasDoppler: boolean
    hasDualRadar: boolean
    hasDualRange: boolean
    hasSparseSpokes: boolean
    noTransmitSectors: number
    controls: Record<string, ControlDefinition>
    legend: Legend
    }
    interface ControlDefinition {
    id: number
    name: string
    description: string
    category:
    | 'base'
    | 'targets'
    | 'guardZones'
    | 'trails'
    | 'advanced'
    | 'installation'
    | 'info'
    dataType: 'number' | 'enum' | 'string' | 'button' | 'sector' | 'zone' | 'rect'
    isReadOnly?: boolean
    hasEnabled?: boolean
    minValue?: number
    maxValue?: number
    stepValue?: number
    maxDistance?: number
    units?: 'm' | 'm/s' | 'rad' | 'rad/s' | 's'
    descriptions?: Record<string, string> // For enum types
    validValues?: number[] // For enum types
    hasAuto?: boolean
    hasAutoAdjustable?: boolean
    autoAdjustMinValue?: number
    autoAdjustMaxValue?: number
    }
    interface ControlValue {
    value?: number | string
    units?:
    | 'm'
    | 'km'
    | 'nm'
    | 'm/s'
    | 'kn'
    | 'rad'
    | 'deg'
    | 'rad/s'
    | 'rpm'
    | 's'
    | 'min'
    | 'h'
    auto?: boolean
    autoValue?: number
    enabled?: boolean
    endValue?: number // End angle for sectors/zones (radians)
    startDistance?: number // Inner radius for zones (meters)
    endDistance?: number // Outer radius for zones (meters)
    x1?: number // Rect: first corner X (meters)
    y1?: number // Rect: first corner Y (meters)
    x2?: number // Rect: second corner X (meters)
    y2?: number // Rect: second corner Y (meters)
    width?: number // Rect: perpendicular width (meters)
    timestamp?: string // ISO 8601 timestamp when value was last changed
    }
    interface Legend {
    lowReturn: number
    mediumReturn: number
    strongReturn: number
    targetBorder: number
    dopplerApproaching?: number
    dopplerReceding?: number
    historyStart: number
    pixelColors: number
    pixels: LegendPixel[]
    }

    interface LegendPixel {
    type: 'normal' | 'dopplerApproaching' | 'dopplerReceding' | 'history'
    color: string
    }
    interface Target {
    id: number
    status: 'tracking' | 'lost' | 'acquiring'
    position: {
    bearing: number // radians [0, 2π)
    distance: number // meters
    latitude?: number // omitted if radar position unavailable
    longitude?: number // omitted if radar position unavailable
    }
    motion?: {
    // omitted if motion not yet computed; present with zeros for stationary targets
    course: number // radians [0, 2π)
    speed: number // m/s
    }
    danger?: {
    // omitted if vessels diverging or own-ship motion unavailable
    cpa: number // meters
    tcpa: number // seconds
    }
    acquisition: 'manual' | 'auto'
    sourceZone?: number // guard zone (1 or 2) that acquired this target; omitted for manual/Doppler
    firstSeen: string // ISO 8601 timestamp
    lastSeen: string // ISO 8601 timestamp
    }