Skip to content

~History: SignalK Data Model

Teppo Kurki edited this page Feb 26, 2020 · 1 revision

This is earlier material and largely superceded by the specification


This page summarises the major technical aspects of the Signal K model for developers.

The architectural model for the Signal K model is based loosely on the UNIX model. In UNIX/Linux every resource is a file, including IO, CPU, RAM, etc. In Signal K every datapoint or resource is an object in the Signal K tree. The tree equates to a traditional hierarchical filesystem.

{
  "vessels": {
    "self": {
      "version": "0.1",
      "name": "motu",
      "mmsi": "2345678",
      "source": "self",
      "timezone": "NZDT",
      "navigation": {
        "state": {},
        "courseOverGroundTrue": {},
        "roll": {},
        "rateOfTurn": {}
      }
    }
  }
}

The branches equate to folders, and the leaves equate to files. The structure is entirely virtual, you are free to implement it however is best suited to your application. It is quite valid to implement it REST style, where a leaf value like roll is derived by a function on the fly. But in terms of external representation, Signal K capable devices will be assuming the filesystem model, and the JSON representation will also reflect that. (The path representation is generally dot-separated, but sometimes / separated in situations where dot is ambiguous, e.g. relative URIs are awful in dot-syntax ...path.to.goal vs ../path/to/goal.)

Navigation of the tree is accomplished in a similar way to any filesystem. * and ? wildcards can be used to define paths which are used in data requests.

"vessels.366982320.navigation.position"
"vessels.\*.navigation.position"
"vessels.\*.navigation.position.\*"

The following rules are used to determine if a node is a branch or a leaf (since a leaf may be a complex object):

  • timestamp/source of parent is the name of the key, so above key is attitude
  • siblings of timestamp/source are the values
  • value is an invisible key, it signifies a primitive value of the parent, eg courseOverGound(.value)=234
  • position has several keys, so its an object, with the keys as object content

invisible means that we don't use it as the value's name. It's there because we need an object to hold timestamp/source, and hence we need a key for what is actually a primitive value for speedOverGround e.g.

"speedOverGround": {
  "timestamp": "2015-03-23T01:57:02.256Z",
  "source": "vessels.motu.sources.nmea.0183.RMC",
  "value": 0.1517598
}

is equivalent to speedOverGround=0.1517598 whereas in position we do care about the value's name as position is a complex object:

"position":{
  "timestamp":"2015-03-23T01:57:02.256Z",
  "source":"vessels.motu.sources.nmea.0183.RMC",
  "longitude":4.58006417,
  "latitude":51.9485185
}

This allows the server to correctly differentiate primitive and object keys without external config help.

Each leaf on the tree has several optional sub-nodes.

"courseOverGroundTrue": {
  "value": 102.7,
  "source": "../n2k1-12-0", // a URI to the source of the data, possibly even http://..
  "meta": {},               // holds data to manage alarms, and auto-config of gui screens
  "_system": {},            // the _system object is a virtual filesystem which exposes the underlying device
  "_attr": {                // filesystem specific data, e.g. security, possibly more later
    "_mode": 644,           // UNIX style permissions, often written in `owner:group:other` form, `-rw-r--r--`
    "_owner": "self",       // owner
    "_group": "self"        // group
  }
}

This introduces an important aspect. Any node name starting with _ is normally stripped from output, especially off-vessel. This is a useful way of filtering custom nodes that only relate to the specific instance of Signal K.

The source node is very powerful. It has two forms, a JSON string, and a JSON object. The JSON string form above is a simple redirect and can be any valid URI. The same source may provide data for more than one signal K key (e.g. NMEA0183 RMC from GPS) so we recommend the sources be held under a top-level (root) tree named sources to avoid duplication.

The object form looks like this and holds meta-data on the source of the value

{
  "source": "sources.n2k1-12-0",              // relative or full URI to the source object
  "n2k1-12-0": {                              // actual physical device that sends the readings
    "value": 102.29,
    "bus": "/dev/actisense",                  // the physical transport reference
    "timestamp": "2014-08-15-16: 00: 00.085", // time of reading
    "src": "201",                             // n2k src field (for an n2k reading)
    "pgn": "130577"                           // n2k pgn field
  }
}

The source object will hold different data for different types of data. Obviously a weather forecast does not have a pgn, but might have a url.

The meta node holds value specific settings that allow monitoring of alarm criteria, and auto-configuration of GUI screens. See Metadata-for-Data-Values.

The _system allows access to the underlying device or server. In the same way as the /proc filesystem in Linux, this can expose data such as current users, subscriptions, RAM or CPU usage, or be used to query an n2k node.

{
  "context": "sources",
  "put": [
    {
      //request n2k 126996 message from device at n2k1-12-0
      "values": [
        {
          "path": "n2k1-12-0._system.pgn.request.126996",
          "value": ""
        }
      ]
    }
  ]
}

There are a selection of messages that provide IO operations to the Signal K tree. Each array has objects with the path attribute in common. The basic verbs are

{
  "context": "path.to.the.parent",
  "list": [],       // list the keys of context.path, recursively
  "get": [],        // get the object at context.path, recursively
  "put": [],        // put (set) the object at context.path
  "updates": [],    // result of a subscribe
  "subscribe": [],  // request regular updates of context.path
  "unsubscribe": [] // cancel subscription
}

These all take a similar set of options including wildcards.

And a typical request is

{
  "context": "vessels.self", // set the common topmost node, eg same as 'cd' to the directory
  "subscribe": [             // the action verb, holds an array of objects 
    {
      "path": "navigation.courseOverGroundTrue", // target for the verb to act on, path is relative from context,
                                                 // e.g. context.path
    }
  ]
}

The above subscription results in a periodic message

{
  "context": "vessels.self",
  "updates": [
    {
      "source": {
        "device": "/dev/actisense",
        "timestamp": "2014-08-15-16:00:00.081",
        "src": "115",
        "pgn": "128267"
      },
      "values": [
        {
          "path": "navigation.courseOverGroundTrue",
          "value": 172.9
        }
      ]
    }
  ]
}

Individual message formats are defined elsewhere, but all verbs have some common sub-nodes:

"inclAttr": [true|false]    //default false, controls sending the _attr node
"inclSource": [true|false]  //default true, controls sending the source node
"followLinks": [true|false] //default true, controls resolution of relative source links.