sk-ais-status-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "none"
5
+ }
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # Signal K AIS Status Plugin
2
+ This plugin evaluates AIS target reporting continuity and maintains a per-target tracking state using class-specific timing thresholds. It publishes `sensors.ais.status` deltas to Signal K, enabling consuming applications to assess target validity, reliability, and reporting continuity.
3
+
4
+ ## About
5
+ AIS reception is often intermittent (collisions, range, antenna shadowing), so a single position report does not necessarily mean a target is reliably tracked, and an old position may be unsafe to treat as current. The published state (`unconfirmed`, `confirmed`, `lost`, `remove`) lets clients display and filter targets appropriately and avoid “ghost” or stale tracks.
6
+
7
+ ### A Note About Client State Interpretation
8
+ Client applications are free to interpret the state, but the basic intended UX is:
9
+
10
+ - `unconfirmed`: show the target, but de-emphasize it (e.g. faded / dotted / “suspect”). Avoid using it for alarms until it becomes confirmed.
11
+ - `confirmed`: show normally and treat as an active track.
12
+ - `lost`: keep showing the last known position briefly, but clearly indicate staleness (e.g. greyed out with “age since last update”). Typically suppress CPA/guard-zone style alarms for lost targets.
13
+ - `remove`: remove the target from display and lists.
14
+
15
+ Keeping `unconfirmed` and `lost` targets visible (but styled differently) helps avoid clutter from one-off decodes while still preserving situational awareness when reception is intermittent.
16
+
17
+ ## Plugin AIS Target State Management
18
+ The plugin applies standardized timing and continuity rules to manage AIS targets throughout their tracking lifecycle. Targets are created on first reception, transition to a confirmed tracking state after sufficient report continuity, and are marked as lost when expected reports are no longer received. The resulting tracking state is continuously updated and published for use by downstream consumers.
19
+
20
+ ### Sources (input scope)
21
+ Subscribes to deltas with the following contexts to determine which entities/updates are tracked:
22
+ - vessels
23
+ - atons
24
+ - shore.basestations
25
+ - sar
26
+ - aircraft
27
+
28
+ ### Target identity & indexing (stable identity)
29
+ The Signal K `context` string is the primary key for in-memory tracking. One item per context. No MMSI indexing or conflict handling is performed by the plugin.
30
+
31
+ ### Position report rules (message quality)
32
+ Each `navigation.position` update advances the tracking state and updates `lastPosition`.
33
+
34
+ ### State thresholds by class (reliability tuning)
35
+ Class A confirms quickly and times out quickly; Class B confirms slower and times out slower, etc. See [Device Class Processing Definition](#device-class-processing-definition) for class details.
36
+
37
+ ### Class resolution (source vs fallback)
38
+ The plugin prefers `sensors.ais.class` when it is present and one of the supported values. If it is missing or unsupported, the class falls back to the target context. Unsupported values are logged at debug level.
39
+
40
+ Fallback mapping:
41
+ - `atons.*` -> `ATON`
42
+ - `shore.basestations.*` -> `BASE`
43
+ - `sar.*` -> `SAR`
44
+ - `aircraft.*` -> `AIRCRAFT`
45
+ - otherwise -> `B`
46
+
47
+ ### State machine (status consistency)
48
+ *Prior to the first message, status will not exist.*
49
+ - unconfirmed = A position has been received, but the minimum message threshold has not been met.
50
+ - confirmed = The minimum number of messages has been received within the `confirmMaxAge` threshold.
51
+ - lost = No messages have been received within the `lostAfter` threshold.
52
+ - remove = No messages have been received within the `removeAfter` threshold.
53
+
54
+ If not enough messages are received within `confirmMaxAge`, the confirmation process resets and the target remains `unconfirmed`. Once enough messages are received, status becomes `confirmed`. Targets become `lost` after a period of silence and `remove` after a longer period without position.
55
+
56
+ ```mermaid
57
+ stateDiagram-v2
58
+ direction LR
59
+
60
+ [*] --> unconfirmed: first position report
61
+
62
+ unconfirmed --> unconfirmed: position report\nmsgNo < confirmAfterMsgs\n(confirmation in progress)
63
+ unconfirmed --> confirmed: position report\nmsgNo ≥ confirmAfterMsgs
64
+ unconfirmed --> unconfirmed: gap > confirmMaxAge\nreset msgCount (msgNo becomes 1)
65
+
66
+ confirmed --> confirmed: position report\nrefresh lastPosition
67
+
68
+ unconfirmed --> lost: timer\nage > lostAfter
69
+ confirmed --> lost: timer\nage > lostAfter
70
+
71
+ lost --> confirmed: position report\n(confirmAfterMsgs = 1)
72
+ lost --> unconfirmed: position report\n(confirmAfterMsgs > 1)
73
+
74
+ unconfirmed --> remove: timer\nage > removeAfter
75
+ confirmed --> remove: timer\nage > removeAfter
76
+ lost --> remove: timer\nage > removeAfter\ndelete target
77
+
78
+ remove --> confirmed: next position report\n(confirmAfterMsgs = 1)\n(new track)
79
+ remove --> unconfirmed: next position report\n(confirmAfterMsgs > 1)\n(new track)
80
+ ```
81
+
82
+ **Trigger:** `<context>.navigation.position` received
83
+ | Condition | Action |
84
+ |--- |--- |
85
+ | `msgCount` < `confirmAfterMsgs` | `status` = **'unconfirmed'** <br>`msgCount` -> increment <br>`lastPosition` = `Date.now()` |
86
+ | (`msgCount` >= `confirmAfterMsgs`) <= `confirmMaxAge` | `status` = **'confirmed'** <br>`msgCount` -> capped at `confirmAfterMsgs` <br>`lastPosition` = `Date.now()`
87
+
88
+ If the time gap between unconfirmed messages exceeds `confirmMaxAge`, `msgCount` resets to `0` before the next increment.
89
+
90
+ **Trigger:** Status Interval Timer Event
91
+ | Condition | Action |
92
+ |--- |--- |
93
+ | `Date.now() - lastPosition` > `lostAfter` | `status` = **'lost'** <br>`msgCount` = 0 <br>`lastPosition` -> unchanged |
94
+ | `Date.now() - lastPosition` > `removeAfter` | `status` = **'remove'** <br>`msgCount` = 0 <br>`lastPosition` -> unchanged <br> delete target from map |
95
+
96
+ ### Timing & publication (resource consumption)
97
+ The plugin emits Signal K deltas to the target context with:
98
+
99
+ - Path: `sensors.ais.status`
100
+ - Values: `unconfirmed`, `confirmed`, `lost`, `remove`
101
+
102
+ There are two processes that generate delta updates:
103
+ 1. Position Update: runs when position is received and applies `unconfirmed` / `confirmed` logic.
104
+ 2. Status Check: runs on a fixed interval and applies `lostAfter` and `removeAfter` thresholds.
105
+
106
+ ## State Management parameters
107
+
108
+ ### Plugin settings
109
+ - `confirmMaxAgeRatio` (number, default `1.1`): Multiplier applied to `confirmMaxAge` for all AIS classes. Example: `1.1` adds a 10% margin before ignoring a message in the confirmation process.
110
+
111
+ ### Device Class Processing Definition
112
+ ``` typescript
113
+ const AIS_CLASS_DEFAULTS = {
114
+ A: {
115
+ confirmAfterMsgs: 2,
116
+ confirmMaxAge: 180000, // ms
117
+ lostAfter: 360000, // ms
118
+ removeAfter: 540000 // ms
119
+ },
120
+ B: {
121
+ confirmAfterMsgs: 3,
122
+ confirmMaxAge: 180000, // ms
123
+ lostAfter: 360000, // ms
124
+ removeAfter: 540000 // ms
125
+ },
126
+ ATON: {
127
+ confirmAfterMsgs: 1,
128
+ confirmMaxAge: 180000, // ms
129
+ lostAfter: 900000, // ms
130
+ removeAfter: 3600000 // ms
131
+ },
132
+ BASE: {
133
+ confirmAfterMsgs: 1,
134
+ confirmMaxAge: 10000, // ms
135
+ lostAfter: 30000, // ms
136
+ removeAfter: 180000 // ms
137
+ },
138
+ SAR: {
139
+ confirmAfterMsgs: 1,
140
+ confirmMaxAge: 10000, // ms
141
+ lostAfter: 30000, // ms
142
+ removeAfter: 180000 // ms
143
+ },
144
+ AIRCRAFT: {
145
+ confirmAfterMsgs: 1,
146
+ confirmMaxAge: 10000, // ms
147
+ lostAfter: 30000, // ms
148
+ removeAfter: 180000 // ms
149
+ }
150
+ }
151
+ ```
152
+
153
+
154
+
155
+ ### Target Selection
156
+ ``` typescript
157
+ const AIS_CONTEXT_PREFIXES = [
158
+ 'atons.*',
159
+ 'shore.basestations.*',
160
+ 'vessels.*',
161
+ 'sar.*',
162
+ 'aircraft.*'
163
+ ];
164
+ ```
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "sk-ais-status-plugin",
3
+ "version": "1.0.0",
4
+ "description": "AIS Status manager plugin for Signal K server.",
5
+ "main": "plugin/index.js",
6
+ "keywords": [
7
+ "signalk-node-server-plugin"
8
+ ],
9
+ "repository": "https://github.com/panaaj/sk-ais-status-plugin",
10
+ "author": "AdrianP",
11
+ "license": "Apache-20",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "build-declaration": "tsc --declaration --allowJs false",
15
+ "watch": "npm run build -- -w",
16
+ "test": "echo \"Error: no test specified\" && exit 1",
17
+ "start": "npm run build -- -w",
18
+ "format": "prettier --write src/*",
19
+ "prepublishOnly": "tsc"
20
+ },
21
+ "dependencies": {
22
+ "@signalk/server-api": "^2.10.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/baconjs": "^0.7.34",
26
+ "@types/express": "^5.0.6",
27
+ "prettier": "^2.5.1",
28
+ "typescript": "^5.9.3"
29
+ },
30
+ "signalk-plugin-enabled-by-default": true
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // **** Signal K flags resources ****
4
+ const server_api_1 = require("@signalk/server-api");
5
+ var AIS_STATUS;
6
+ (function (AIS_STATUS) {
7
+ AIS_STATUS["unconfirmed"] = "unconfirmed";
8
+ AIS_STATUS["confirmed"] = "confirmed";
9
+ AIS_STATUS["lost"] = "lost";
10
+ AIS_STATUS["remove"] = "remove";
11
+ })(AIS_STATUS || (AIS_STATUS = {}));
12
+ const AIS_CLASS_DEFAULTS = {
13
+ A: {
14
+ confirmAfterMsgs: 2,
15
+ confirmMaxAge: 3 * 60000, // 3 min when moored, < 10 sec when moving)
16
+ lostAfter: 6 * 60000,
17
+ removeAfter: 9 * 60000
18
+ },
19
+ B: {
20
+ confirmAfterMsgs: 3,
21
+ confirmMaxAge: 3 * 60000, // 3 min when moored, < 30 sec when moving)
22
+ lostAfter: 6 * 60000,
23
+ removeAfter: 9 * 60000
24
+ },
25
+ ATON: {
26
+ confirmAfterMsgs: 1,
27
+ confirmMaxAge: 3 * 60000, // 3 min nominal
28
+ lostAfter: 15 * 60000, // 15 min = timeout / loss
29
+ removeAfter: 60 * 60000
30
+ },
31
+ BASE: {
32
+ confirmAfterMsgs: 1,
33
+ confirmMaxAge: 10000, // 10 sec nominal
34
+ lostAfter: 30000,
35
+ removeAfter: 3 * 60000
36
+ },
37
+ SAR: {
38
+ confirmAfterMsgs: 1,
39
+ confirmMaxAge: 10000, // 10 sec nominal
40
+ lostAfter: 30000,
41
+ removeAfter: 3 * 60000
42
+ },
43
+ AIRCRAFT: {
44
+ confirmAfterMsgs: 1,
45
+ confirmMaxAge: 10000, // treat similarly to SAR/BASE (fast turnover)
46
+ lostAfter: 30000,
47
+ removeAfter: 3 * 60000
48
+ }
49
+ };
50
+ const isAISClass = (value) => typeof value === 'string' &&
51
+ ['A', 'B', 'ATON', 'BASE', 'SAR', 'AIRCRAFT'].includes(value);
52
+ const classFromContext = (context) => {
53
+ if (context.startsWith('atons.'))
54
+ return 'ATON';
55
+ if (context.startsWith('shore.basestations.'))
56
+ return 'BASE';
57
+ if (context.startsWith('sar.'))
58
+ return 'SAR';
59
+ if (context.startsWith('aircraft.'))
60
+ return 'AIRCRAFT';
61
+ return 'B';
62
+ };
63
+ const STATUS_CHECK_INTERVAL = 5000;
64
+ const CONFIG_SCHEMA = {
65
+ properties: {
66
+ confirmMaxAgeRatio: {
67
+ type: 'number',
68
+ title: 'Confirmation max age margin',
69
+ description: 'Multiplier applied to the maximum message age threshold of the target confirmation process (e.g., 1.1 = +10%). Applied to all AIS classes.',
70
+ default: 1.1,
71
+ minimum: 0.1
72
+ }
73
+ }
74
+ };
75
+ const CONFIG_UISCHEMA = {};
76
+ const DEFAULT_SETTINGS = {
77
+ confirmMaxAgeRatio: 1.1
78
+ };
79
+ module.exports = (server) => {
80
+ let subscriptions = []; // stream subscriptions
81
+ let timers = []; // interval timers
82
+ const plugin = {
83
+ id: 'sk-ais-status',
84
+ name: 'AIS Status Manager',
85
+ schema: () => CONFIG_SCHEMA,
86
+ uiSchema: () => CONFIG_UISCHEMA,
87
+ start: (options, restart) => {
88
+ doStartup(options);
89
+ },
90
+ stop: () => {
91
+ doShutdown();
92
+ }
93
+ };
94
+ let settings = { ...DEFAULT_SETTINGS };
95
+ let self = '';
96
+ let targets = new Map();
97
+ const doStartup = (options) => {
98
+ try {
99
+ server.debug(`${plugin.name} starting.......`);
100
+ if (options) {
101
+ settings = { ...DEFAULT_SETTINGS, ...options };
102
+ }
103
+ else {
104
+ // save defaults if no options loaded
105
+ server.savePluginOptions(settings, () => {
106
+ server.debug(`Default configuration applied...`);
107
+ });
108
+ }
109
+ server.debug(`Applied configuration: ${JSON.stringify(settings)}`);
110
+ server.setPluginStatus(`Started`);
111
+ // initialize plugin
112
+ initialize();
113
+ }
114
+ catch (error) {
115
+ const msg = `Started with errors!`;
116
+ server.setPluginError(msg);
117
+ server.error('error: ' + error);
118
+ }
119
+ };
120
+ const doShutdown = () => {
121
+ server.debug(`${plugin.name} stopping.......`);
122
+ server.debug('** Un-registering Update Handler(s) **');
123
+ subscriptions.forEach((b) => b());
124
+ subscriptions = [];
125
+ server.debug('** Stopping Timer(s) **');
126
+ timers.forEach((t) => clearInterval(t));
127
+ timers = [];
128
+ const msg = 'Stopped.';
129
+ server.setPluginStatus(msg);
130
+ };
131
+ /**
132
+ * initialize plugin
133
+ */
134
+ const initialize = () => {
135
+ server.debug('Initializing ....');
136
+ // setup subscriptions
137
+ initSubscriptions();
138
+ self = server.getPath('self');
139
+ timers.push(setInterval(() => checkStatus(), STATUS_CHECK_INTERVAL));
140
+ };
141
+ // register DELTA stream message handler
142
+ const initSubscriptions = () => {
143
+ const subDef = [
144
+ {
145
+ path: 'navigation.position',
146
+ period: 500
147
+ }
148
+ ];
149
+ const subs = [
150
+ {
151
+ context: 'vessels.*',
152
+ subscribe: subDef
153
+ },
154
+ {
155
+ context: 'atons.*',
156
+ subscribe: subDef
157
+ },
158
+ {
159
+ context: 'shore.basestations.*',
160
+ subscribe: subDef
161
+ },
162
+ {
163
+ context: 'sar.*',
164
+ subscribe: subDef
165
+ },
166
+ {
167
+ context: 'aircraft.*',
168
+ subscribe: subDef
169
+ }
170
+ ];
171
+ server.debug(`Subscribing to contexts: ${subs.map((s) => s.context).join(', ')}`);
172
+ server.debug(`With paths: ${subDef
173
+ .map((s) => `${s.path} (${s.period} ms)`)
174
+ .join(', ')}`);
175
+ subs.forEach((s) => {
176
+ server.subscriptionmanager.subscribe(s, subscriptions, onError, onMessage);
177
+ });
178
+ };
179
+ /**
180
+ * Delta message handler
181
+ * @param delta Delta message
182
+ */
183
+ const onMessage = (delta) => {
184
+ if (!delta.updates) {
185
+ return;
186
+ }
187
+ if (delta.context === self) {
188
+ return;
189
+ }
190
+ delta.updates.forEach((u) => {
191
+ if (!(0, server_api_1.hasValues)(u)) {
192
+ return;
193
+ }
194
+ u.values.forEach((v) => {
195
+ if (v.path === 'navigation.position') {
196
+ if (!targets.has(delta.context)) {
197
+ targets.set(delta.context, {
198
+ lastPosition: 0,
199
+ msgCount: 0
200
+ });
201
+ }
202
+ processTarget(delta.context);
203
+ }
204
+ });
205
+ });
206
+ };
207
+ /** Handle subscription error */
208
+ const onError = (error) => {
209
+ server.error(`${plugin.id} Error: ${error}`);
210
+ };
211
+ /** Process target after position message */
212
+ const processTarget = (context) => {
213
+ const target = targets.get(context);
214
+ if (!target)
215
+ return;
216
+ const aisClass = getAisClass(context);
217
+ const confirmMaxAge = getConfirmMaxAge(aisClass);
218
+ const now = Date.now();
219
+ if (target.msgCount > 0 &&
220
+ target.msgCount < AIS_CLASS_DEFAULTS[aisClass].confirmAfterMsgs) {
221
+ const elapse = now - target.lastPosition;
222
+ if (elapse > confirmMaxAge) {
223
+ server.debug(`*** Confirm max age exceeded (${elapse} ms > ${confirmMaxAge} ms) -> reset confirmation`, context, aisClass);
224
+ target.msgCount = 0;
225
+ }
226
+ }
227
+ const msgNo = target.msgCount + 1;
228
+ target.lastPosition = now;
229
+ // confirmMsg threshold met?
230
+ if (msgNo < AIS_CLASS_DEFAULTS[aisClass].confirmAfterMsgs) {
231
+ target.msgCount = msgNo;
232
+ if (emitAisStatus(context, AIS_STATUS.unconfirmed)) {
233
+ server.debug(`*** Threshold not met (${msgNo}/${AIS_CLASS_DEFAULTS[aisClass].confirmAfterMsgs}) -> unconfirmed`, context, aisClass);
234
+ }
235
+ }
236
+ else {
237
+ target.msgCount = AIS_CLASS_DEFAULTS[aisClass].confirmAfterMsgs;
238
+ if (emitAisStatus(context, AIS_STATUS.confirmed)) {
239
+ server.debug(`*** Threshold met (${msgNo}/${AIS_CLASS_DEFAULTS[aisClass].confirmAfterMsgs}) -> confirmed`, context, aisClass);
240
+ }
241
+ }
242
+ targets.set(context, target);
243
+ };
244
+ /**
245
+ * Return AIS Class of supplied Context
246
+ * @param context Signal K context
247
+ * @returns AIS class (falls back to context-derived class)
248
+ */
249
+ const getAisClass = (context) => {
250
+ const aisClass = server.getPath(`${context}.sensors.ais.class`)?.value;
251
+ if (isAISClass(aisClass)) {
252
+ return aisClass;
253
+ }
254
+ return classFromContext(context);
255
+ };
256
+ const getConfirmMaxAge = (aisClass) => {
257
+ const ratio = Number(settings.confirmMaxAgeRatio);
258
+ if (!Number.isFinite(ratio) || ratio <= 0) {
259
+ return AIS_CLASS_DEFAULTS[aisClass].confirmMaxAge;
260
+ }
261
+ return Math.round(AIS_CLASS_DEFAULTS[aisClass].confirmMaxAge * ratio);
262
+ };
263
+ /**
264
+ * Check and update AIS target(s) status
265
+ */
266
+ const checkStatus = () => {
267
+ targets.forEach((v, k) => {
268
+ const aisClass = getAisClass(k);
269
+ const tDiff = Date.now() - v.lastPosition;
270
+ if (tDiff >= AIS_CLASS_DEFAULTS[aisClass].removeAfter) {
271
+ if (emitAisStatus(k, AIS_STATUS.remove)) {
272
+ server.debug('*** Remove threshold met -> remove', k, aisClass);
273
+ }
274
+ targets.delete(k);
275
+ }
276
+ else if (tDiff >= AIS_CLASS_DEFAULTS[aisClass].lostAfter) {
277
+ v.msgCount = 0;
278
+ if (emitAisStatus(k, AIS_STATUS.lost)) {
279
+ server.debug('*** Lost threshold met -> lost', k, aisClass);
280
+ }
281
+ }
282
+ });
283
+ };
284
+ /**
285
+ * Emits sensors.ais.status delta
286
+ * @param context Signal K context
287
+ */
288
+ const emitAisStatus = (context, status) => {
289
+ let currStatus = server.getPath(`${context}.sensors.ais.status`);
290
+ if (status === currStatus?.value) {
291
+ return false;
292
+ }
293
+ server.handleMessage(plugin.id, {
294
+ context: context,
295
+ updates: [
296
+ {
297
+ values: [
298
+ {
299
+ path: 'sensors.ais.status',
300
+ value: status
301
+ }
302
+ ]
303
+ }
304
+ ]
305
+ }, server_api_1.SKVersion.v1);
306
+ return true;
307
+ };
308
+ return plugin;
309
+ };
@@ -0,0 +1,175 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "version": "2.5.0",
5
+ "title": "Signal K Flags API",
6
+ "description": "API endpoints exposed by `signalk-flags` plugin for displaying flags based on MMSI or country code.",
7
+ "termsOfService": "http://signalk.org/terms/",
8
+ "license": {
9
+ "name": "Apache 2.0",
10
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
11
+ }
12
+ },
13
+ "externalDocs": {
14
+ "url": "http://signalk.org/specification/",
15
+ "description": "Signal K specification."
16
+ },
17
+ "servers": [
18
+ {
19
+ "url": "/"
20
+ }
21
+ ],
22
+ "tags": [
23
+ {
24
+ "name": "Flags",
25
+ "description": "Endpoints for retrieving country flags."
26
+ }
27
+ ],
28
+ "components": {
29
+ "schemas": {},
30
+ "responses": {
31
+ "200OKResponse": {
32
+ "description": "Successful operation",
33
+ "content": {
34
+ "application/json": {
35
+ "schema": {
36
+ "type": "object",
37
+ "description": "Request success response",
38
+ "properties": {
39
+ "state": {
40
+ "type": "string",
41
+ "enum": ["COMPLETED"]
42
+ },
43
+ "statusCode": {
44
+ "type": "number",
45
+ "enum": [200]
46
+ }
47
+ },
48
+ "required": ["state", "statusCode"]
49
+ }
50
+ }
51
+ }
52
+ },
53
+ "ErrorResponse": {
54
+ "description": "Failed operation",
55
+ "content": {
56
+ "application/json": {
57
+ "schema": {
58
+ "type": "object",
59
+ "description": "Request error response",
60
+ "properties": {
61
+ "state": {
62
+ "type": "string",
63
+ "enum": ["FAILED"]
64
+ },
65
+ "statusCode": {
66
+ "type": "number",
67
+ "enum": [404]
68
+ },
69
+ "message": {
70
+ "type": "string"
71
+ }
72
+ },
73
+ "required": ["state", "statusCode", "message"]
74
+ }
75
+ }
76
+ }
77
+ }
78
+ },
79
+ "parameters": {
80
+ "mmsi": {
81
+ "in": "path",
82
+ "required": true,
83
+ "name": "mmsi",
84
+ "description": "Maritime Mobile Service Identity (MMSI).",
85
+ "schema": {
86
+ "type": "number",
87
+ "minLength": 9,
88
+ "maxLength": 9,
89
+ "example": 520345561
90
+ }
91
+ },
92
+ "countryCode": {
93
+ "in": "path",
94
+ "required": true,
95
+ "name": "code",
96
+ "description": "Two letter country code.",
97
+ "schema": {
98
+ "type": "string",
99
+ "minLength": 2,
100
+ "maxLength": 2,
101
+ "example": "al"
102
+ }
103
+ },
104
+ "aspect": {
105
+ "in": "query",
106
+ "required": false,
107
+ "name": "aspect",
108
+ "description": "Aspect of flag image to return. 4x3 (default)",
109
+ "schema": {
110
+ "type": "string",
111
+ "enum": ["4x3", "1x1"]
112
+ }
113
+ }
114
+ },
115
+ "securitySchemes": {
116
+ "bearerAuth": {
117
+ "type": "http",
118
+ "scheme": "bearer",
119
+ "bearerFormat": "JWT"
120
+ },
121
+ "cookieAuth": {
122
+ "type": "apiKey",
123
+ "in": "cookie",
124
+ "name": "JAUTHENTICATION"
125
+ }
126
+ }
127
+ },
128
+ "security": [{ "cookieAuth": [] }, { "bearerAuth": [] }],
129
+ "paths": {
130
+ "/signalk/v2/api/resources/flags/mmsi/{mmsi}": {
131
+ "parameters": [
132
+ {
133
+ "$ref": "#/components/parameters/mmsi"
134
+ },
135
+ {
136
+ "$ref": "#/components/parameters/aspect"
137
+ }
138
+ ],
139
+ "get": {
140
+ "tags": ["Flags"],
141
+ "summary": "Returns flag for supplied MMSI.",
142
+ "responses": {
143
+ "default": {
144
+ "description": "Return SVG content for flag derived from the supplied MMSI.",
145
+ "content": {
146
+ "image/svg+xml": {}
147
+ }
148
+ }
149
+ }
150
+ }
151
+ },
152
+ "/signalk/v2/api/resources/flags/country/{code}": {
153
+ "parameters": [
154
+ {
155
+ "$ref": "#/components/parameters/countryCode"
156
+ },
157
+ {
158
+ "$ref": "#/components/parameters/aspect"
159
+ }
160
+ ],
161
+ "get": {
162
+ "tags": ["Flags"],
163
+ "summary": "Returns flag for supplied country code.",
164
+ "responses": {
165
+ "default": {
166
+ "description": "Return SVG content for flag derived from the supplied country code.",
167
+ "content": {
168
+ "image/svg+xml": {}
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }