0% found this document useful (0 votes)
3 views30 pages

Solution 2

The document outlines the architecture and design of an MQTT Ship Agent for data acquisition from ship hardware, emphasizing a cloud-based approach without a local MQTT broker. It evaluates different topic subscription hierarchies, ultimately selecting the ISA-95/UNS full hierarchy for its operational efficiency and precise QoS management. Key features include a device registry for configuration, payload validation mechanisms, and access control measures to ensure secure and efficient data handling.

Uploaded by

Vikie
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views30 pages

Solution 2

The document outlines the architecture and design of an MQTT Ship Agent for data acquisition from ship hardware, emphasizing a cloud-based approach without a local MQTT broker. It evaluates different topic subscription hierarchies, ultimately selecting the ISA-95/UNS full hierarchy for its operational efficiency and precise QoS management. Key features include a device registry for configuration, payload validation mechanisms, and access control measures to ensure secure and efficient data handling.

Uploaded by

Vikie
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

How will the ship agent know which topics to subscribe to, and how will that subscription

config
be delivered — will it be preconfigured via the desktop app the same way tags are configured?
(imp)

MQTT Ship Agent — Topic Subscription & Forwarding


Design
Internal Technical Design Note | v4 | May 2026

1. Architecture Overview
The Standard Ship Data Framework uses a .NET 8 Windows Service as the onboard data
acquisition agent. It reads directly from ship hardware using industrial protocols and publishes
normalised data to a cloud MQTT broker. There is no local MQTT broker on the ship.

Sensors / Equipment
↓ (Modbus / NMEA / CIP / Serial / HTTP)
.NET 8 Windows Service (Ship Data Agent)
↓ online: publish directly to cloud broker
↓ offline: buffer to local PostgreSQL + TimescaleDB, drain on
reconnect
Cloud MQTT Broker

[Link] Core API + PostgreSQL + TimescaleDB

Analytics / Dashboard / Reporting

Layer Responsibility
Driver Layer Reads raw data from hardware — Modbus, NMEA, CIP, Serial, HTTP.
Each protocol has its own driver.
Normalization Maps raw device values to standard fleet parameters. Adds deviceId,
Layer shipId, unit, componentId.
PostgreSQL Stores readings locally when satellite link is down. Drained in order on
Buffer reconnect.
MQTT Publisher Publishes normalised payload to cloud broker on the correct fleet topic.

2. Why There Is No Local MQTT Broker


In a typical IoT setup with off-the-shelf devices, a local broker is needed because devices are
hardcoded to publish MQTT locally and cannot reach the internet directly. This framework is
different:

1. The ship agent reads from hardware using industrial protocols — Modbus registers, NMEA
sentences, CIP tags — not MQTT subscriptions.
2. There is no intermediate publisher between devices and the agent. The agent IS the
publisher.
3. Offline resilience is handled by PostgreSQL + TimescaleDB, not by a local broker with
persistent sessions.
4. This eliminates an entire infrastructure component, reduces latency, and simplifies ops.

Key distinction: The ship agent does not subscribe to MQTT anywhere. It only publishes
MQTT — to the cloud broker. All data acquisition happens via driver protocols, not via
MQTT.

3. Topic Hierarchy — Approaches Considered


Three approaches were evaluated. Each is documented with tradeoffs because the choice
directly affects QoS, ACL, broker load, and backend filtering cost.
Approach A — Flat Hierarchy (3 levels)

fleet/{ship_id}/{component_type}

// Example:
fleet/IMO-1234567/flowmeter
fleet/IMO-1234567/engine
fleet/IMO-1234567/nav

What it gives you:

Topic count = ships × component types — 20 ships × 8 types = 160 topics permanently
Backend wildcards are simple and stable
Adding a new ship requires zero backend subscription changes

What it costs you:

QoS is assigned per topic — RPM and TEMP share the same topic and therefore the same
QoS. You cannot protect critical parameters differently from non-critical ones.
ACL operates at topic level — you cannot restrict one service to RPM only if RPM and
TEMP live in the same topic.
Backend must filter by parameter field in app code — the broker cannot do it for you.
Excessive wildcard usage on broad topics increases resource consumption on cloud broker
and backend services.

Verdict: Operationally simple but sacrifices QoS per parameter and ACL per parameter — two
of MQTT's core strengths.

