-
-
Notifications
You must be signed in to change notification settings - Fork 201
feat: received-message log in core (DSC first) #2805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
clarkbw
wants to merge
1
commit into
SignalK:master
Choose a base branch
from
clarkbw:feat/communications-message-log
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ dist/ | |
| .DS_Store | ||
| .vscode/ | ||
| *.db | ||
| *.db-shm | ||
| *.db-wal | ||
| logs/* | ||
| bower_components | ||
| settings/ssl-key.pem | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { describe, it } from 'mocha' | ||
| import { strict as assert } from 'assert' | ||
| import { | ||
| MessageLogEntry, | ||
| MessageLogEntryInput, | ||
| MessageLogStore | ||
| } from './communications' | ||
|
|
||
| describe('communications types', () => { | ||
| it('an input entry is assignable and an entry carries id + disposition', () => { | ||
| const input: MessageLogEntryInput = { | ||
| type: 'dsc', | ||
| priority: 'distress', | ||
| sender: { mmsi: '316123456' }, | ||
| summary: 'DSC distress alert: MMSI 316123456', | ||
| payload: { | ||
| format: '12', | ||
| category: 'distress', | ||
| natureOfDistress: 'sinking' | ||
| }, | ||
| raw: '$CDDSC,12,3161234560,...' | ||
| } | ||
| const entry: MessageLogEntry = { | ||
| ...input, | ||
| id: 'abc', | ||
| receivedAt: '2026-06-25T00:00:00.000Z', | ||
| sourceRef: 'ais.GP', | ||
| transport: 'nmea0183', | ||
| subject: undefined, | ||
| position: undefined, | ||
| notificationId: undefined, | ||
| disposition: {} | ||
| } | ||
| assert.equal(entry.type, 'dsc') | ||
| assert.equal(entry.disposition.acknowledgedAt, undefined) | ||
| }) | ||
|
|
||
| it('MessageLogStore shape is implementable', () => { | ||
| const store: Partial<MessageLogStore> = { | ||
| append: async (e) => ({ | ||
| ...e, | ||
| id: 'x', | ||
| receivedAt: '2026-06-25T00:00:00.000Z', | ||
| disposition: {} | ||
| }) | ||
| } | ||
| assert.equal(typeof store.append, 'function') | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| /** | ||
| * Received-message log types. Generic envelope; DSC is the only v1 `type`. | ||
| * @category Communications API | ||
| */ | ||
|
|
||
| export type MessageType = 'dsc' | ||
| export type MessagePriority = 'distress' | 'urgency' | 'safety' | 'routine' | ||
| export type MessageTransport = 'nmea0183' | 'nmea2000' | ||
|
|
||
| export interface MessageSender { | ||
| mmsi?: string | ||
| name?: string | ||
| } | ||
|
|
||
| /** The vessel a relay/ack/cancel refers to (distinct from the sender). */ | ||
| export interface MessageSubject { | ||
| mmsi?: string | ||
| } | ||
|
|
||
| export interface MessagePosition { | ||
| latitude: number | ||
| longitude: number | ||
| } | ||
|
|
||
| /** Audit trail mirroring the linked notification's ack/clear lifecycle. */ | ||
| export interface MessageDisposition { | ||
| acknowledgedAt?: string | ||
| clearedAt?: string | ||
| } | ||
|
|
||
| export interface MessageLogEntryCommon { | ||
| /** ISO 8601; defaults to server receive time when omitted. */ | ||
| receivedAt?: string | ||
| /** `$source` — connection/device. */ | ||
| sourceRef?: string | ||
| transport?: MessageTransport | ||
| priority: MessagePriority | ||
| sender: MessageSender | ||
| subject?: MessageSubject | ||
| position?: MessagePosition | ||
| summary: string | ||
| /** Original sentence(s) / PGN text. */ | ||
| raw: string | ||
| /** Link to a live notification, if one was raised. */ | ||
| notificationId?: string | ||
| } | ||
|
|
||
| /** | ||
| * What a producer submits to `app.logMessage()`. Server assigns | ||
| * id/receivedAt/disposition. Discriminated on `type` so each message type | ||
| * carries its own typed `payload`; new types add an arm without touching storage. | ||
| */ | ||
| export type MessageLogEntryInput = MessageLogEntryCommon & { | ||
| type: 'dsc' | ||
| payload: DscPayload | ||
| } | ||
|
|
||
| /** | ||
| * The input/persisted split keeps `id`, `receivedAt` and `disposition` | ||
| * server-assigned — producers cannot forge them on the regulatory record. | ||
| */ | ||
| export type MessageLogEntry = MessageLogEntryInput & { | ||
| id: string | ||
| receivedAt: string | ||
| disposition: MessageDisposition | ||
| } | ||
|
|
||
| export interface MessageLogQuery { | ||
| /** ISO 8601 — inclusive lower bound on receivedAt. */ | ||
| from?: string | ||
| /** ISO 8601 — inclusive upper bound on receivedAt. */ | ||
| to?: string | ||
| type?: MessageType | ||
| priority?: MessagePriority | ||
| /** Filter by sender MMSI. */ | ||
| sender?: string | ||
| limit?: number | ||
| order?: 'asc' | 'desc' | ||
| } | ||
|
|
||
| /** Mutable patch shape applied via `MessageLogStore.update`; distinct from `MessageDisposition`, the stored audit state. */ | ||
| export interface DispositionPatch { | ||
| acknowledgedAt?: string | ||
| clearedAt?: string | ||
| } | ||
|
|
||
| /** | ||
| * Storage seam. Only `SqliteMessageLogStore` ships in v1; a Postgres-class | ||
| * backend can implement this and be registered later. | ||
| * @category Communications API | ||
| */ | ||
| export interface MessageLogStore { | ||
| append(entry: MessageLogEntryInput): Promise<MessageLogEntry> | ||
| get(id: string): Promise<MessageLogEntry | undefined> | ||
| query(filter: MessageLogQuery): Promise<MessageLogEntry[]> | ||
| update( | ||
| id: string, | ||
| patch: DispositionPatch | ||
| ): Promise<MessageLogEntry | undefined> | ||
| } | ||
|
|
||
| /** | ||
| * App surface added by the Communications API: the single ingestion door | ||
| * for all message producers (parsers, manual entries, future NAVTEX/AIS-text). | ||
| * @category Communications API | ||
| */ | ||
| export type WithMessageLog = { | ||
| logMessage?: (entry: MessageLogEntryInput) => Promise<MessageLogEntry> | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // ---- DSC type-specific payload + parser contract ---- | ||
|
|
||
| /** Structured DSC call emitted by a transport parser (NMEA0183 or N2K PGN 129808). */ | ||
| export interface DscCall { | ||
| /** Format specifier symbol minus leading 1 (e.g. '12' = distress alert). */ | ||
| format: string | ||
| category: 'distress' | 'urgency' | 'safety' | 'routine' | 'unknown' | ||
| /** Sender MMSI as a string (preserve leading zeros). */ | ||
| mmsi?: string | ||
| /** Nature of distress (distress/relay calls). */ | ||
| natureOfDistress?: string | ||
| /** MMSI of the vessel in distress on relays/acks/cancellations. */ | ||
| distressMmsi?: string | ||
| position?: MessagePosition | ||
| /** UTC time reported in the call, if present. */ | ||
| reportedTime?: string | ||
| transport: MessageTransport | ||
| /** Human one-liner summary. */ | ||
| summary: string | ||
| /** Original sentence(s) / PGN text. */ | ||
| raw: string | ||
| /** `$source` of the producing connection. */ | ||
| sourceRef?: string | ||
| } | ||
|
|
||
| /** DSC content stored in `MessageLogEntry.payload`. */ | ||
| export interface DscPayload { | ||
| format: string | ||
| category: DscCall['category'] | ||
| natureOfDistress?: string | ||
| distressMmsi?: string | ||
| reportedTime?: string | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { Type, Static } from '@sinclair/typebox' | ||
|
|
||
| export const MessageDispositionSchema = Type.Object({ | ||
| acknowledgedAt: Type.Optional(Type.String({ format: 'date-time' })), | ||
| clearedAt: Type.Optional(Type.String({ format: 'date-time' })) | ||
| }) | ||
|
|
||
| export const DscPayloadSchema = Type.Object({ | ||
| format: Type.String(), | ||
| category: Type.Union([ | ||
| Type.Literal('distress'), | ||
| Type.Literal('urgency'), | ||
| Type.Literal('safety'), | ||
| Type.Literal('routine'), | ||
| Type.Literal('unknown') | ||
| ]), | ||
| natureOfDistress: Type.Optional(Type.String()), | ||
| distressMmsi: Type.Optional(Type.String()), | ||
| reportedTime: Type.Optional(Type.String()) | ||
| }) | ||
|
|
||
| export const MessageLogEntrySchema = Type.Object({ | ||
| id: Type.String(), | ||
| type: Type.Literal('dsc'), | ||
| receivedAt: Type.String({ format: 'date-time' }), | ||
| sourceRef: Type.Optional(Type.String()), | ||
| transport: Type.Optional( | ||
| Type.Union([Type.Literal('nmea0183'), Type.Literal('nmea2000')]) | ||
| ), | ||
| priority: Type.Union([ | ||
| Type.Literal('distress'), | ||
| Type.Literal('urgency'), | ||
| Type.Literal('safety'), | ||
| Type.Literal('routine') | ||
| ]), | ||
| sender: Type.Object({ | ||
| mmsi: Type.Optional(Type.String()), | ||
| name: Type.Optional(Type.String()) | ||
| }), | ||
| subject: Type.Optional(Type.Object({ mmsi: Type.Optional(Type.String()) })), | ||
| position: Type.Optional( | ||
| Type.Object({ | ||
| latitude: Type.Number(), | ||
| longitude: Type.Number() | ||
| }) | ||
| ), | ||
| summary: Type.String(), | ||
| payload: DscPayloadSchema, | ||
| raw: Type.String(), | ||
| notificationId: Type.Optional(Type.String()), | ||
| disposition: MessageDispositionSchema | ||
| }) | ||
|
|
||
| export const MessageLogListSchema = Type.Array(MessageLogEntrySchema) | ||
|
|
||
| export type MessageLogEntryStatic = Static<typeof MessageLogEntrySchema> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { | ||
| DscCall, | ||
| DscPayload, | ||
| MessageLogEntryInput, | ||
| MessagePriority | ||
| } from '@signalk/server-api' | ||
| import { createDebug } from '../../debug' | ||
|
|
||
| const debug = createDebug('signalk-server:api:communications') | ||
|
|
||
| function priorityForCategory(category: DscCall['category']): MessagePriority { | ||
| switch (category) { | ||
| case 'distress': | ||
| return 'distress' | ||
| case 'urgency': | ||
| return 'urgency' | ||
| case 'safety': | ||
| return 'safety' | ||
| default: | ||
| return 'routine' | ||
| } | ||
| } | ||
|
|
||
| export function dscToMessageInput(call: DscCall): MessageLogEntryInput { | ||
| return { | ||
| type: 'dsc', | ||
| // use the call's reported UTC when present; the store falls back to server receive time when undefined | ||
| receivedAt: call.reportedTime, | ||
| sourceRef: call.sourceRef, | ||
| transport: call.transport, | ||
| priority: priorityForCategory(call.category), | ||
| sender: { mmsi: call.mmsi }, | ||
| subject: call.distressMmsi ? { mmsi: call.distressMmsi } : undefined, | ||
| position: call.position, | ||
| summary: call.summary, | ||
| payload: { | ||
| format: call.format, | ||
| category: call.category, | ||
| natureOfDistress: call.natureOfDistress, | ||
| distressMmsi: call.distressMmsi, | ||
| reportedTime: call.reportedTime | ||
| } satisfies DscPayload, | ||
| raw: call.raw | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registration seam: parser libs (or the legacy plugin during migration) call | ||
| * `onCall` with a typed {@link DscCall}; we map it and submit via `app.logMessage`. | ||
| * Returns the bound handler so callers/tests can invoke it directly. | ||
| */ | ||
| export function registerDscAdapter(app: { | ||
| logMessage?: (e: MessageLogEntryInput) => Promise<unknown> | ||
| }): (call: DscCall) => Promise<void> { | ||
| return async (call: DscCall) => { | ||
| if (!app.logMessage) { | ||
| debug('logMessage not available; dropping DSC call') | ||
| return | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| await app.logMessage(dscToMessageInput(call)) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.