Proposal: Course API
Feature: Course API
Description:
Define an API for Signal K client applications that provides methods to set a destination or navigate a route.
These methods manage the setting of values in the relevant Signal K paths under both navigation.courseGreatCircle and navigation.courseRhumbline to enable a course computer to generate additional navigation information (XTE, DTG, etc) as well as facilitate display on a chart plotter.
The API will facilitate operations such as:
- Setting a position (lat, lon) as a destination
- Referencing a waypoint from
/resources/waypointsas a destination - Activating a route by supplying a reference to an entry under
/resources/routes - Specify to follow route points in reverse order, etc.
Motivation:
Currently Signal K makes available paths to store navigation data but it is largely left up to implementors of client applications to determine how they are used.
This is can cause interoperability issues and inconsistency in application (e.g. calculations in signalk-derived-data plugin will use a mixture of paths navigation.courseGreatCircle and navigation.courseRhumbline) so depending on an individual implementation results may vary.
Defining and implementing an API will provide reliability in how the values in these paths are populated ensuring confidence in the source data used in course calculations.
By clearly defining and managing the use of specific course paths within the Signal K schema, this will ensure consistency in the values they contain and engender confidence in their use.
Maintaining quality data in these paths enables the reliable operation of other navigation equipment such as:
- Course computers
- Auto-pilots
by providing a trusted source of data for use in calculating navigation information for steering a course.
The paths within the Signal K schema pertaining to other navigation operations will be maintained by the relevant equipment or Signal K API.
1. Signal K Paths in Scope:
Note: All paths outlined below are relative to /signalk/v1/api/vessels/self.
The Signal K specification contains a navigation.course schema definition which is applied to both the navigation/courseGreatCircle and navigation/courseRhumbline paths.
The following properties under both these paths are in scope for management by this API:
activeRoute.href
activeRoute.startTime
nextPoint.value.href
nextPoint.value.type
nextPoint.position
previousPoint.value.href
previousPoint.value.type
previousPoint.position
This API will provide endpoints under the path navigation/course in order to set a course as well as query the current course information.
2.API Operation:
While the intended use of the in scope Signal K paths are defined in the specification, the actual use of these paths in practise determines the success of an implementation.
2.1 Use of previousPoint.
To facilitate course calculations such as XTE where the source position is required, the previousPoint.position attribute will be set (at the time the destination is set) as follows:
-
When a position (lat, lon) is supplied as a destination: Set the value of
previousPoint.positionto the location of the vessel. -
When a reference to a waypoint resource is supplied as a destination: Set the value of
previousPoint.positionto the location of the vessel. -
When a reference to a route resource is is supplied:
-
If the supplied route
pointIndexis0(point at start of the route) then set the value ofpreviousPoint.positionto the location of the vessel. -
If the supplied route
pointIndexis not0then set the value ofpreviousPoint.positionto that of the preceding point in the route.
-
-
When a "Course Restart" is requested: Set the value of
previousPoint.positionto the location of the vessel. -
When a destination or active route is "Cancelled": then set the value of
previousPoint.positiontonull.
3.API Methods:
The following endpoints under the path navigation/course make up the Course API.
3.1 Set a position (lat, lon) as a destination
Use case: Provide "navigate to here" operation.
Action: PUT
Path: <self>/navigation/course/destination
Request body:
{
"position": {"latitude": -28.5,"longitude":138.5},
"type": "Location",
"arrivalCircle": 500
}
where:
-
position: The destination lat, lon (as per Signal K schema) -
type(optional): A string describing the destination (as per Signal K schema). -
arrivalCircle(optional): Radius of circle centered at destination indicating arrival.
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": null,
"startTime": null
},
"nextPoint": {
"href": null,
"type": null if type not supplied,
"position": {
"latitude": supplied latitude,
"longitude": supplied longitude
},
"arrivalCircle": unchanged if value not suplied
},
"previousPoint": {
"href": null,
"type": null,
"position": {
"latitude": latitude of vessel at time of destination being set,
"longitude": longitude of vessel at time of destination being set
}
}
}
3.2 Set a Waypoint as a destination
Use case: Provide "navigate to selected waypoint resource" operation.
Action: PUT
Path: <self>/navigation/course/destination
Request body:
{
"href": "/resources/waypoints/urn:mrn:signalk:uuid:0d95e282-3e1f-4521-8c30-8288addbdbab",
"arrivalCircle": 500
}
where:
-
href: The path to the target waypoint in/resources/waypoints/. -
arrivalCircle(optional): Radius of circle centered at destination indicating arrival.
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": null,
"startTime": null
},
"nextPoint": {
"href": supplied href,
"type": null if type not supplied,
"position": {
"latitude": latitude of referenced waypoint,
"longitude": longitude of referenced waypoint
},
"arrivalCircle": unchanged if value not supplied
},
"previousPoint": {
"href": null,
"type": null,
"position": {
"latitude": latitude of vessel at time of destination being set,
"longitude": longitude of vessel at time of destination being set
}
}
}
3.3 Clear / Cancel a destination or Activated Route.
Use case: Provide "stop navigating to destination" or "deactivate as route" operation.
Action: DELETE
Path: <self>/navigation/course/destination or <self>/navigation/course/activeRoute
Request body: empty
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": null,
"startTime": null
},
"nextPoint": {
"href": null,
"type": null,
"position": null,
"arrivalCircle": unchanged
},
"previousPoint": {
"href": null,
"type": null,
"position": null
}
}
3.4 Activate a Route to follow.
Use case: Provide "activate / follow a route" operation.
Action: PUT
Path: <self>/navigation/course/activeRoute
Request body:
{
"href": "/resources/routes/urn:mrn:signalk:uuid:0d95e282-3e1f-4521-8c30-8288addbdbab",
"pointIndex": 0,
"reverse": false,
"arrivalCircle": 500
}
where:
-
href: The path to the target route in/resources/routes/. -
pointIndex(optional): Zero based index of the point within the route to use as the initial destination (defaults to 0 if not supplied or if value is larger than index of last point in the route). -
reverse(optional): Iftrueperforms operations on route points in reverse order (defaults to false). -
arrivalCircle(optional): Radius of circle centered at destination indicating arrival.
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": supplied href,
"startTime": Time at which route was activated (as per Signal K schema)
},
"nextPoint": {
"href": null,
"type": null if value not supplied,
"position": {
"latitude": latitude of route point at supplied index,
"longitude": longitude of route point at supplied index
},
"arrivalCircle": unchanged if value not supplied
},
"previousPoint": {
"href": null,
"type": null,
"position": {
"latitude": latitude of: vessel if supplied pointIndex=0 or point at poiintIndex-1,
"longitude": longitude of: vessel if supplied pointIndex=0 or point at poiintIndex-1
}
}
}
3.5 Select point in the active Route as destination.
Use case: Provide "go to point in route" operation.
Action: PUT
Path: <self>/navigation/course/activeRoute/pointIndex
Request body:
{
"value": 3
}
where:
-
value: Zero based index of the point within the route to use as the initial destination (if value is larger than index of last point in the route then destination is not changed).
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": unchanged,
"startTime": unchanged
},
"nextPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": latitude of route point at supplied index,
"longitude": longitude of route point at supplied index
},
"arrivalCircle": unchanged
},
"previousPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": latitude of: vessel if pointIndex=0 or route point at poiintIndex-1,
"longitude": longitude of: vessel if pointIndex=0 or route point at poiintIndex-1
}
}
}
3.6 Increment / decrement point in the active Route as destination.
Use case: Provide "previous / next point" operation.
Action: PUT
Path: <self>/navigation/course/activeRoute/nextPoint
Request body:
{
"value": -1
}
where:
-
value: Is either1(next point) or-1(previous point).
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": unchanged,
"startTime": unchanged
},
"nextPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": latitude of previous (if -1 supplied) or next (if 1 supplied) route point,
"longitude": longitude of previous (if -1 supplied) or next (if 1 supplied) route point
},
"arrivalCircle": unchanged
},
"previousPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": latitude of: vessel (if pointIndex=0) or point at poiintIndex-1,
"longitude": longitude of: vessel (if pointIndex=0) or point at poiintIndex-1
}
}
}
3.7 Restart course calculations.
Use case: Provide "restart XTE" operation.
Action: PUT
Path: <self>/navigation/course/activeRoute/restart
Request body: empty
This will result in the following Signal K path values being set:
```JSON
{
"activeRoute": {
"href": unchanged,
"startTime": unchanged
},
"nextPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": unchanged,
"longitude": unchanged
},
"arrivalCircle": unchanged
},
"previousPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": latitude of vessel,
"longitude": longitude of vessel
}
}
}
3.8 Set arrival circle.
Use case: Provide ability to set the radius of a circle centered at destination indicating arrival.
Action: PUT
Path: <self>/navigation/course/arrivalCircle
Request body:
{
"value": 500
}
where:
-
value: Is the radius of the circle in meters.
This will result in the following Signal K path values being set:
{
"activeRoute": {
"href": unchanged,
"startTime": unchanged
},
"nextPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": unchanged,
"longitude": unchanged
},
"arrivalCircle": supplied value
},
"previousPoint": {
"href": unchanged,
"type": unchanged,
"position": {
"latitude": unchanged,
"longitude": unchanged
}
}
}
3.9 Query current course details.
Use case: Provide "get current course", "get course details" operation.
Action: GET
Path: <self>/navigation/course
Response: JSON formatted object containing the current course details as per the following example:
{
"activeRoute": {
"href": "/resources/routes/urn:mrn:signalk:uuid:0d95e282-3e1f-4521-8c30-8288addbdbab",
"startTime": "2021-10-23T05:17:20.065Z",
"pointIndex": 2,
"reverse": false,
"pointIndex": 2
},
"nextPoint": {
"href": null,
"type": "RoutePoint",
"position": {
"latitude":-29.5,
"longitude":137.5
}
},
"previousPoint": {
"href": null,
"type": null,
"position": {
"latitude":-29.05,
"longitude":137.75
}
}
}
4. Signal K Stream Deltas
The implementation of the Course API requires that the relevant delta messages are sent for the in-scope Signal K paths when:
- Values have changed
- Periodically e.g. every 30 seconds).
The absence of a delta for a specific Signal K path indicates that the path has never had a value assigned to it.
Where a delta value is null, this indicates that a previous value is no longer valid (i.e. there is a provider for this path but there is no current value available).
Delta messages for in-scope paths are as follows:
[
{
"path": "navigation.courseGreatCircle.activeRoute.href",
"value": reference to route resource,
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.activeRoute.startTime",
"value": Time at which route was activated (as per Signal K schema),
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.nextPoint.position",
"value": {
"latitude": number,
"longitude": number
},
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.nextPoint.value.href",
"value": reference to waypoint resource,
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.nextPoint.value.type",
"value": string,
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.previousPoint.position",
"value": {
"latitude": number,
"longitude": number
},
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.previousPoint.value.href",
"value": reference to waypoint resource,
"context": "vessels.self",
"source": source of value
},
{
"path": "navigation.courseGreatCircle.previousPoint.value.type",
"value": string,
"context": "vessels.self",
"source": source of value
}
]
This is really commendable work!
I think we should also include what deltas are sent. For example if I am running several UIs, like multiple instances of Freeboard, I want to keep them in sync. A practical use case would be setting the course down below in the use navigation station and the settings reflected at the helm.
Do we want to keep somehow track of who set the course? For example the user, or a course computer advancing the route as the vessel makes way along the route (in the latter case it would be useful to know who originally set the route to follow).
Is it useful to have the irrelevant fields as ´null` or should they be just undefined / not there at all?
How about advance/go back one waypoint?
What about the sequence
- activate route with waypoint 0
- steer the boat so that it is near, but not really at waypoint 0
- forward the course, so that the active waypoint is 1 now previousPoint should be waypoint0, shouldn't it? And XTE calculated between vessel position and the line between points 0 and 1.
I am not sure if we have really used hrefs anywhere in anger yet. As the SK http paths are mounted at /signalk/v1/api should there be a mechanism to indicate that the path in href is relative to that, or should there be something in the begining of the path, denoting that this is the case and the path does not start at the http server's root? Or should the hrefs be relative?
I think we should also include what deltas are sent. For example if I am running several UIs, like multiple instances of Freeboard, I want to keep them in sync. A practical use case would be setting the course down below in the use navigation station and the settings reflected at the helm.
Agreed.
I will add something to the README.md document in apis/course (which can be used as the documentation for this API).
Do we want to keep somehow track of who set the course? For example the user, or a course computer advancing the route as the vessel makes way along the route (in the latter case it would be useful to know who originally set the route to follow).
Have added a source attribute to hold a value representing who made the change.
Is it useful to have the irrelevant fields as ´null` or should they be just undefined / not there at all?
The convention to date seems to have been:
- No provider for the path, or path has never contained a value... then path is not returned via
apiendpoint / no delta emitted viastream. - Path value is assigned
nullvalue when a previous value has been deleted / removed or path has a provider but no value is available.
This convention seems to work quite well currently.
How about advance/go back one waypoint?
Was covered in course/activeRoute/nextPoint API by the request being able to contain either:
-
incrementattribute the value of which can be 1 (increment) or -1 (decrement) -
pointIndexattribute which sets the specific point in the route
Have separated this out to two APIs:
-
course/activeRoute/nextPointfor previous / next -
course/activeRoute/setPointfor setting a specific point in the route
Both of these APIs will consider the value of the reverse attribute value provided when the route was activated.
What about the sequence
- activate route with waypoint 0
- steer the boat so that it is near, but not really at waypoint 0
- forward the course, so that the active waypoint is 1 now previousPoint should be waypoint0, shouldn't it? And XTE calculated between vessel position and the line between points 0 and 1.
My understanding is that XTE works on source and destination position.... so if the vessel source position was only proximal to the destination location prior to advancing to the next point....... would using the route point position as the source instead of the vessel position negatively impact the XTE calculation significantly?
What does this mean for autopilot operation, etc?
I am not sure if we have really used hrefs anywhere in anger yet. As the SK http paths are mounted at /signalk/v1/api should there be a mechanism to indicate that the path in href is relative to that, or should there be something in the begining of the path, denoting that this is the case and the path does not start at the http server's root? Or should the hrefs be relative?
From the relevant part of the specification the implication is that it's relative to signalk/v1/api... I'm OK with that based on the work done with Freeboard and resources so far.
Extract from navigation.course.activeRoute.href
"href": {
"description": "A reference (URL) to the presently active route, in resources.",
"example": "/resources/routes/urn:mrn:signalk:uuid:3dd34dcc-36bf-4d61-ba80-233799b25672",
"type": "string"
}
https://www.manualsdir.com/manuals/199085/raymarine-chartplotter.html?page=48&original=1
Relevant features:
- waypoint advance (discussed above already)
- restart XTE - in our case would set previousPoint to current position, to go directly to the next point and calculate xte accordingly
Re: XTE: forget my example of being near or far from the waypoint. If we are navigating a route imho XTE would be the perpendicular distance between current position and the route leg, defined by two waypoints of the route, that we (or the autopilot) has set to be nextPoint and previousPoint. Does not really matter what our position is - XTE can be calculated even when we are behind previousPosition.
The only exception would be "restart XTE" case.
Added restart api endpoint and updated description of previousPoint to align.
Added arrivalCircle endpoint to enable setting the radius to determine arrival at destination.
Also added arrivalCircle attribute to destination and activeRoute endpoints.
Do we want to include specifying the notifications for entering arrival circle and passing waypoint perpendicular, or is that going into autopilot functionality?
I don't love the "PUT" or "POST". I think these should all be PUT, or at least pick one.
Do we want to include specifying the notifications for entering arrival circle and passing waypoint perpendicular, or is that going into autopilot functionality?
I think that it's worth specifiying at least the path of the notification that would be raised.
e.g. notifications.navigation.course.arrivalCircleEntered
I don't love the "PUT" or "POST". I think these should all be PUT, or at least pick one.
I'm OK with just PUT.
Updated OpenAPI definition to remove POST operatrions. Updated the README.md to reflect these changes as well as general formatting and readability.
This is more in the realm of implementation, but the frequency at which deltas are sent and whether the course values are persisted, so they survive a server restart, all contribute to expected behaviour. Are these worth including?
I am adding a bunch of comments, see what you make of them.
I am starting to think that it would be useful to create the server implementation and a UI to exercise this API before merging this. While this sounds pretty straightforward the devils in the details and they are hard to get right just working in the abstract.
I have been mocking this up by extending the freeboard-sk-helper plugin and a modified sk-resources-fs plugin to allow route details to be retrieved. I can confirm the proposed operation.
As far as embedding the course API into the server it will first require the resource path handling functionality to enable the courseAPI to retrieve route details.
I have working skeleton of the resources path handling as described in server issues #1351 .
If it is the correct protocol I can push this branch to signalk-server repository.
If it is the correct protocol I can push this branch to signalk-server repository.
Please do. I had already trouble looking for PR 1351 and then realised it was an issue, not a PR.
Collaboration is easier when working off the same repo: a bit less work with checking out work locally and at least trivial corrections like typos need not go via comments. Can also collaborate on stuff where we are not stepping on each other's toes.