Approach B — Flat Hierarchy + Criticality Suffix (4 levels)

fleet/{ship_id}/{component_type}/{criticality}

// Example:
fleet/IMO-1234567/engine/critical → RPM, load, pressure
fleet/IMO-1234567/engine/stream → temperature, vibration

What it gives you:

QoS and ACL split by criticality — critical topics get QoS 1, stream topics get QoS 0
Topic count doubles to 320 — still manageable
Broker does coarse filtering by criticality

What it costs you:

Criticality is a workaround — it approximates parameter-level control without actually having


it
RPM and TEMP are still in the same topic payload, just on different criticality buckets
Backend still needs app-code filtering to separate individual parameters
Adds ops overhead — every parameter must be classified at commissioning

Verdict: A patch for the wrong hierarchy. Discarded once ISA-95 alignment was identified as
the correct path.

Approach C — ISA-95 / UNS Full Hierarchy (5 levels, chosen)

fleet/{ship_id}/{component_type}/{device_id}/{parameter}

// Example:
fleet/IMO-1234567/engine/moxa-002/RPM
fleet/IMO-1234567/engine/moxa-002/TEMP
fleet/IMO-1234567/flowmeter/krohne-001/MEMINFLOW
fleet/IMO-1234567/nav/gps-001/HEADING

What it gives you:

Each parameter is its own topic — QoS and ACL are native, no workarounds needed
RPM gets QoS 1, TEMP gets QoS 0 — set individually at the broker, not approximated
Broker handles all filtering — backend services receive exactly the data they need, no app-
code filtering
Aligns with ISA-95 international standard used across industrial IoT
Full subscription precision — services subscribe at exactly the level they own

What it costs you:

Topic count grows: ships × components × devices × parameters


Requires subscription discipline — broad wildcards like fleet/# become expensive at
scale
Registry must define per-parameter QoS at commissioning

Verdict: Chosen approach. The correct hierarchy for this domain. Topic count is managed
through subscription discipline, not by flattening the hierarchy.
4. ISA-95 / UNS — Why This Is the Standard
ISA-95 is the international standard for industrial automation hierarchies. The Unified
Namespace (UNS) is the modern MQTT-based implementation of it. The full hierarchy it
prescribes maps directly to the ship domain:

ISA-95: Enterprise / Site / Area / Line / Cell / Device / Parameter

Ship UNS: fleet / {ship_id} / {component_type} / {device_id} / {parameter}

Real world reference — automotive (Tesla-scale):

AutoIndustries/Munich/PressShopArea/DoorPressProductionLine/CuttingWorkCell

Companies like Tesla use this for millions of vehicles over MQTT. They do not simplify the
hierarchy to make consumption easier — they keep the full depth because the hierarchy is the
data model. What they engineer carefully is the subscription layer — each service subscribes
at exactly the level it owns, never broader.

The 5-level hierarchy you originally proposed was not wrong. It is literally the industry standard.
The evaluations in Section 3 confirm it is the right choice once subscription discipline is in
place.

5. Final Topic Design


5.1 Topic format

fleet/{ship_id}/{component_type}/{device_id}/{parameter}

5.2 Examples

fleet/IMO-1234567/engine/moxa-002/RPM
fleet/IMO-1234567/engine/moxa-002/TEMP
fleet/IMO-1234567/engine/moxa-002/LOAD
fleet/IMO-1234567/flowmeter/krohne-001/MEMINFLOW
fleet/IMO-1234567/nav/gps-001/HEADING
fleet/IMO-1234567/nav/gps-001/SPEED
fleet/IMO-9999999/engine/moxa-005/RPM ← different ship, identical
shape

5.3 QoS assignment — per parameter, at the broker


Because each parameter is its own topic, QoS is set individually with no approximation:

Parameter QoS Reason


RPM 1 Operational — losing readings has consequences
LOAD 1 Operational — drives fuel calculations
MEMINFLOW 1 Operational — flow rate monitoring
HEADING 1 Safety — navigation critical
TEMP 0 Trend analysis — occasional loss acceptable
SPEED 0 Trend analysis — occasional loss acceptable

Rule: QoS 1 for parameters where losing a reading has operational or safety consequence.
QoS 0 for parameters used only in trend analysis or reporting. This decision is made at
commissioning in the device registry — not in code.

5.4 Payload structure


The payload is now intentionally minimal. The topic already carries ship, component, device,
and parameter identity — the payload carries only the reading itself:

{
"value": 20,
"unit": "m3/h",
"timestamp": "2026-05-18T12:22:00Z"
}

Full context is reconstructed on the backend by parsing the topic string:

fleet/IMO-1234567/flowmeter/krohne-001/MEMINFLOW
→ shipId: IMO-1234567
→ component: flowmeter
→ deviceId: krohne-001
→ parameter: MEMINFLOW

This keeps payload size minimal — important over satellite links with constrained bandwidth.
6. Device Registry
Each ship has a configuration file loaded by the ship agent at startup. This is the single source
of truth for what devices exist, how to read them, what parameters they expose, and what QoS
each parameter requires.

{
"shipId": "IMO-1234567",
"devices": [
{
"deviceId": "krohne-001",
"type": "flowmeter",
"componentId": "mainengine/me1",
"protocol": "Modbus",
"register": "40001",
"pollInterval": 5,
"parameters": [
{ "name": "MEMINFLOW", "unit": "m3/h", "qos": 1 }
]
},
{
"deviceId": "moxa-002",
"type": "engine",
"componentId": "mainengine",
"protocol": "Modbus",
"register": "40010",
"pollInterval": 5,
"parameters": [
{ "name": "RPM", "unit": "rpm", "qos": 1 },
{ "name": "TEMP", "unit": "C", "qos": 0 },
{ "name": "LOAD", "unit": "%", "qos": 1 }
]
},
{
"deviceId": "gps-001",
"type": "nav",
"componentId": "bridge",
"protocol": "NMEA",
"sentence": "GGA",
"pollInterval": 1,
"parameters": [
{ "name": "HEADING", "unit": "deg", "qos": 1 },
{ "name": "SPEED", "unit": "kn", "qos": 0 }
]
}
]
}

Key points:

protocol + register/sentence tells the driver how to acquire the reading — there is no
localTopic
qos is defined per parameter — the MQTT Publisher uses this when calling publish
pollInterval drives the Polling Engine per device
forwardTopic is constructed at runtime:
fleet/{shipId}/{type}/{deviceId}/{parameter}
Registry grows or shrinks per ship — topic structure never changes

Registry update process: When a device is replaced at port, the ops team pushes an
updated registry over the satellite/4G uplink. Agent picks it up on next restart. Ship crew are
not involved.

7. How Broker Routing Works


When the ship agent publishes to fleet/IMO-1234567/engine/moxa-002/RPM , the broker
matches it against all active subscriptions:

fleet/IMO-1234567/engine/moxa-002/RPM → exact match — this parameter only


fleet/IMO-1234567/engine/moxa-002/+ → all parameters from this device
fleet/IMO-1234567/engine/+/RPM → RPM from all engine devices on
this ship
fleet/+/engine/+/RPM → RPM from all engine devices, all
ships
fleet/IMO-1234567/# → everything from this ship
fleet/# → everything, all ships

The broker does this natively. No application code needed for routing. Each backend service
receives only what it subscribed to — pre-filtered at the broker, not in app code.

8. Cloud Backend Wildcard Subscriptions


Service Subscription Receives QoS
RPM monitoring fleet/+/engine/+/RPM RPM from all engine 1
devices, all ships
Flow monitoring fleet/+/flowmeter/+/MEMINFLOW Flow rate from all ships 1
Navigation fleet/+/nav/+/HEADING Heading from all ships 1
safety
Full engine data fleet/+/engine/# All engine parameters, all Mixed
ships
Per-ship fleet/IMO-1234567/# Everything from one ship Mixed
dashboard
Raw ingestion / fleet/# Everything — catch-all for Mixed
DB storage only

Subscription discipline — team rule: Every service subscribes at exactly the level it
owns. No service uses fleet/# except raw ingestion. Wildcard breadth maps directly to
broker delivery load and backend resource consumption. Every new subscription must be
reviewed for breadth before going to production. This is not optional at scale.

9. Payload Validation
Problem 1 — Empty or malformed payload
The broker routes on topic string only. It delivers empty or malformed JSON without complaint.
The backend receives it and either crashes or silently drops the reading.

Problem 2 — Missing fields inside valid JSON


value , unit , or timestamp could be null even inside valid JSON. The backend stores an
incomplete row or passes null into the formula engine producing garbage output.

Fix — validate at the ship agent before publishing

Driver reads value from hardware



Normalization layer builds payload

Validation — complete and well-formed?
↓ yes ↓ no
Publish to cloud broker Write to PostgreSQL buffer
with error flag + raw value
retry on next poll or alert ops

Field Rule
value Numeric, not null, within configured sensor range
unit Non-null, matches parameter definition in registry
timestamp Valid UTC ISO 8601, not future-dated

Second layer — defensive parsing on backend

if ([Link](payload))
return; // log and discard

var reading = [Link]<SensorReading>(payload);

if (reading?.Value is null)
// write to dead letter table — do not process

Rule: Validate at the agent — it is the only place that knows what a valid reading looks like
per parameter. Backend parsing is a safety net, not the primary defence.

10. Offline Handling

State Agent behaviour


Online Driver reads → Normalize → validate → publish to cloud broker with correct
QoS
Offline Driver reads → Normalize → validate → write to PostgreSQL buffer with
published = false

Reconnect Drain buffer ordered by captured_at ASC → publish with original QoS →
mark published = true

Important: QoS level from the registry must be preserved when draining the buffer on
reconnect — not defaulted to QoS 0.

11. Access Control


# Ship agent publishes only to its own shipId subtree
fleet/IMO-1234567/# → ship-agent-IMO-1234567 → publish only

# Parameter-level precision — RPM monitoring service


fleet/+/engine/+/RPM → rpm-service → subscribe only

# Component-level — full engine data service


fleet/+/engine/# → engine-service → subscribe only

# Raw ingestion — only legitimate broad subscriber


fleet/# → ingestion-service → subscribe only

# Third-party scoped to one vessel


fleet/IMO-1234567/# → client-vessel-xyz → subscribe only

12. Scaling Considerations


Topic count at scale:

20 ships × 8 component types × 5 devices × 8 parameters = 6,400 topics

This is manageable for any modern MQTT broker. HiveMQ, EMQX, and Mosquitto handle
millions of topics. The concern is not topic count — it is subscriber load per wildcard.

At Tesla-scale the pattern is:

MQTT broker handles IoT fan-out → bridge to Kafka or similar stream processor → aggregation
and filtering happens in the stream layer, not on the broker.

For this MVP, the broker handles everything. When fleet size justifies it, add a broker-to-stream
bridge without changing the topic structure — the ISA-95 hierarchy is compatible with this
pattern by design.

13. Summary

Decision Approach Why


No local PostgreSQL handles offline Simpler ops,
broker agent owns
Decision Approach Why
acquisition via
drivers
ISA-95 5- fleet/{shipId}/{component}/{deviceId}/{parameter} Industry
level topic standard,
native QoS +
ACL per
parameter
QoS per Defined in registry per parameter No
parameter approximation,
no criticality
suffix needed
Minimal value + unit + timestamp only Topic carries
payload all identity,
saves satellite
bandwidth
Agent config Registry with per-parameter QoS Single source
of truth, ops-
managed
Backend Narrow wildcards per service Broker filters,
subscriptions not app code
Scale path Broker → stream processor bridge No topic
restructure
needed, ISA-
95 is
compatible

Final rule: The sender's topic design and the receiver's subscription design are two
separate problems. The hierarchy must be designed with both in mind simultaneously. ISA-
95 solves the sender side completely. Subscription discipline solves the receiver side. Both
are required.

Payload validation
Problem 1 — Empty or malformed payload
The broker routes on topic string only. It delivers empty, null, or malformed JSON payloads
without complaint. The backend receives it and either crashes or silently drops the reading —
no visibility into what was lost.
Problem 2 — Missing fields inside valid JSON
Even if JSON parses correctly, individual fields like deviceId , parameter , or value could be
null or missing. The backend then stores an incomplete row — or worse, passes null into the
formula engine and produces garbage output downstream.

Fix — validate at the ship agent before publishing


The ship agent is the only place that knows what a valid payload looks like per device.
Validation must happen there, not on the backend.

Driver reads value from hardware



Normalization layer builds payload

Validation check — complete and well-formed?
↓ yes ↓ no
Publish to cloud broker Write to SQLite buffer
with error flag + raw value
retry on next poll or alert ops

Required field rules enforced by agent before every publish:

Field Rule
shipId Non-null, matches registry
deviceId Non-null, exists in registry
componentId Non-null, exists in registry
parameter Non-null, known parameter name
value Numeric, not null, within sensor range
timestamp Valid UTC ISO 8601

Second layer — defensive parsing on backend


Even with agent-side validation, the backend never trusts the payload blindly:
if ([Link](payload))
return; // log and discard

var reading = [Link]<SensorReading>(payload);

if (reading?.Value is null || reading?.DeviceId is null)


// write to dead letter table — do not process

The dead letter table holds every message that failed backend validation. No reading is silently
lost — it is quarantined for ops investigation.

Rule
Validate at the ship agent — it is the only place that knows what a valid payload looks like
per device.
Backend parsing is a safety net, not the primary defence.

UNS for ISA-95 for FPS


Flowchart

┌────────────────────────────┐
│ Device Registry JSON │
│-----------------------------│
│ shipId │
│ protocol │
│ register / sentence │
│ component type │
│ deviceId │
│ parameter mappings │
│ qos │
│ poll interval │
└─────────────┬──────────────┘


┌────────────────────────────────┐
│ .NET Ship Agent Service │
└────────────────────────────────┘

┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼

┌────────────────┐ ┌────────────────┐ ┌────────────────┐


│ Modbus Driver │ │ NMEA Driver │ │ CIP Driver │
│ Read Register │ │ Parse Sentence │ │ Read Tag/Data │
└──────┬─────────┘ └──────┬─────────┘ └──────┬─────────┘
│ │ │
└──────────────┬────────┴──────────────┬────────┘
│ │

┌──────────────────────┐
│ Normalization Layer │
│----------------------│
│ Map raw values │
│ Resolve parameter │
│ Attach metadata │
│ Validate payload │
└──────────┬───────────┘


┌──────────────────────┐
│ Topic Builder Engine │
│----------------------│
│ fleet/{ship}/ │
│ {component}/ │
│ {device}/ │
│ {parameter} │
└──────────┬───────────┘


┌──────────────────────┐
│ MQTT Publisher Layer │
│----------------------│
│ Apply QoS │
│ Publish to Broker │
└──────────┬───────────┘

┌───────────────┴────────────────┐
│ │
▼ ▼

┌────────────────────┐ ┌──────────────────────┐
│ Internet Available │ │ Internet Unavailable │
└──────────┬─────────┘ └──────────┬───────────┘
│ │
▼ ▼

┌────────────────────┐ ┌──────────────────────┐
│ Cloud MQTT Broker │ │ PostgreSQL Buffer │
│--------------------│ │----------------------│
│ Topic Routing │ │ Store unpublished │
│ Wildcard Matching │ │ telemetry locally │
│ ACL Enforcement │ │ Preserve QoS/order │
└──────────┬─────────┘ └──────────┬───────────┘
│ │
│ ▼
│ ┌────────────────────────┐
│ │ Reconnect Drain Engine │
│ │------------------------│
│ │ Publish oldest first │
│ │ Mark as published │
│ └──────────┬─────────────┘
│ │
└──────────────┬─────────────┘

┌────────────────────────┐
│ [Link] Core Backend │
│------------------------│
│ MQTT Subscribers │
│ Topic Parsing │
│ Parameter Mapping │
│ Validation │
│ Store into TimescaleDB │
└──────────┬─────────────┘

┌────────────────────────┐
│ Analytics / Dashboard │
│ Alerts / Reporting │
│ Fleet Monitoring │
└────────────────────────┘

2)What should the topic hierarchy look like — something like


fleet/{ship_id}/{component_id}/{device_id}/{parameter_id} — and how does the
broker route a published message to only the right subscriber? (imp)

Topic Hierarchy & Broker Routing


The MQTT topic hierarchy chosen for the telemetry system is:

fleet/{ship_id}/{component_type}/{device_id}/{parameter}

Examples:

fleet/IMO-1234567/engine/moxa-002/RPM
fleet/IMO-1234567/engine/moxa-002/TEMP
fleet/IMO-1234567/flowmeter/krohne-001/MEMINFLOW
fleet/IMO-1234567/nav/gps-001/HEADING

This hierarchy follows the ISA-95 / Unified Namespace (UNS) industrial telemetry standard
where the topic itself becomes part of the data model and routing identity.

Each topic level carries semantic meaning:

ship_id → identifies the vessel


component → logical subsystem (engine, flowmeter, nav)
device_id → actual onboard hardware/gateway
parameter → individual telemetry signal

This design was chosen because:

1. Each parameter becomes its own topic.


This allows:
QoS assignment per parameter
ACL security per parameter
native broker-level filtering
precise backend subscriptions
2. Backend services receive only the telemetry they actually need.
Example:

fleet/+/engine/+/RPM

receives only RPM readings from all ships.


3. The broker handles message routing natively using topic matching and wildcards.
No application-level routing logic is required.

Example routing:

Published topic:
fleet/IMO-1234567/engine/moxa-002/RPM

Possible subscribers:

fleet/IMO-1234567/engine/moxa-002/RPM
→ exact parameter only

fleet/IMO-1234567/engine/moxa-002/+
→ all parameters from one device

fleet/IMO-1234567/engine/+/RPM
→ RPM from all engine devices on one ship

fleet/+/engine/+/RPM
→ RPM from all ships

fleet/IMO-1234567/#
→ all telemetry from one ship

fleet/#
→ all telemetry in the system
The MQTT broker performs this routing internally using topic matching trees. Backend services
only receive messages matching their subscriptions.

Important scaling consideration:

The primary scalability concern is NOT total topic count. Modern brokers can handle millions of
topics. The actual concern is excessive wildcard breadth and subscriber fanout.

For example:

fleet/#

causes the subscriber to receive every telemetry message in the system and increases:

network traffic
backend CPU usage
payload parsing cost
memory usage
processing latency

Therefore, subscription discipline is required:


Every backend service must subscribe only to the narrowest topic range it actually owns.

This hierarchy also keeps the payload lightweight because most telemetry identity already
exists inside the topic itself.

Here it is:

Q3 — QoS Level Mapping Per Parameter


Internal Technical Design Note | v4 | May 2026

Question
What QoS level should each parameter type use — which parameters are strict enough to need
QoS 2, which can tolerate QoS 1 or QoS 0, and how do we decide that mapping?

Answer in Brief
The QoS mapping is not hardcoded in the application. It is defined per parameter in the device
registry JSON — making it easy to change without touching code. The actual classification of
which parameter gets which QoS level is decided by the domain team using the sensor
parameter excel sheet, not by the development team unilaterally.

What Each QoS Level Actually Means


Before mapping, the team must understand what each level guarantees — because QoS is a
cost, not just a label.

QoS Guarantee Delivery Broker storage Use when


0 Fire and forget At most once — Not stored Loss of a reading has no
may be lost operational consequence
1 Acknowledged At least once — Stored until ack Loss of a reading is
may duplicate unacceptable, duplicates
are tolerable
2 Handshake Exactly once — Stored until full Loss AND duplicate are
no loss, no handshake both unacceptable
duplicate

Critical point: QoS 2 is a 4-step handshake between publisher and broker. It doubles
network round trips per message. Over a satellite link with high latency — common on ships
— QoS 2 can significantly reduce throughput and increase buffering time. Use it only where
exactly-once delivery is genuinely required, not as a default safe choice.

Decision Framework — How to Classify Any Parameter


Three questions decide the QoS level for any parameter:

Question 1 — What happens if this reading is lost?

Nothing — it only affects a trend graph or report → QoS 0


An alert might be missed or a calculation is wrong → QoS 1
A regulatory record is incomplete or a safety event goes undetected → QoS 2

Question 2 — What happens if this reading arrives twice?

Fine — duplicate value in a time series is harmless → QoS 1


Not fine — triggers a duplicate alert, a double billing, or corrupts a counter → QoS 2

Question 3 — What is the publish frequency?

High frequency (every 1–5 seconds) — QoS 2 handshake overhead becomes significant →
prefer QoS 1 maximum
Low frequency (every 30s+) — QoS 2 overhead is tolerable if exactly-once is genuinely
needed

General Classification — Starting Point


This is a starting point only. The final mapping must be validated against the sensor parameter
excel sheet by the domain team.

QoS Parameter category Reasoning


2 Bunker / fuel transfer metering, Duplicate triggers double-billing or
regulatory compliance events, alarm duplicate alarm logs. Loss creates a
state changes regulatory gap. Exactly-once is genuinely
required.
1 RPM, shaft power, flow rate Loss is unacceptable operationally.
(operational), load, voltage, current, Duplicates are tolerable — a duplicate
GPS heading, GPS position RPM reading in a time series does no
harm.
0 Temperature trends, vibration High frequency, loss of individual readings
monitoring, auxiliary counters, does not affect operational decisions.
humidity, non-critical environmental
readings

Where the Mapping Lives — Device Registry


QoS is defined per parameter entry in the device registry JSON. This means:

Changing a parameter's QoS requires only a registry update and agent restart — no code
change
Different ships can have different QoS for the same parameter if their operational profile
differs
The ops team owns QoS classification, not the development team
{
"shipId": "IMO-1234567",
"devices": [
{
"deviceId": "krohne-001",
"type": "flowmeter",
"componentId": "mainengine/me1",
"protocol": "Modbus",
"register": "40001",
"pollInterval": 5,
"parameters": [
{ "name": "MEMINFLOW", "unit": "m3/h", "qos": 1 },
{ "name": "MEMOUTFLOW", "unit": "m3/h", "qos": 1 },
{ "name": "BUNKER", "unit": "m3", "qos": 2 }
]
},
{
"deviceId": "moxa-002",
"type": "engine",
"componentId": "mainengine",
"protocol": "Modbus",
"register": "40010",
"pollInterval": 5,
"parameters": [
{ "name": "RPM", "unit": "rpm", "qos": 1 },
{ "name": "LOAD", "unit": "%", "qos": 1 },
{ "name": "TEMP", "unit": "C", "qos": 0 }
]
},
{
"deviceId": "gps-001",
"type": "nav",
"componentId": "bridge",
"protocol": "NMEA",
"sentence": "GGA",
"pollInterval": 1,
"parameters": [
{ "name": "HEADING", "unit": "deg", "qos": 1 },
{ "name": "POSITION", "unit": "deg", "qos": 1 },
{ "name": "SPEED", "unit": "kn", "qos": 0 }
]
}
]
}
How the Ship Agent Uses QoS at Runtime
The MQTT Publisher reads the qos field per parameter from the registry and passes it directly
to the publish call:
No hardcoded QoS anywhere in application code. The registry drives everything.

QoS and Offline Buffer — Important Interaction


When the ship loses connectivity, readings are buffered in PostgreSQL. When the link restores,
the agent drains the buffer and republishes. The original QoS level must be preserved from the
registry — not defaulted to QoS 0 on drain.

On drain:

Why this matters: A QoS 2 bunker metering reading buffered during an outage must still
arrive exactly once when the link restores. Draining it at QoS 0 defeats the original
guarantee entirely.

What the Domain Team Needs to Do


The development team cannot make this classification alone. The sensor parameter excel
sheet must be reviewed and each parameter assigned a QoS level based on the three
questions above. The output of that review becomes the initial device registry entries.

Suggested columns to add to the excel sheet:

Parameter Unit Frequency Loss Duplicate QoS


consequence consequence
MEMINFLOW m3/h 5s Operational gap Harmless 1
BUNKER m3 30s Regulatory gap Double billing 2
TEMP C 5s Trend gap only Harmless 0
RPM rpm 5s Operational gap Harmless 1
HEADING deg 1s Safety gap Harmless 1

This table becomes the registry. Any future QoS change is a registry update — reviewed,
versioned, and deployed by ops without a code release.
Summary

QoS When to use Ship parameter examples


0 Loss acceptable, high frequency, TEMP, vibration, humidity, auxiliary counters
trend data only
1 Loss unacceptable, duplicates RPM, LOAD, MEMINFLOW, HEADING,
tolerable POSITION, voltage, current
2 Loss AND duplicate both BUNKER metering, regulatory compliance
unacceptable, low frequency events, alarm state transitions

Rule: QoS is a cost on the satellite link and broker. Do not default to QoS 2 for safety.
Default to QoS 1 for operational parameters and reserve QoS 2 only for parameters where
a duplicate causes a real downstream consequence — billing, regulatory records, or alarm
deduplication. The mapping lives in the registry. The domain team owns the classification.

What should the payload structure look like — what fields must every message carry regardless
of parameter type? (imp)

Payload Structure
The telemetry payload structure defines the actual sensor reading that is transported through
MQTT. Since the topic hierarchy already carries most of the telemetry identity ( ship_id ,
component , device_id , parameter ), the payload is intentionally kept minimal to reduce
bandwidth usage over satellite links.

Final payload structure:

{
"value": 20,
"unit": "m3/h",
"timestamp": "2026-05-18T12:22:00Z"
}

Example:

Topic:
fleet/IMO-1234567/flowmeter/krohne-001/MEMINFLOW

Payload:
{
"value": 20,
"unit": "m3/h",
"timestamp": "2026-05-18T12:22:00Z"
}

From the topic the backend already knows:

shipId → IMO-1234567
component → flowmeter
deviceId → krohne-001
parameter → MEMINFLOW

Therefore repeating those fields again inside the payload would:

increase payload size,


waste satellite bandwidth,
duplicate metadata,
increase serialization/deserialization overhead.

The payload should only carry the actual reading information.

Required Fields
Every telemetry payload must contain these fields regardless of parameter type.

1. value

Represents the actual sensor reading.

Examples:

RPM value
temperature
flow rate
GPS speed
pressure

Examples:

{
"value": 80
}
{
"value": 32.5
}

Rules:

must not be null


must be numeric for telemetry parameters
must be validated against expected sensor ranges
invalid readings should not be published

2. unit

Represents the engineering unit of the reading.

Examples:

rpm
m3/h
deg
C
%
kn

Example:

{
"unit": "rpm"
}

Reason:
Without units the backend or analytics engine may interpret values incorrectly.

Example:

20 could mean:
20 RPM
20 °C
20 m3/h

The unit removes ambiguity.


Rules:

must match the parameter definition in the device registry


should use standardized engineering units
should never be inferred dynamically

3. timestamp

Represents the UTC time at which the reading was captured on the ship.

Example:

{
"timestamp": "2026-05-18T12:22:00Z"
}

Rules:

must always be UTC


must use ISO 8601 format
should represent capture time, not publish time
future-dated timestamps should be rejected or flagged

Reason:
Telemetry systems require proper ordering and replay capability.

Without timestamps:

reconnect buffering becomes unreliable


ordering breaks
trend analysis becomes incorrect
delayed/offline telemetry cannot be reconstructed properly

Why Payload Is Intentionally Minimal


The system intentionally avoids large payloads because:

1. MQTT already uses topics for routing identity


2. Satellite bandwidth is constrained onboard ships
3. Smaller payloads reduce:
bandwidth usage
broker load
serialization cost
backend parsing overhead
4. Topic hierarchy already carries semantic metadata

This design follows industrial telemetry practices where:

topics carry routing identity,


payloads carry measurement values.

Backend Reconstruction
The backend reconstructs full telemetry context by combining:

MQTT topic hierarchy


payload content

Example:

Topic:
fleet/IMO-1234567/engine/moxa-002/RPM

Payload:
{
"value": 80,
"unit": "rpm",
"timestamp": "2026-05-18T12:22:00Z"
}

Backend reconstructs:

{
"shipId": "IMO-1234567",
"component": "engine",
"deviceId": "moxa-002",
"parameter": "RPM",
"value": 80,
"unit": "rpm",
"timestamp": "2026-05-18T12:22:00Z"
}
Payload Validation Rules
Validation must happen at the ship agent before publishing.

Checks:

payload is valid JSON


value is not null
value is within sensor range
unit matches registry definition
timestamp is valid UTC ISO 8601
timestamp is not corrupted or future-dated

Invalid payloads:

should not be published immediately


should be stored locally with error flags
should be retried or inspected later

Backend validation still exists as a defensive safety layer but should not be the primary
validation mechanism.

Future Extension Possibility


Additional optional metadata can later be added without changing the topic hierarchy.

Examples:

quality
alarm state
sequence number
source protocol
confidence score
checksum

Example:

{
"value": 80,
"unit": "rpm",
"timestamp": "2026-05-18T12:22:00Z",
"quality": "GOOD"
}

However, for the MVP the minimal payload structure is preferred because it keeps the telemetry
lightweight, simple, and operationally efficient.

You might also like