From 0aa61bac75065844df00e3eedc8b35806460daac Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 19 May 2026 14:45:10 -0500 Subject: [PATCH 01/14] initial NMEA Ais driver push --- .../sensorhub-driver-nmeaais/README.md | 121 ++++++++++++++ .../sensorhub-driver-nmeaais/build.gradle | 40 +++++ .../impl/sensor/nmeaais/Activator.java | 22 +++ .../impl/sensor/nmeaais/Descriptor.java | 42 +++++ .../impl/sensor/nmeaais/NmeaAisConfig.java | 33 ++++ .../impl/sensor/nmeaais/NmeaAisDriver.java | 114 +++++++++++++ .../impl/sensor/nmeaais/NmeaAisHandler.java | 31 ++++ .../impl/sensor/nmeaais/NmeaAisOutput.java | 143 +++++++++++++++++ .../nmeaais/NmeaAisOutputInterface.java | 7 + .../sensor/nmeaais/NmeaAisOutputPosition.java | 151 ++++++++++++++++++ .../org.sensorhub.api.module.IModuleProvider | 1 + 11 files changed, 705 insertions(+) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/README.md create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/build.gradle create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Activator.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Descriptor.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider diff --git a/sensors/positioning/sensorhub-driver-nmeaais/README.md b/sensors/positioning/sensorhub-driver-nmeaais/README.md new file mode 100644 index 000000000..7f3122295 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/README.md @@ -0,0 +1,121 @@ +# Kraken SDR +The KrakenSDR is a five-channel, phase-coherent software-defined radio (SDR) +built using RTL-SDR components, designed primarily for radio direction finding. +It utilizes five synchronized RTL-SDR receivers to achieve accurate +[beamforming](https://www.techtarget.com/searchnetworking/definition/beamforming) +and direction-of-arrival estimation. This allows users to locate the source of +radio signals, making it useful for applications like locating interference, +tracking assets, and even search and rescue efforts. + +# Hardware Setup +- KrakenSDR +- VK-162 USB GPS + + +# MANUAL SETUP with Fresh Image of RPi4 +## Expected Process: +For the following steps to work, it is assumed that you're starting with a fresh image of a raspberry pi 4. These +steps can be broken into (4) parts: +- Initial Pi setup +- Installation of [Kraken's Heimdall Firmware](https://github.com/krakenrf/heimdall_daq_fw) to handle synchronization of all 5 antennas and serve data +- Installation of [Kraken DoA DSP](https://github.com/krakenrf/krakensdr_doa) for DoA Digital Signal Processing and establishing DoA + +To begin these steps, logon to your Raspberry Pi and begin: + +## Setup Pi +1. SSH into your pi and update / install dependencies: + ``` + sudo apt update && sudo apt upgrade + ``` +2. Make sure java is installed on raspberry pi (latest 21 was used during testing) + ``` + sudo apt install openjdk-21-jdk + ``` +3. Install prerequisites to turn RTL2832U chip of the Kraken Device into SDR + ``` + sudo apt-get install libusb-dev libusb-1.0-0-dev build-essential cmake git + ``` + - Purge old RTL-SDR and librtlsdr install + ``` + sudo apt purge librtlsdr* + sudo rm -rvf /usr/lib/librtlsdr* /usr/include/rtl-sdr* /usr/local/lib/librtlsdr* /usr/local/include/rtl-sdr* + ``` +## Install HeIMDALL DAQ Firmware +Follow the [Manual Step by Step Install](https://github.com/krakenrf/heimdall_daq_fw?tab=readme-ov-file#manual-step-by-step-install) instructions +to install the HeIMDALL DAQ Firmware. Depending on your setup, take special car in following the instructions. For example, if you +are using a Raspberry Pi, make sure to follow the ARM instructions. + +## Install DoA DSP Software +Follow the [Manual Install](https://github.com/krakenrf/krakensdr_doa?tab=readme-ov-file#manual-installation-from-a-fresh-os) instructions +to install teh DoA Data Signal Processing Software. + +### Additional Info for GPS Integration +For ***Section 4***, I used a [VK-162](https://www.amazon.com/Navigation-External-Receiver-Raspberry-Geekstory/dp/B078Y52FGQ) GPS. +To set this up properly, i found the following [YouTube Tutorial](https://www.youtube.com/watch?v=A1zmhxcUOxw). However, you should be able +to type these commands in your Raspberry Pi's terminal: +1. Install GPSD +```commandline +sudo apt-get install gpsd gpsd-clients +pip3 install gpsd-py3 +``` +2. Stop current GPSD service, rebind to the correct serial, and then restart it. + +``` +sudo systemctl stop gpsd.socket +sudo systemctl disable gpsd.socket +``` +3. Update config StreamAdd +```commandline +sudo nano /lib/systemd/system/gpsd.socket +``` +Update ```ListenStream=127.0.0.1:2947``` to ```0.0.0.0:2947``` and save settings. + +4. Kill any ongoing process and rebind gpsd to serial port, most likely ttyAMC0 +``` +sudo killall gpsd +sudo gpsd /dev/ttyACM0 -F /var/run/gpsd.socket +``` +5. Feel free to test by typing ```gpsmon``` in your terminal + +### Additional Help for Remote Control +According to the `gui_run.sh` shell script located in the *krakensdr_doa* directory, the web-server is +created either using php (if remote control in not enabled) or miniserve (if remote control is enabled). + +To update this, navigate to `krakensdr_doa/_share/settings.json` and update the `en_remote_control` value to *true* + +Sometimes, miniserve must be set manaully. If you are not getting data from 8081, try manually setting up miniserve using the following command in the : +```commandline +miniserve -i 0.0.0.0 -p 8081 -P -u --on-duplicate-files overwrite -- _share +``` + +this allows the remote server to be updatable. If you continue to run into errors with the above command, make sure any process +using port 8081 has been terminated: +```java +// Check if 8081 is being used: +sudo lsof -i :8081 + +// Terminate existing process: +sudo kill -9 $(sudo lsof -t -i :8081) +``` + +### More Troubleshooting +If you find that the OSH node is still not updating the KrakenSDR's settings, check the permissions of the `_share` directory +and make sure you have write access: +```java +//Check Permissions: +ls -ld /home/user/krakensdr/krakensdr_doa/_share + +// Make sure the ownership is correct and change if needed: +sudo chown -R user:user /home/user/krakensdr/krakensdr_doa/_share + +// Update permissions: +chmod -R u+rw /home/user/krakensdr/krakensdr_doa/_share + +``` + + +## Helpful Resources +- [KrakenSDR Wiki](https://github.com/krakenrf/krakensdr_docs/wiki/) +- [Kraken Pi Image](https://github.com/krakenrf/krakensdr_doa/releases) +- [Kraken DOA video](https://www.youtube.com/watch?v=3ugAT5BLBc0) +- [DOA QUICKSTART](https://github.com/krakenrf/krakensdr_docs/wiki/02.-Direction-Finding-Quickstart-Guide) \ No newline at end of file diff --git a/sensors/positioning/sensorhub-driver-nmeaais/build.gradle b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle new file mode 100644 index 000000000..ceb2c7dbb --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle @@ -0,0 +1,40 @@ +description = 'NMEA AIS Decoder' +ext.details = "Driver to Decode NMEA AIS Messages" +version = '1.0.0' + +dependencies { + implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion +// implementation project(':sensorhub-comm-rxtx') + testImplementation('junit:junit:4.13.1') +// embeddedImpl 'com.neuronrobotics:nrjavaserial:5.2.1' +// embeddedImpl 'commons-net:commons-net:3.9.0' +// embeddedImpl 'com.google.code.gson:gson:2.10.1' +} + +// exclude tests requiring connection to the sensor +// these have to be run manually +// If tests are to be excluded list them here as follows +// exclude '**/TestNameClass.class' +//test { +// useJUnit() +//} + +// add info to OSGi manifest +osgi { + manifest { + attributes ('Bundle-Vendor': 'Botts Inc') + attributes ('Bundle-Activator': 'org.sensorhub.impl.sensor.aisshipxplorer.Activator') + } +} + +// add info to maven pom +ext.pom >>= { + developers { + developer { + id 'BillBrown341' + name 'Bill Brown' + organization 'Botts-inc' + organizationUrl 'https://botts-inc.com/' + } + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Activator.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Activator.java new file mode 100644 index 000000000..d173c407c --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Activator.java @@ -0,0 +1,22 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020-2025 Botts Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.sensor.nmeaais; + +import org.sensorhub.utils.OshBundleActivator; + +/** + * The presence of this class tells the OpenSensorHub OSGI machinery about this module. + * It is referenced in the 'Bundle-Activator' attribute in the OSGI section of the build.gradle file. + */ +@SuppressWarnings("unused") +public class Activator extends OshBundleActivator { +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Descriptor.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Descriptor.java new file mode 100644 index 000000000..e745439b4 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Descriptor.java @@ -0,0 +1,42 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020-2025 Botts Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.sensor.nmeaais; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.module.JarModuleProvider; + +/** + * Descriptor classes provide access to informative data on the OpenSensorHub driver. + */ +public class Descriptor extends JarModuleProvider implements IModuleProvider { + /** + * Retrieves the class implementing the OpenSensorHub interface necessary to perform SOS/SPS/SOS-T operations. + * + * @return The class used to interact with the sensor/sensor platform. + */ + @Override + public Class> getModuleClass() { + return NmeaAisDriver.class; + } + + /** + * Identifies the class used to configure this driver. + * + * @return The java class used to exposing configuration settings for the driver. + */ + @Override + public Class getModuleConfigClass() { + return NmeaAisConfig.class; + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java new file mode 100644 index 000000000..e0effd342 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java @@ -0,0 +1,33 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020-2025 Botts Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.sensor.nmeaais; + +import org.sensorhub.api.config.DisplayInfo; +import org.sensorhub.api.sensor.SensorConfig; + +/** + * Configuration settings for the {@link NmeaAisDriver} driver exposed via the OpenSensorHub Admin panel. + */ +public class NmeaAisConfig extends SensorConfig { + + /** + * The unique identifier for the configured sensor (or sensor platform). + */ + @DisplayInfo.Required + @DisplayInfo(desc = "Serial number or unique identifier") + public String serialNumber = "myShipXplorer"; + + @DisplayInfo.Required + @DisplayInfo(label = "UDP Port", desc = "Local port to receive AIS NMEA sentences from AIS-Catcher (default: 10110)") + public int udpPort = 10110; + +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java new file mode 100644 index 000000000..ab2deaad6 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -0,0 +1,114 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020-2025 Botts Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.sensor.nmeaais; + +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * Driver implementation for the sensor. + *

+ * This class is responsible for providing sensor information, managing output registration, + * and performing initialization and shutdown for the driver and its outputs. + */ +public class NmeaAisDriver extends AbstractSensorModule { + static final String UID_PREFIX = "urn:osh:sensor:shipxplorer:"; + static final String XML_PREFIX = "shipXplorer"; + + // GLOBAL VARIABLES FOR SENSOR OPERATION + NmeaAisHandler nmeaAisHandler; + NmeaAisOutputPosition nmeaAisOutputPosition; + + static final int MAX_PACKET_SIZE = 4096; + + DatagramSocket socket; + volatile boolean started; + + // INITIALIZE + @Override + public void doInit() throws SensorHubException { + super.doInit(); + + // Create SensorHub Identifiers using designated prefix and serial number from Admin Panel + generateUniqueID(UID_PREFIX, config.serialNumber); + generateXmlID(XML_PREFIX, config.serialNumber); + + // INITIALIZE OUTPUT + nmeaAisOutputPosition = new NmeaAisOutputPosition(this); + addOutput(nmeaAisOutputPosition, false); + nmeaAisOutputPosition.doInit(); + + // Initialize Parser + nmeaAisHandler = new NmeaAisHandler(this); + } + + + @Override + public void doStart() throws SensorHubException { + super.doStart(); + + try { + // SO_REUSEADDR allows binding to a port that another process (e.g. AIS-Catcher) is also sending to, + // preventing "Address already in use" when multiple consumers subscribe to the same UDP feed. + socket = new DatagramSocket(null); + socket.setReuseAddress(true); + socket.bind(new InetSocketAddress(config.udpPort)); + getLogger().info("Listening for AIS data on UDP port {}", config.udpPort); + } catch (IOException e) { + throw new SensorHubException("Cannot bind UDP socket on port " + config.udpPort, e); + } + + Thread t = new Thread(() -> { + byte[] buf = new byte[MAX_PACKET_SIZE]; + DatagramPacket packet = new DatagramPacket(buf, buf.length); + while (started) { + try { + socket.receive(packet); + String aisNmeaMsg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim(); + nmeaAisHandler.handleNmeaAisMessage(aisNmeaMsg); + + } catch (IOException e) { + if (started) + getLogger().error("Error reading AIS UDP packet", e); + } + } + }); + + started = true; + t.setName("ais-reader"); + t.setDaemon(true); + t.start(); + } + + @Override + public void doStop() throws SensorHubException { + started = false; + + if (socket != null) { + socket.close(); + socket = null; + } + + super.doStop(); + } + + @Override + public boolean isConnected() { + return socket != null && !socket.isClosed(); + } + +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java new file mode 100644 index 000000000..c3d3386d9 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -0,0 +1,31 @@ +package org.sensorhub.impl.sensor.nmeaais; + +public class NmeaAisHandler { + String nmeaAisMsg; + String rawPayload; + + private final NmeaAisDriver nmeaAisDriver; + + public NmeaAisHandler(NmeaAisDriver driver){ + this.nmeaAisDriver = driver; + } + + public void handleNmeaAisMessage(String nmeaAisMsg){ + this.nmeaAisMsg = nmeaAisMsg; + String[] nmea = nmeaAisMsg.split(","); + rawPayload = nmea[5]; + parsePayload(rawPayload); + } + + public void parsePayload(String payload){ + // Todo This function will take the raw payload and get its Message Id. If Message ID is 1, 2, or 3 it will ParsePositionReport() + + } + + public void parsePositionReport(String positionPayload){ + // Todo This function will create a position report array or object with the following variables: + } + + + +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java new file mode 100644 index 000000000..4928c7ce7 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java @@ -0,0 +1,143 @@ +package org.sensorhub.impl.sensor.nmeaais; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +import java.util.Objects; + +public class NmeaAisOutput extends VarRateSensorOutput { + private static final double INITIAL_SAMPLING_PERIOD = 1.0; + + protected DataRecord aisRecord; + protected DataEncoding dataEncoding; + protected int aisRecordSize; + + // AIS Message Variables + String sentenceType; + int fragmentTotal; + int fragmentNumber; + String sequentialId; + String channel; + double channelFreq; + String rawPayload; + int fillBits; + String checkSum; + + NmeaAisOutput(String outputName, NmeaAisDriver nmeaAisDriver) { + super(outputName, nmeaAisDriver, INITIAL_SAMPLING_PERIOD); + } + + /** + * Initializes the data structure for the output, defining the fields, their ordering, and data types. + */ + void doInit(String outputName, String outputLabel, String outputDescription, String outputDefinition) { + // Get an instance of SWE Factory suitable to build components + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + // Create the data record description + + // Create the data record description + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(outputName) + .label(outputLabel) + .description(outputDescription) + .definition(outputDefinition) + .addField("nmeaAisMsg", sweFactory.createRecord() + .label("NMEA AIS Message Info") + .description("Data Record for the general NMEA AIS message") + .definition(SWEHelper.getPropertyUri("NmeaAisMsg")) + .addField("sampleTime", geoFac.createTime() + .asSamplingTimeIsoUTC() + .label("Sample Time") + .description("Time of data collection") + .definition("SampleTime")) + .addField("sentenceType", sweFactory.createText() + .label("Sentence Type") + .description("AIS VHF Data-link Message") + .definition(SWEHelper.getPropertyUri("SentenceType"))) + .addField("fragmentCount", sweFactory.createQuantity() + .label("Fragment Count") + .description("Total number of fragments in this message") + .definition(SWEHelper.getPropertyUri("FragmentCount"))) + .addField("fragmentNumber", sweFactory.createQuantity() + .label("Fragment Number") + .description("Number of fragment per message") + .definition(SWEHelper.getPropertyUri("FragmentNumber"))) + .addField("sequentialId", sweFactory.createText() + .label("Sequential Id") + .description("Id to link multipart messages") + .definition(SWEHelper.getPropertyUri("SequentialId"))) + .addField("channel", sweFactory.createText() + .label("AIS Channel") + .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") + .definition(SWEHelper.getPropertyUri("Channel"))) + .addField("rawPayload", sweFactory.createText() + .label("Raw PayLoad") + .description("Encoded AIS payload") + .definition(SWEHelper.getPropertyUri("RawPayload"))) + .addField("fillBits", sweFactory.createQuantity() + .label("Fill Bits") + .description("Padding bits added to final payload") + .definition(SWEHelper.getPropertyUri("FillBits"))) + .addField("checkSum", sweFactory.createText() + .label("Check Sum") + .description("NMEA checksum (hex)") + .definition(SWEHelper.getPropertyUri("CheckSum"))) + ); + + aisRecord = recordBuilder.build(); + + aisRecordSize = aisRecord.getNumFields()-1; + + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + + @Override + public DataComponent getRecordDescription() { + return aisRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + + public void setAisMsgData(String nmeaAisMsg){ + // Step 1: Parse NMEA fields of NMEA AIS Sentence (ex. nmea = !AIVDM,1,1,,A,15Muan<000qm2=2CavBWSCL20@2?,0*6A) + String[] nmea = nmeaAisMsg.split(","); + sentenceType = nmea[0]; + fragmentTotal = Integer.parseInt(nmea[1]); + fragmentNumber = Integer.parseInt(nmea[2]); + sequentialId = nmea[3]; + channel = nmea[4]; + channelFreq = Objects.equals(channel, "A") ? 161.975 :162.025; + rawPayload = nmea[5]; + + // Split up last field to get checksum and fill bits + String[] lastField = nmea[6].split("\\*"); // last field of a nmea sentence is a [fill bits and checksum] + fillBits = Integer.parseInt(lastField[0].trim()); + checkSum = lastField[1].trim(); + } + + public void populateNmeaAisDataStructure(DataBlock dataBlock){ + // Populate Parent Class Packet Data + dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(1, sentenceType); + dataBlock.setIntValue(2, fragmentTotal); + dataBlock.setIntValue(3, fragmentNumber); + dataBlock.setStringValue(4, sequentialId); + dataBlock.setStringValue(5, channel); + dataBlock.setStringValue(6, rawPayload); + dataBlock.setIntValue(7, fillBits); + dataBlock.setStringValue(8, checkSum); + } + +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java new file mode 100644 index 000000000..30bf098eb --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java @@ -0,0 +1,7 @@ +package org.sensorhub.impl.sensor.nmeaais; + +public interface NmeaAisOutputInterface { + void setData( String nmeaAisMsg, String[] payloadData ); +} + + diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java new file mode 100644 index 000000000..a7a39ba5f --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java @@ -0,0 +1,151 @@ +package org.sensorhub.impl.sensor.nmeaais; + +import net.opengis.swe.v20.DataBlock; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NmeaAisOutputPosition extends NmeaAisOutput implements NmeaAisOutputInterface { + private static final String OUTPUT_NAME = "nmeaAisOutputPosition"; + private static final String OUTPUT_LABEL = "Position Report"; + private static final String OUTPUT_DESCRIPTION = "Class A AIS Position Report"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputPosition"); + + private final Object processingLock = new Object(); + + NmeaAisOutputPosition(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver); + } + + /** + * Initializes the data structure for the output, defining the fields, their ordering, and data types. + */ + void doInit() { + // INITIALIZE THE PACKET PARENT CLASS + super.doInit(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION); + + // Get an instance of SWE Factory suitable to build components + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + aisRecord.addField("payloadInfo", sweFactory.createRecord() + .label("Decoded Payload") + .description("Decoded Payload") + .definition(SWEHelper.getPropertyUri("Payload")) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message 1, 2 or 3") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createText() + .label("MMSI Number") + .description("MMSI Number") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("navStatus", sweFactory.createQuantity() + .label("Navigational Status") + .description( + "0 = under way using engine, 1 = at anchor, 2 = not under command, 3 = restricted maneuverability, 4 = constrained by her draught, 5 = moored, 6 = aground, 7 = engaged in fishing, 8 = under way sailing, 9 = reserved for future amendment of navigational status for ships carrying DG, HS, or MP, or IMO hazard or pollutant category C, high speed craft (HSC), 10 = reserved for future amendment of navigational status for ships carrying dangerous goods (DG), harmful substances (HS) or marine pollutants (MP), or IMO hazard or pollutant category A, wing in ground (WIG); 11 = power-driven vessel towing astern (regional use); 12 = power-driven vessel pushing ahead or towing alongside (regional use);\n" + + "13 = reserved for future use,\n" + + "14 = AIS-SART (active), MOB-AIS, EPIRB-AIS\n" + + "15 = undefined = default (also used by AIS-SART, MOB-AIS and EPIRB-AIS under test)" + ) + .definition(SWEHelper.getPropertyUri("NavStatus"))) + .addField("rot", sweFactory.createQuantity() + .label("Rate of Turn") + .description( + "0 to +126 = turning right at up to 708 deg per min or higher\n" + + "0 to -126 = turning left at up to 708 deg per min or higher Values between 0 and 708 deg per min coded by ROTAIS = 4.733 SQRT(ROTsensor) degrees per min\n" + + "where ROTsensor is the Rate of Turn as input by an external Rate of Turn Indicator (TI). ROTAIS is rounded to the nearest integer value.\n" + + "+127 = turning right at more than 5 deg per 30 s (No TI available)\n" + + "-127 = turning left at more than 5 deg per 30 s (No TI available)\n" + + "-128 (80 hex) indicates no turn information available (default).\n" + + "ROT data should not be derived from COG information." + ) + .definition(SWEHelper.getPropertyUri("Rot"))) + .addField("positionAccuracy", sweFactory.createQuantity() + .label("Position Accuracy") + .description( + "1 = high (<= 10 m)\n" + + "0 = low (> 10 m)\n" + + "0 = default" + ) + .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) + .addField("location",geoFac.createLocationVectorLatLon() + .label("Location")) + .addField("cog",sweFactory.createQuantity() + .label("COG") + .description("Course over ground in 1/10 = (0-3599). 3600 (E10h) = not available = default. 3 601-4 095 should not be used") + .definition(SWEHelper.getPropertyUri("Cog"))) + .addField("heading",sweFactory.createQuantity() + .label("True Heading") + .uom("deg") + .description("Degrees (0-359) (511 indicates not available = default)") + .definition(SWEHelper.getPropertyUri("heading"))) + .addField("timeStamp",sweFactory.createTime() + .label("Time Stamp") + .description("UTC second when the report was generated by the electronic position system (EPFS) (0-59, or 60 if time stamp is not available, which should also be the default value, or 61 if positioning system is in manual input mode, or 62 if electronic position fixing system operates in estimated (dead reckoning) mode, or 63 if the positioning system is inoperative)") + .definition(SWEHelper.getPropertyUri("TimeStamp"))) + .addField("smi",sweFactory.createQuantity() + .label("Special Maneuvre Indicator") + .description("0 = not available = default\n" + + "1 = not engaged in special maneuver\n" + + "2 = engaged in special maneuver\n" + + "(i.e.: regional passing arrangement on Inland Waterway)") + .definition(SWEHelper.getPropertyUri("Smi"))) + .addField("raim",sweFactory.createQuantity() + .label("RAIM-flag") + .description("Receiver autonomous integrity monitoring (RAIM) flag of electronic position fixing device; 0 = RAIM not in use = default; 1 = RAIM in use. See Table") + .definition(SWEHelper.getPropertyUri("Raim"))) + .addField("commState",sweFactory.createQuantity() + .label("Communication State") + .description("visit https://www.navcen.uscg.gov/ais-class-a-reports#CommState") + .definition(SWEHelper.getPropertyUri("CommState"))) + .addField("bits",sweFactory.createQuantity() + .label("Number of Bits") + .description("Number of Bits") + .definition(SWEHelper.getPropertyUri("bits"))) + .build() + ); + + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + /** + * Sets the data for the output and publishes it. + */ + @Override + public void setData(String nmeaAisMsg, String[] positionData) { + synchronized (processingLock) { + // Set PacketInfo in Parent Class + setAisMsgData(nmeaAisMsg); + +// try { + DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); + + // POPULATE PACKET FIELDS + populateNmeaAisDataStructure(dataBlock); + +// // POPULATE POSITION FIELDS +// dataBlock.setDoubleValue(aisRecordSize + 1, lon); +// dataBlock.setDoubleValue(aisRecordSize + 2, lat); +// dataBlock.setDoubleValue(aisRecordSize + 3, alt); +// +// // CREATE FOI UID +// String foiUID = parentSensor.addFoi(packetFrom); +// // Publish the data block +// latestRecord = dataBlock; +// latestRecordTime = System.currentTimeMillis(); +// updateSamplingPeriod(latestRecordTime); +// eventHandler.publish(new DataEvent(latestRecordTime, MeshtasticOutputPosition.this, foiUID, dataBlock)); + + +// } catch (InvalidProtocolBufferException e) { +// parentSensor.getLogger().error("Failed to parse Position payload: {}", e.getMessage()); +// } + + } + } + +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensors/positioning/sensorhub-driver-nmeaais/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 000000000..abf62acae --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.nmeaais.Descriptor From 1784b39a8307d14d03cfc87748318bbb0ab31efd Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 19 May 2026 16:05:28 -0500 Subject: [PATCH 02/14] reorganized output and report types. Added multiple message handling in handler --- .../impl/sensor/nmeaais/NmeaAisDriver.java | 40 ++++- .../impl/sensor/nmeaais/NmeaAisHandler.java | 157 ++++++++++++++++-- .../nmeaais/NmeaAisOutputInterface.java | 7 - .../nmeaais/{ => Outputs}/NmeaAisOutput.java | 5 +- .../Outputs/NmeaAisOutputInterface.java | 9 + .../{ => Outputs}/NmeaAisOutputPosition.java | 107 +++++++----- .../nmeaais/ReportTypes/PositionReport.java | 24 +++ 7 files changed, 289 insertions(+), 60 deletions(-) delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{ => Outputs}/NmeaAisOutput.java (96%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputInterface.java rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{ => Outputs}/NmeaAisOutputPosition.java (68%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index ab2deaad6..417ab75fe 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -13,6 +13,9 @@ import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.nmeaais.Outputs.NmeaAisOutputPosition; +import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; +import org.vast.ogc.om.MovingFeature; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; @@ -26,8 +29,8 @@ * and performing initialization and shutdown for the driver and its outputs. */ public class NmeaAisDriver extends AbstractSensorModule { - static final String UID_PREFIX = "urn:osh:sensor:shipxplorer:"; - static final String XML_PREFIX = "shipXplorer"; + static final String UID_PREFIX = "urn:osh:sensor:nmea:ais:"; + static final String XML_PREFIX = "nmea:ais:"; // GLOBAL VARIABLES FOR SENSOR OPERATION NmeaAisHandler nmeaAisHandler; @@ -111,4 +114,37 @@ public boolean isConnected() { return socket != null && !socket.isClosed(); } + /** + * Registers an AIS vessel as a Feature of Interest (FOI) keyed by its MMSI. + * Subsequent calls with the same MMSI are no-ops; the existing FOI UID is returned. + */ + + /** + * Registers an AIS vessel as a Feature of Interest (FOI) keyed by its MMSI. + * Subsequent calls with the same MMSI are no-ops; the existing FOI UID is returned. + */ + public String addFoi(String mmsi) { + String foiUID = UID_PREFIX + "foi:" + mmsi; + + if (!foiMap.containsKey(foiUID)) { + MovingFeature foi = new MovingFeature(); + foi.setId(mmsi); + foi.setUniqueIdentifier(foiUID); + foi.setName("Vessel " + mmsi); + foi.setDescription("AIS vessel with MMSI " + mmsi); + addFoi(foi); + getLogger().debug("New AIS vessel added as FOI: {}", foiUID); + } + + return foiUID; + } + + /** + * Forwards a decoded position report to the position output for publishing. + * Called by {@link NmeaAisHandler} — keeps the handler decoupled from the Outputs package. + */ + void publishPositionReport(String nmeaAisMsg, PositionReport report) { + nmeaAisOutputPosition.setData(nmeaAisMsg, report); + } + } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index c3d3386d9..20fd0f3c5 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -1,31 +1,168 @@ package org.sensorhub.impl.sensor.nmeaais; +import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; + +import java.util.HashMap; +import java.util.Map; + public class NmeaAisHandler { String nmeaAisMsg; String rawPayload; private final NmeaAisDriver nmeaAisDriver; - public NmeaAisHandler(NmeaAisDriver driver){ + /** + * Buffer for assembling multi-sentence AIS messages. + * Key: sequential ID (field 3 of the NMEA sentence, "1"–"9"). + */ + private final Map fragmentBuffers = new HashMap<>(); + + public NmeaAisHandler(NmeaAisDriver driver) { this.nmeaAisDriver = driver; } - public void handleNmeaAisMessage(String nmeaAisMsg){ - this.nmeaAisMsg = nmeaAisMsg; - String[] nmea = nmeaAisMsg.split(","); - rawPayload = nmea[5]; - parsePayload(rawPayload); + public void handleNmeaAisMessage(String sentence) { + String[] nmea = sentence.split(","); + int fragmentCount = Integer.parseInt(nmea[1]); + int fragmentNumber = Integer.parseInt(nmea[2]); + String sequentialId = nmea[3]; + String payload = nmea[5]; + + // Check Fragment Count to see if there are multiple messages that will need to be combined + if (fragmentCount == 1) { + // Single-sentence message — process immediately + this.nmeaAisMsg = sentence; + this.rawPayload = payload; + parsePayload(payload); + } else { + // Multi-sentence message — buffer until all fragments arrive + reassembleAndProcess(sentence, fragmentCount, fragmentNumber, sequentialId, payload); + } + } + + /** + * Buffers an individual fragment. When all fragments for a sequential ID have + * arrived the payloads are concatenated in order and passed to {@link #parsePayload}. + * The NMEA envelope from the first fragment is used for output metadata. + */ + private void reassembleAndProcess(String sentence, int fragmentCount, int fragmentNumber, + String sequentialId, String payload) { + FragmentBuffer buf = fragmentBuffers.get(sequentialId); + + // If no buffer exists yet, or a stale buffer with a different fragment count is + // sitting there (e.g. a previous message was never completed), start fresh. + if (buf == null || buf.fragmentCount != fragmentCount) { + buf = new FragmentBuffer(fragmentCount, sentence); + fragmentBuffers.put(sequentialId, buf); + } + + buf.addFragment(fragmentNumber, payload); + + if (buf.isComplete()) { + fragmentBuffers.remove(sequentialId); + this.nmeaAisMsg = buf.firstSentence; + this.rawPayload = buf.getCombinedPayload(); + parsePayload(this.rawPayload); + } + } + + public void parsePayload(String payload) { + int messageId = extractBits(payload, 0, 6); + if (messageId == 1 || messageId == 2 || messageId == 3) { + PositionReport report = parsePositionReport(payload); + nmeaAisDriver.publishPositionReport(nmeaAisMsg, report); + } } - public void parsePayload(String payload){ - // Todo This function will take the raw payload and get its Message Id. If Message ID is 1, 2, or 3 it will ParsePositionReport() + public PositionReport parsePositionReport(String payload) { + PositionReport report = new PositionReport(); + + report.messageId = extractBits(payload, 0, 6); + report.repeat = extractBits(payload, 6, 2); + report.mmsi = String.format("%09d", extractBits(payload, 8, 30)); + report.navStatus = extractBits(payload, 38, 4); + report.rot = signExtend(extractBits(payload, 42, 8), 8); + report.sog = extractBits(payload, 50, 10) / 10.0; + report.posAccuracy = extractBits(payload, 60, 1); + report.longitude = signExtend(extractBits(payload, 61, 28), 28) / 600000.0; + report.latitude = signExtend(extractBits(payload, 89, 27), 27) / 600000.0; + report.cog = extractBits(payload, 116, 12) / 10.0; + report.heading = extractBits(payload, 128, 9); + report.timeStamp = extractBits(payload, 137, 6); + report.smi = extractBits(payload, 143, 2); + report.spare = extractBits(payload, 145, 3); + report.raim = extractBits(payload, 148, 1); + report.commState = extractBits(payload, 149, 19); + report.bits = payload.length() * 6; + + return report; + } + /** + * Extracts {@code numBits} bits starting at {@code startBit} from an AIS + * ASCII-armored payload string (6 bits per character, MSB first). + */ + private int extractBits(String payload, int startBit, int numBits) { + int result = 0; + for (int i = 0; i < numBits; i++) { + int bitPos = startBit + i; + int charIndex = bitPos / 6; + int bitInChar = 5 - (bitPos % 6); // MSB first within each 6-bit group + int charVal = payload.charAt(charIndex) - 48; + if (charVal > 40) charVal -= 8; + int bit = (charVal >> bitInChar) & 1; + result = (result << 1) | bit; + } + return result; } - public void parsePositionReport(String positionPayload){ - // Todo This function will create a position report array or object with the following variables: + /** + * Sign-extends a value that was extracted as an unsigned int from {@code numBits} bits + * into a signed Java int using two's-complement interpretation. + */ + private int signExtend(int value, int numBits) { + if ((value & (1 << (numBits - 1))) != 0) { + value -= (1 << numBits); + } + return value; } + // ------------------------------------------------------------------------- + // Fragment reassembly support + // ------------------------------------------------------------------------- + private static class FragmentBuffer { + final int fragmentCount; + /** NMEA sentence from the first fragment — used for the output's envelope fields. */ + final String firstSentence; + private final String[] payloads; + private int receivedCount; + FragmentBuffer(int fragmentCount, String firstSentence) { + this.fragmentCount = fragmentCount; + this.firstSentence = firstSentence; + this.payloads = new String[fragmentCount]; + } + + void addFragment(int fragmentNumber, String payload) { + int index = fragmentNumber - 1; // fragment numbers are 1-based + if (payloads[index] == null) { + payloads[index] = payload; + receivedCount++; + } + } + + boolean isComplete() { + return receivedCount == fragmentCount; + } + + /** Concatenates all fragment payloads in order into a single bit string. */ + String getCombinedPayload() { + StringBuilder sb = new StringBuilder(); + for (String p : payloads) { + sb.append(p); + } + return sb.toString(); + } + } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java deleted file mode 100644 index 30bf098eb..000000000 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputInterface.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.sensorhub.impl.sensor.nmeaais; - -public interface NmeaAisOutputInterface { - void setData( String nmeaAisMsg, String[] payloadData ); -} - - diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutput.java similarity index 96% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutput.java index 4928c7ce7..908c4d5cb 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutput.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutput.java @@ -1,10 +1,11 @@ -package org.sensorhub.impl.sensor.nmeaais; +package org.sensorhub.impl.sensor.nmeaais.Outputs; import net.opengis.swe.v20.DataBlock; import net.opengis.swe.v20.DataComponent; import net.opengis.swe.v20.DataEncoding; import net.opengis.swe.v20.DataRecord; import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; import org.vast.swe.SWEBuilders; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; @@ -93,7 +94,7 @@ void doInit(String outputName, String outputLabel, String outputDescription, Str aisRecord = recordBuilder.build(); - aisRecordSize = aisRecord.getNumFields()-1; + aisRecordSize = 9; // 9 leaf fields in nmeaAisMsg sub-record (flat indices 0–8) dataEncoding = geoFac.newTextEncoding(",", "\n"); } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputInterface.java new file mode 100644 index 000000000..12aa417b2 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputInterface.java @@ -0,0 +1,9 @@ +package org.sensorhub.impl.sensor.nmeaais.Outputs; + +import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; + +public interface NmeaAisOutputInterface { + void setData(String nmeaAisMsg, PositionReport report); +} + + diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java similarity index 68% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java index a7a39ba5f..8bc667f3f 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisOutputPosition.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java @@ -1,6 +1,9 @@ -package org.sensorhub.impl.sensor.nmeaais; +package org.sensorhub.impl.sensor.nmeaais.Outputs; import net.opengis.swe.v20.DataBlock; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; @@ -12,14 +15,14 @@ public class NmeaAisOutputPosition extends NmeaAisOutput implements NmeaAisOutpu private final Object processingLock = new Object(); - NmeaAisOutputPosition(NmeaAisDriver nmeaAisDriver) { + public NmeaAisOutputPosition(NmeaAisDriver nmeaAisDriver) { super(OUTPUT_NAME, nmeaAisDriver); } /** * Initializes the data structure for the output, defining the fields, their ordering, and data types. */ - void doInit() { + public void doInit() { // INITIALIZE THE PACKET PARENT CLASS super.doInit(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION); @@ -27,6 +30,23 @@ void doInit() { GeoPosHelper geoFac = new GeoPosHelper(); SWEHelper sweFactory = new SWEHelper(); + // Flat index map for payloadInfo sub-record (NMEA fields occupy 0–8): + // 9 = messageId + // 10 = repeat + // 11 = mmsi + // 12 = navStatus + // 13 = rot + // 14 = sog + // 15 = positionAccuracy + // 16 = latitude (lat component of createLocationVectorLatLon) + // 17 = longitude (lon component of createLocationVectorLatLon) + // 18 = cog + // 19 = heading + // 20 = timeStamp + // 21 = smi + // 22 = raim + // 23 = commState + // 24 = bits aisRecord.addField("payloadInfo", sweFactory.createRecord() .label("Decoded Payload") .description("Decoded Payload") @@ -64,6 +84,11 @@ void doInit() { "ROT data should not be derived from COG information." ) .definition(SWEHelper.getPropertyUri("Rot"))) + .addField("sog", sweFactory.createQuantity() + .label("Speed Over Ground") + .description("Speed over ground in knots (0–102.2 knots, 102.3 = not available, 102.3+ should not be used)") + .uom("[kn_i]") + .definition(SWEHelper.getPropertyUri("SpeedOverGround"))) .addField("positionAccuracy", sweFactory.createQuantity() .label("Position Accuracy") .description( @@ -72,37 +97,37 @@ void doInit() { "0 = default" ) .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) - .addField("location",geoFac.createLocationVectorLatLon() + .addField("location", geoFac.createLocationVectorLatLon() .label("Location")) - .addField("cog",sweFactory.createQuantity() + .addField("cog", sweFactory.createQuantity() .label("COG") - .description("Course over ground in 1/10 = (0-3599). 3600 (E10h) = not available = default. 3 601-4 095 should not be used") + .description("Course over ground in 1/10 = (0-3599). 3600 (E10h) = not available = default. 3601-4095 should not be used") .definition(SWEHelper.getPropertyUri("Cog"))) - .addField("heading",sweFactory.createQuantity() + .addField("heading", sweFactory.createQuantity() .label("True Heading") .uom("deg") .description("Degrees (0-359) (511 indicates not available = default)") .definition(SWEHelper.getPropertyUri("heading"))) - .addField("timeStamp",sweFactory.createTime() + .addField("timeStamp", sweFactory.createTime() .label("Time Stamp") .description("UTC second when the report was generated by the electronic position system (EPFS) (0-59, or 60 if time stamp is not available, which should also be the default value, or 61 if positioning system is in manual input mode, or 62 if electronic position fixing system operates in estimated (dead reckoning) mode, or 63 if the positioning system is inoperative)") .definition(SWEHelper.getPropertyUri("TimeStamp"))) - .addField("smi",sweFactory.createQuantity() + .addField("smi", sweFactory.createQuantity() .label("Special Maneuvre Indicator") .description("0 = not available = default\n" + "1 = not engaged in special maneuver\n" + "2 = engaged in special maneuver\n" + "(i.e.: regional passing arrangement on Inland Waterway)") .definition(SWEHelper.getPropertyUri("Smi"))) - .addField("raim",sweFactory.createQuantity() + .addField("raim", sweFactory.createQuantity() .label("RAIM-flag") .description("Receiver autonomous integrity monitoring (RAIM) flag of electronic position fixing device; 0 = RAIM not in use = default; 1 = RAIM in use. See Table") .definition(SWEHelper.getPropertyUri("Raim"))) - .addField("commState",sweFactory.createQuantity() + .addField("commState", sweFactory.createQuantity() .label("Communication State") .description("visit https://www.navcen.uscg.gov/ais-class-a-reports#CommState") .definition(SWEHelper.getPropertyUri("CommState"))) - .addField("bits",sweFactory.createQuantity() + .addField("bits", sweFactory.createQuantity() .label("Number of Bits") .description("Number of Bits") .definition(SWEHelper.getPropertyUri("bits"))) @@ -116,36 +141,40 @@ void doInit() { * Sets the data for the output and publishes it. */ @Override - public void setData(String nmeaAisMsg, String[] positionData) { + public void setData(String nmeaAisMsg, PositionReport report) { synchronized (processingLock) { - // Set PacketInfo in Parent Class setAisMsgData(nmeaAisMsg); -// try { - DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); - - // POPULATE PACKET FIELDS - populateNmeaAisDataStructure(dataBlock); - -// // POPULATE POSITION FIELDS -// dataBlock.setDoubleValue(aisRecordSize + 1, lon); -// dataBlock.setDoubleValue(aisRecordSize + 2, lat); -// dataBlock.setDoubleValue(aisRecordSize + 3, alt); -// -// // CREATE FOI UID -// String foiUID = parentSensor.addFoi(packetFrom); -// // Publish the data block -// latestRecord = dataBlock; -// latestRecordTime = System.currentTimeMillis(); -// updateSamplingPeriod(latestRecordTime); -// eventHandler.publish(new DataEvent(latestRecordTime, MeshtasticOutputPosition.this, foiUID, dataBlock)); - - -// } catch (InvalidProtocolBufferException e) { -// parentSensor.getLogger().error("Failed to parse Position payload: {}", e.getMessage()); -// } - + DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); + + // NMEA envelope fields (flat indices 0–8) + populateNmeaAisDataStructure(dataBlock); + + // Decoded payload fields (flat indices 9–24) + dataBlock.setIntValue(9, report.messageId); + dataBlock.setIntValue(10, report.repeat); + dataBlock.setStringValue(11, report.mmsi); + dataBlock.setIntValue(12, report.navStatus); + dataBlock.setIntValue(13, report.rot); + dataBlock.setDoubleValue(14, report.sog); + dataBlock.setIntValue(15, report.posAccuracy); + dataBlock.setDoubleValue(16, report.latitude); + dataBlock.setDoubleValue(17, report.longitude); + dataBlock.setDoubleValue(18, report.cog); + dataBlock.setIntValue(19, report.heading); + dataBlock.setIntValue(20, report.timeStamp); + dataBlock.setIntValue(21, report.smi); + dataBlock.setIntValue(22, report.raim); + dataBlock.setIntValue(23, report.commState); + dataBlock.setIntValue(24, report.bits); + + // Register the vessel as a FOI keyed by MMSI + String foiUID = parentSensor.addFoi(report.mmsi); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPosition.this, foiUID, dataBlock)); } } - } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java new file mode 100644 index 000000000..6803433b2 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java @@ -0,0 +1,24 @@ +package org.sensorhub.impl.sensor.nmeaais.ReportTypes; + +/** + * Holds decoded fields from an AIS Class A Position Report (message types 1, 2, 3). + */ +public class PositionReport { + public int messageId; // 1, 2, or 3 + public int repeat; // 0–3 + public String mmsi; // 9-digit MMSI, zero-padded + public int navStatus; // 0–15 + public int rot; // signed, -128 to +127 deg/min + public double sog; // speed over ground in knots (raw / 10.0) + public int posAccuracy; // 0 = low, 1 = high + public double longitude; // decimal degrees (raw / 600000.0) + public double latitude; // decimal degrees (raw / 600000.0) + public double cog; // course over ground in degrees (raw / 10.0) + public int heading; // true heading 0–359, 511 = not available + public int timeStamp; // UTC second 0–59, 60/61/62/63 = special + public int smi; // special manoeuvre indicator 0–2 + public int spare; // 3 bits, reserved + public int raim; // 0 = not in use, 1 = in use + public int commState; // 19-bit communication state + public int bits; // total payload bits (always 168 for Class A) +} From 8379f1abb067f413b8eb91ce9edebedacecadcc9 Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Wed, 20 May 2026 14:52:10 -0500 Subject: [PATCH 03/14] refractored Class A reports and added class B report --- .../impl/sensor/nmeaais/NmeaAisDriver.java | 31 +++- .../impl/sensor/nmeaais/NmeaAisHandler.java | 43 ++++- .../Outputs/NmeaAisOutputInterface.java | 9 - .../{Outputs => outputs}/NmeaAisOutput.java | 47 +++-- .../outputs/NmeaAisOutputInterface.java | 7 + .../NmeaAisOutputPositionClassA.java} | 92 ++++------ .../outputs/NmeaAisOutputPositionClassB.java | 166 ++++++++++++++++++ .../PositionReportClassA.java} | 6 +- .../reportschemas/PositionReportClassB.java | 28 +++ 9 files changed, 331 insertions(+), 98 deletions(-) delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputInterface.java rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{Outputs => outputs}/NmeaAisOutput.java (78%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{Outputs/NmeaAisOutputPosition.java => outputs/NmeaAisOutputPositionClassA.java} (76%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{ReportTypes/PositionReport.java => reportschemas/PositionReportClassA.java} (87%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index 417ab75fe..b4cfe3b09 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -13,8 +13,10 @@ import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; -import org.sensorhub.impl.sensor.nmeaais.Outputs.NmeaAisOutputPosition; -import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassA; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassB; +import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; +import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; import org.vast.ogc.om.MovingFeature; import java.io.IOException; import java.net.DatagramPacket; @@ -34,10 +36,15 @@ public class NmeaAisDriver extends AbstractSensorModule { // GLOBAL VARIABLES FOR SENSOR OPERATION NmeaAisHandler nmeaAisHandler; - NmeaAisOutputPosition nmeaAisOutputPosition; + NmeaAisOutputPositionClassA nmeaAisOutputPositionClassA; + NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; static final int MAX_PACKET_SIZE = 4096; +// static final String test1 = "!AIVDM,1,1,,A,15NfK=PP00qm21jCarCv4?wf20S4,0*13"; +// static final String test2 = "!AIVDM,1,1,,B,35NNm0dP@Vqm19HCbhs { } /** - * Initializes the data structure for the output, defining the fields, their ordering, and data types. + * Returns a {@link SWEBuilders.DataRecordBuilder} pre-populated with the record metadata + * and the {@code nmeaAisMsg} sub-record (flat indices 0–8). Subclasses can chain additional + * fields onto this builder before calling {@code .build()} once, avoiding a per-field + * {@code .build()} call on an already-constructed {@code DataRecord}. */ - void doInit(String outputName, String outputLabel, String outputDescription, String outputDefinition) { - // Get an instance of SWE Factory suitable to build components - GeoPosHelper geoFac = new GeoPosHelper(); + protected SWEBuilders.DataRecordBuilder createRecordBuilder( + String outputName, + String outputLabel, + String outputDescription, + String outputDefinition + ) { SWEHelper sweFactory = new SWEHelper(); - // Create the data record description - - // Create the data record description - SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + return sweFactory.createRecord() .name(outputName) .label(outputLabel) .description(outputDescription) @@ -53,7 +56,7 @@ void doInit(String outputName, String outputLabel, String outputDescription, Str .label("NMEA AIS Message Info") .description("Data Record for the general NMEA AIS message") .definition(SWEHelper.getPropertyUri("NmeaAisMsg")) - .addField("sampleTime", geoFac.createTime() + .addField("sampleTime", sweFactory.createTime() .asSamplingTimeIsoUTC() .label("Sample Time") .description("Time of data collection") @@ -78,6 +81,10 @@ void doInit(String outputName, String outputLabel, String outputDescription, Str .label("AIS Channel") .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") .definition(SWEHelper.getPropertyUri("Channel"))) + .addField("frequency", sweFactory.createText() + .label("AIS Channel Frequency") + .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") + .definition(SWEHelper.getPropertyUri("Frequency"))) .addField("rawPayload", sweFactory.createText() .label("Raw PayLoad") .description("Encoded AIS payload") @@ -91,12 +98,15 @@ void doInit(String outputName, String outputLabel, String outputDescription, Str .description("NMEA checksum (hex)") .definition(SWEHelper.getPropertyUri("CheckSum"))) ); + } - aisRecord = recordBuilder.build(); - - aisRecordSize = 9; // 9 leaf fields in nmeaAisMsg sub-record (flat indices 0–8) - - dataEncoding = geoFac.newTextEncoding(",", "\n"); + /** + * Initializes the data structure for the output, defining the fields, their ordering, and data types. + */ + void doInit(String outputName, String outputLabel, String outputDescription, String outputDefinition) { + aisRecord = createRecordBuilder(outputName, outputLabel, outputDescription, outputDefinition).build(); + aisRecordSize = aisRecord.getNumFields()-1; // 9 leaf fields in nmeaAisMsg sub-record (flat indices 0–8) + dataEncoding = new GeoPosHelper().newTextEncoding(",", "\n"); } @@ -136,9 +146,10 @@ public void populateNmeaAisDataStructure(DataBlock dataBlock){ dataBlock.setIntValue(3, fragmentNumber); dataBlock.setStringValue(4, sequentialId); dataBlock.setStringValue(5, channel); - dataBlock.setStringValue(6, rawPayload); - dataBlock.setIntValue(7, fillBits); - dataBlock.setStringValue(8, checkSum); + dataBlock.setDoubleValue(6, channelFreq); + dataBlock.setStringValue(7, rawPayload); + dataBlock.setIntValue(8, fillBits); + dataBlock.setStringValue(9, checkSum); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java new file mode 100644 index 000000000..4d295d65a --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java @@ -0,0 +1,7 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +public interface NmeaAisOutputInterface { + void setData(String nmeaAisMsg, T report); +} + + diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java similarity index 76% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java index 8bc667f3f..f83399ef5 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/Outputs/NmeaAisOutputPosition.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java @@ -1,56 +1,42 @@ -package org.sensorhub.impl.sensor.nmeaais.Outputs; +package org.sensorhub.impl.sensor.nmeaais.outputs; import net.opengis.swe.v20.DataBlock; import org.sensorhub.api.data.DataEvent; import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; -import org.sensorhub.impl.sensor.nmeaais.ReportTypes.PositionReport; +import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPosition extends NmeaAisOutput implements NmeaAisOutputInterface { - private static final String OUTPUT_NAME = "nmeaAisOutputPosition"; - private static final String OUTPUT_LABEL = "Position Report"; +public class NmeaAisOutputPositionClassA extends NmeaAisOutput implements NmeaAisOutputInterface { + private static final String OUTPUT_NAME = "nmeaAisOutputPositionClassA"; + private static final String OUTPUT_LABEL = "Position Report Class A"; private static final String OUTPUT_DESCRIPTION = "Class A AIS Position Report"; - private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputPosition"); + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputPositionClassA"); private final Object processingLock = new Object(); - public NmeaAisOutputPosition(NmeaAisDriver nmeaAisDriver) { + public NmeaAisOutputPositionClassA(NmeaAisDriver nmeaAisDriver) { super(OUTPUT_NAME, nmeaAisDriver); } /** * Initializes the data structure for the output, defining the fields, their ordering, and data types. + * + * Flat index map (nmeaAisMsg sub-record occupies 0–8; position fields are top-level siblings): + * 9 = messageId 10 = repeat 11 = mmsi + * 12 = navStatus 13 = rot 14 = sog + * 15 = positionAccuracy + * 16 = latitude (lat component of createLocationVectorLatLon) + * 17 = longitude (lon component of createLocationVectorLatLon) + * 18 = cog 19 = heading 20 = timeStamp + * 21 = smi 22 = raim 23 = commState + * 24 = bits */ public void doInit() { - // INITIALIZE THE PACKET PARENT CLASS - super.doInit(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION); - - // Get an instance of SWE Factory suitable to build components GeoPosHelper geoFac = new GeoPosHelper(); SWEHelper sweFactory = new SWEHelper(); - // Flat index map for payloadInfo sub-record (NMEA fields occupy 0–8): - // 9 = messageId - // 10 = repeat - // 11 = mmsi - // 12 = navStatus - // 13 = rot - // 14 = sog - // 15 = positionAccuracy - // 16 = latitude (lat component of createLocationVectorLatLon) - // 17 = longitude (lon component of createLocationVectorLatLon) - // 18 = cog - // 19 = heading - // 20 = timeStamp - // 21 = smi - // 22 = raim - // 23 = commState - // 24 = bits - aisRecord.addField("payloadInfo", sweFactory.createRecord() - .label("Decoded Payload") - .description("Decoded Payload") - .definition(SWEHelper.getPropertyUri("Payload")) + aisRecord = createRecordBuilder(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION) .addField("messageId", sweFactory.createQuantity() .label("Message Id") .description("Identifier for this message 1, 2 or 3") @@ -131,8 +117,8 @@ public void doInit() { .label("Number of Bits") .description("Number of Bits") .definition(SWEHelper.getPropertyUri("bits"))) - .build() - ); + .build(); + aisRecordSize = 9; dataEncoding = geoFac.newTextEncoding(",", "\n"); } @@ -141,7 +127,7 @@ public void doInit() { * Sets the data for the output and publishes it. */ @Override - public void setData(String nmeaAisMsg, PositionReport report) { + public void setData(String nmeaAisMsg, PositionReportClassA report) { synchronized (processingLock) { setAisMsgData(nmeaAisMsg); @@ -150,23 +136,23 @@ public void setData(String nmeaAisMsg, PositionReport report) { // NMEA envelope fields (flat indices 0–8) populateNmeaAisDataStructure(dataBlock); - // Decoded payload fields (flat indices 9–24) - dataBlock.setIntValue(9, report.messageId); - dataBlock.setIntValue(10, report.repeat); - dataBlock.setStringValue(11, report.mmsi); - dataBlock.setIntValue(12, report.navStatus); - dataBlock.setIntValue(13, report.rot); - dataBlock.setDoubleValue(14, report.sog); - dataBlock.setIntValue(15, report.posAccuracy); - dataBlock.setDoubleValue(16, report.latitude); - dataBlock.setDoubleValue(17, report.longitude); - dataBlock.setDoubleValue(18, report.cog); - dataBlock.setIntValue(19, report.heading); - dataBlock.setIntValue(20, report.timeStamp); - dataBlock.setIntValue(21, report.smi); - dataBlock.setIntValue(22, report.raim); - dataBlock.setIntValue(23, report.commState); - dataBlock.setIntValue(24, report.bits); + // Decoded payload fields (flat indices 10–25) + dataBlock.setIntValue(aisRecordSize+1, report.messageId); + dataBlock.setIntValue(aisRecordSize + 2, report.repeat); + dataBlock.setStringValue(aisRecordSize + 3, report.mmsi); + dataBlock.setIntValue(aisRecordSize + 4, report.navStatus); + dataBlock.setIntValue(aisRecordSize + 5, report.rot); + dataBlock.setDoubleValue(aisRecordSize + 6, report.sog); + dataBlock.setIntValue(aisRecordSize + 7, report.posAccuracy); + dataBlock.setDoubleValue(aisRecordSize + 8, report.latitude); + dataBlock.setDoubleValue(aisRecordSize + 9, report.longitude); + dataBlock.setDoubleValue(aisRecordSize + 10, report.cog); + dataBlock.setIntValue(aisRecordSize + 11, report.heading); + dataBlock.setIntValue(aisRecordSize + 12, report.timeStamp); + dataBlock.setIntValue(aisRecordSize + 13, report.smi); + dataBlock.setIntValue(aisRecordSize + 14, report.raimFlag); + dataBlock.setIntValue(aisRecordSize + 15, report.commState); + dataBlock.setIntValue(aisRecordSize + 16, report.bits); // Register the vessel as a FOI keyed by MMSI String foiUID = parentSensor.addFoi(report.mmsi); @@ -174,7 +160,7 @@ public void setData(String nmeaAisMsg, PositionReport report) { latestRecord = dataBlock; latestRecordTime = System.currentTimeMillis(); updateSamplingPeriod(latestRecordTime); - eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPosition.this, foiUID, dataBlock)); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassA.this, foiUID, dataBlock)); } } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java new file mode 100644 index 000000000..4d419ed4b --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java @@ -0,0 +1,166 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import net.opengis.swe.v20.DataBlock; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NmeaAisOutputPositionClassB extends NmeaAisOutput implements NmeaAisOutputInterface { + private static final String OUTPUT_NAME = "nmeaAisOutputPositionClassB"; + private static final String OUTPUT_LABEL = "Position Report Class B"; + private static final String OUTPUT_DESCRIPTION = "Class B AIS Position Report"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputPositionClassB"); + + private final Object processingLock = new Object(); + + public NmeaAisOutputPositionClassB(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver); + } + + /** + * Initializes the data structure for the output, defining the fields, their ordering, and data types. + */ + public void doInit() { + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + aisRecord = createRecordBuilder(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message 1, 2 or 3") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createText() + .label("MMSI Number") + .description("MMSI Number") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("sog", sweFactory.createQuantity() + .label("Speed Over Ground") + .description("Speed over ground in knots (0–102.2 knots, 102.3 = not available, 102.3+ should not be used)") + .uom("[kn_i]") + .definition(SWEHelper.getPropertyUri("SpeedOverGround"))) + .addField("positionAccuracy", sweFactory.createQuantity() + .label("Position Accuracy") + .description( + "1 = high (<= 10 m)\n" + + "0 = low (> 10 m)\n" + + "0 = default" + ) + .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) + .addField("location", geoFac.createLocationVectorLatLon() + .label("Location")) + .addField("cog", sweFactory.createQuantity() + .label("COG") + .description("Course over ground in 1/10 = (0-3599). 3600 (E10h) = not available = default. 3601-4095 should not be used") + .definition(SWEHelper.getPropertyUri("Cog"))) + .addField("heading", sweFactory.createQuantity() + .label("True Heading") + .uom("deg") + .description("Degrees (0-359) (511 indicates not available = default)") + .definition(SWEHelper.getPropertyUri("heading"))) + .addField("timeStamp", sweFactory.createTime() + .label("Time Stamp") + .description("UTC second when the report was generated by the electronic position system (EPFS) (0-59, or 60 if time stamp is not available, which should also be the default value, or 61 if positioning system is in manual input mode, or 62 if electronic position fixing system operates in estimated (dead reckoning) mode, or 63 if the positioning system is inoperative)") + .definition(SWEHelper.getPropertyUri("TimeStamp"))) + .addField("unitFlag", sweFactory.createQuantity() + .label("Class B Unit Flag") + .description("0 (Class B unit) or 1 (Class B CS unit)") + .definition(SWEHelper.getPropertyUri("UnitFlag"))) + .addField("displayFlag", sweFactory.createQuantity() + .label("Class B Display Flag") + .description("0 = No display available; not capable of displaying Message 12 and 14\n" + + "1 = Equipped with integrated display displaying Message 12 and 14") + .definition(SWEHelper.getPropertyUri("DisplayFlag"))) + .addField("dscFlag", sweFactory.createQuantity() + .label("Class B DSC Flag") + .description("0 = Not equipped with DSC function\n" + + "1 = Equipped with DSC function (dedicated or time-shared)") + .definition(SWEHelper.getPropertyUri("DscFlag"))) + .addField("bandFlag", sweFactory.createQuantity() + .label("Class B Band Flag") + .description("0 = Capable of operating over the upper 525 kHz band of the marine band\n" + + "1 = Capable of operating over the whole marine band (irrelevant if \"Class B Message 22 flag\" is 0)") + .definition(SWEHelper.getPropertyUri("BandFlag"))) + .addField("message22Flag", sweFactory.createQuantity() + .label("Class B Message 22 Flag") + .description("0 = No frequency management via Message 22 , operating on AIS1, AIS2 only\n" + + "1 = Frequency management via Message 22") + .definition(SWEHelper.getPropertyUri("Message22Flag"))) + .addField("modeFlag", sweFactory.createQuantity() + .label("Mode Flag") + .description("0 = Station operating in autonomous and continuous mode = default\n" + + "1 = Station operating in assigned mode") + .definition(SWEHelper.getPropertyUri("ModeFlag"))) + .addField("raim", sweFactory.createQuantity() + .label("RAIM-flag") + .description("Receiver autonomous integrity monitoring (RAIM) flag of electronic position fixing device; 0 = RAIM not in use = default; 1 = RAIM in use. See Table") + .definition(SWEHelper.getPropertyUri("Raim"))) + .addField("commStateFlag", sweFactory.createQuantity() + .label("Communication State Selector Flag") + .description("0 = SOTDMA communication state follows\n" + + "1 = ITDMA communication state follows (always 1 for Class-B \"CS\")") + .definition(SWEHelper.getPropertyUri("CommStateFlag"))) + .addField("commState", sweFactory.createQuantity() + .label("Communication State") + .description("OTDMA communication state. Because Class B \"CS\" does not use any Communication State information, this field shall be filled with the following value: 1100000000000000110.") + .definition(SWEHelper.getPropertyUri("CommState"))) + .addField("bits", sweFactory.createQuantity() + .label("Number of Bits") + .description("Number of Bits") + .definition(SWEHelper.getPropertyUri("bits"))) + .build(); + aisRecordSize = 9; + + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + /** + * Sets the data for the output and publishes it. + */ + @Override + public void setData(String nmeaAisMsg, PositionReportClassB report) { + synchronized (processingLock) { + setAisMsgData(nmeaAisMsg); + + DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); + + // NMEA envelope fields (flat indices 0–8) + populateNmeaAisDataStructure(dataBlock); + + // Decoded payload fields (flat indices 10–25) + dataBlock.setIntValue(aisRecordSize+1, report.messageId); + dataBlock.setIntValue(aisRecordSize + 2, report.repeat); + dataBlock.setStringValue(aisRecordSize + 3, report.mmsi); + dataBlock.setDoubleValue(aisRecordSize + 4, report.sog); + dataBlock.setIntValue(aisRecordSize + 5, report.posAccuracy); + dataBlock.setDoubleValue(aisRecordSize + 6, report.latitude); + dataBlock.setDoubleValue(aisRecordSize + 7, report.longitude); + dataBlock.setDoubleValue(aisRecordSize + 8, report.cog); + dataBlock.setIntValue(aisRecordSize + 9, report.heading); + dataBlock.setIntValue(aisRecordSize + 10, report.timeStamp); + dataBlock.setIntValue(aisRecordSize + 11, report.unitFlag); + dataBlock.setIntValue(aisRecordSize + 12, report.displayFlag); + dataBlock.setIntValue(aisRecordSize + 13, report.dscFlag); + dataBlock.setIntValue(aisRecordSize + 14, report.bandFlag); + dataBlock.setIntValue(aisRecordSize + 15, report.message22Flag); + dataBlock.setIntValue(aisRecordSize + 16, report.modeFlag); + dataBlock.setIntValue(aisRecordSize + 17, report.raimFlag); + dataBlock.setIntValue(aisRecordSize + 18, report.commStateFlag); + dataBlock.setIntValue(aisRecordSize + 19, report.commState); + dataBlock.setIntValue(aisRecordSize + 20, report.bits); + + // Register the vessel as a FOI keyed by MMSI + String foiUID = parentSensor.addFoi(report.mmsi); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassB.this, foiUID, dataBlock)); + } + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java similarity index 87% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java index 6803433b2..93c6e6836 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/ReportTypes/PositionReport.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java @@ -1,9 +1,9 @@ -package org.sensorhub.impl.sensor.nmeaais.ReportTypes; +package org.sensorhub.impl.sensor.nmeaais.reportschemas; /** * Holds decoded fields from an AIS Class A Position Report (message types 1, 2, 3). */ -public class PositionReport { +public class PositionReportClassA { public int messageId; // 1, 2, or 3 public int repeat; // 0–3 public String mmsi; // 9-digit MMSI, zero-padded @@ -18,7 +18,7 @@ public class PositionReport { public int timeStamp; // UTC second 0–59, 60/61/62/63 = special public int smi; // special manoeuvre indicator 0–2 public int spare; // 3 bits, reserved - public int raim; // 0 = not in use, 1 = in use + public int raimFlag; // 0 = not in use, 1 = in use public int commState; // 19-bit communication state public int bits; // total payload bits (always 168 for Class A) } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java new file mode 100644 index 000000000..d33c0c829 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java @@ -0,0 +1,28 @@ +package org.sensorhub.impl.sensor.nmeaais.reportschemas; + +/** + * Holds decoded fields from an AIS Class A Position Report (message types 1, 2, 3). + */ +public class PositionReportClassB { + public int messageId; + public int repeat; + public String mmsi; + public double sog; + public int posAccuracy; + public double longitude; + public double latitude; + public double cog; + public int heading; + public int timeStamp; + public int unitFlag; + public int displayFlag; + public int dscFlag; + public int bandFlag; + public int message22Flag; + public int modeFlag; + public int raimFlag; + public int commStateFlag; + public int commState; + public int bits; + +} From dd3b49c8cc335d3d80b8133586b5100c8bd61dda Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Wed, 20 May 2026 15:33:45 -0500 Subject: [PATCH 04/14] made AIS Messags its own output --- .../impl/sensor/nmeaais/NmeaAisDriver.java | 23 ++- .../impl/sensor/nmeaais/NmeaAisHandler.java | 8 +- .../outputs/NmeaAidOutputRawMessages.java | 134 +++++++++++++++ .../sensor/nmeaais/outputs/NmeaAisOutput.java | 155 ------------------ .../outputs/NmeaAisOutputInterface.java | 7 - .../outputs/NmeaAisOutputPositionClassA.java | 99 ++++++----- .../outputs/NmeaAisOutputPositionClassB.java | 103 +++++++----- .../outputs/NmeaAisReportInterface.java | 5 + 8 files changed, 278 insertions(+), 256 deletions(-) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutput.java delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index b4cfe3b09..be54436df 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -13,6 +13,7 @@ import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAidOutputRawMessages; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassA; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassB; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; @@ -36,6 +37,7 @@ public class NmeaAisDriver extends AbstractSensorModule { // GLOBAL VARIABLES FOR SENSOR OPERATION NmeaAisHandler nmeaAisHandler; + NmeaAidOutputRawMessages nmeaAidOutputRawMessages; NmeaAisOutputPositionClassA nmeaAisOutputPositionClassA; NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; @@ -58,6 +60,10 @@ public void doInit() throws SensorHubException { generateXmlID(XML_PREFIX, config.serialNumber); // INITIALIZE OUTPUT + nmeaAidOutputRawMessages = new NmeaAidOutputRawMessages(this); + addOutput(nmeaAidOutputRawMessages, false); + nmeaAidOutputRawMessages.doInit(); + nmeaAisOutputPositionClassA = new NmeaAisOutputPositionClassA(this); addOutput(nmeaAisOutputPositionClassA, false); nmeaAisOutputPositionClassA.doInit(); @@ -149,15 +155,24 @@ public String addFoi(String mmsi) { return foiUID; } + /** + * Publishes a raw NMEA AIS sentence to the messages output. + * Called by {@link NmeaAisHandler} for every parsed message regardless of type. + */ + void publishRawMessage(String nmeaAisMsg) { + nmeaAidOutputRawMessages.setData(nmeaAisMsg); + } + /** * Forwards a decoded position report to the position output for publishing. * Called by {@link NmeaAisHandler} — keeps the handler decoupled from the Outputs package. */ - void publishPositionAReport(String nmeaAisMsg, PositionReportClassA report) { - nmeaAisOutputPositionClassA.setData(nmeaAisMsg, report); + void publishPositionAReport(PositionReportClassA report) { + nmeaAisOutputPositionClassA.setData(report); } - void publishPositionBReport(String nmeaAisMsg, PositionReportClassB report) { - nmeaAisOutputPositionClassB.setData(nmeaAisMsg, report); + + void publishPositionBReport(PositionReportClassB report) { + nmeaAisOutputPositionClassB.setData(report); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index 2c394e62f..a12452675 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -68,14 +68,16 @@ private void reassembleAndProcess(String sentence, int fragmentCount, int fragme } public void parsePayload(String payload) { + nmeaAisDriver.publishRawMessage(this.nmeaAisMsg); + int messageId = extractBits(payload, 0, 6); if (messageId == 1 || messageId == 2 || messageId == 3) { PositionReportClassA report = parsePositionAReport(payload); - nmeaAisDriver.publishPositionAReport(nmeaAisMsg, report); + nmeaAisDriver.publishPositionAReport(report); } - if (messageId == 18){ + if (messageId == 18) { PositionReportClassB report = parsePositionBReport(payload); - nmeaAisDriver.publishPositionBReport(nmeaAisMsg, report); + nmeaAisDriver.publishPositionBReport(report); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java new file mode 100644 index 000000000..cfa2b2e94 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java @@ -0,0 +1,134 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +import java.util.Objects; + +public class NmeaAidOutputRawMessages extends VarRateSensorOutput { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + + private final Object processingLock = new Object(); + + public NmeaAidOutputRawMessages(NmeaAisDriver nmeaAisDriver) { + super("nmeaAisOutputRawMessages", nmeaAisDriver, 1.); + } + + /** + * Initializes the data structure for the output. + * + * Flat index map: + * 0 = sampleTime 1 = sentenceType 2 = fragmentCount + * 3 = fragmentNumber 4 = sequentialId 5 = channel + * 6 = frequency 7 = rawPayload 8 = fillBits + * 9 = checkSum + */ + public void doInit() { + SWEHelper sweFactory = new SWEHelper(); + GeoPosHelper geoFac = new GeoPosHelper(); + + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name("nmeaAisOutputRawMessages") + .label("NMEA AIS Messages") + .description("Raw NMEA AIS message envelope for all received AIS sentences") + .definition(SWEHelper.getPropertyUri("NmeaAisOutputRawMessages")) + .addField("sampleTime", sweFactory.createTime() + .asSamplingTimeIsoUTC() + .label("Sample Time") + .description("Time of data collection") + .definition("SampleTime")) + .addField("sentenceType", sweFactory.createText() + .label("Sentence Type") + .description("AIS VHF Data-link Message") + .definition(SWEHelper.getPropertyUri("SentenceType"))) + .addField("fragmentCount", sweFactory.createQuantity() + .label("Fragment Count") + .description("Total number of fragments in this message") + .definition(SWEHelper.getPropertyUri("FragmentCount"))) + .addField("fragmentNumber", sweFactory.createQuantity() + .label("Fragment Number") + .description("Number of fragment per message") + .definition(SWEHelper.getPropertyUri("FragmentNumber"))) + .addField("sequentialId", sweFactory.createText() + .label("Sequential Id") + .description("Id to link multipart messages") + .definition(SWEHelper.getPropertyUri("SequentialId"))) + .addField("channel", sweFactory.createText() + .label("AIS Channel") + .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") + .definition(SWEHelper.getPropertyUri("Channel"))) + .addField("frequency", sweFactory.createQuantity() + .label("AIS Channel Frequency") + .description("AIS channel frequency in MHz") + .uom("MHz") + .definition(SWEHelper.getPropertyUri("Frequency"))) + .addField("rawPayload", sweFactory.createText() + .label("Raw Payload") + .description("Encoded AIS payload") + .definition(SWEHelper.getPropertyUri("RawPayload"))) + .addField("fillBits", sweFactory.createQuantity() + .label("Fill Bits") + .description("Padding bits added to final payload") + .definition(SWEHelper.getPropertyUri("FillBits"))) + .addField("checkSum", sweFactory.createText() + .label("Check Sum") + .description("NMEA checksum (hex)") + .definition(SWEHelper.getPropertyUri("CheckSum"))); + + aisReportRecord = recordBuilder.build(); + + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + public void setData(String nmeaAisMsg) { + synchronized (processingLock) { + String[] nmea = nmeaAisMsg.split(","); + String sentenceType = nmea[0]; + int fragmentCount = Integer.parseInt(nmea[1]); + int fragmentNumber = Integer.parseInt(nmea[2]); + String sequentialId = nmea[3]; + String channel = nmea[4]; + double channelFreq = Objects.equals(channel, "A") ? 161.975 : 162.025; + String rawPayload = nmea[5]; + String[] lastField = nmea[6].split("\\*"); + int fillBits = Integer.parseInt(lastField[0].trim()); + String checkSum = lastField[1].trim(); + + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(1, sentenceType); + dataBlock.setIntValue(2, fragmentCount); + dataBlock.setIntValue(3, fragmentNumber); + dataBlock.setStringValue(4, sequentialId); + dataBlock.setStringValue(5, channel); + dataBlock.setDoubleValue(6, channelFreq); + dataBlock.setStringValue(7, rawPayload); + dataBlock.setIntValue(8, fillBits); + dataBlock.setStringValue(9, checkSum); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAidOutputRawMessages.this, dataBlock)); + } + } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutput.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutput.java deleted file mode 100644 index 924ddbd17..000000000 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutput.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.sensorhub.impl.sensor.nmeaais.outputs; - -import net.opengis.swe.v20.DataBlock; -import net.opengis.swe.v20.DataComponent; -import net.opengis.swe.v20.DataEncoding; -import net.opengis.swe.v20.DataRecord; -import org.sensorhub.impl.sensor.VarRateSensorOutput; -import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; -import org.vast.swe.SWEBuilders; -import org.vast.swe.SWEHelper; -import org.vast.swe.helper.GeoPosHelper; - -import java.util.Objects; - -public class NmeaAisOutput extends VarRateSensorOutput { - private static final double INITIAL_SAMPLING_PERIOD = 1.0; - - protected DataRecord aisRecord; - protected DataEncoding dataEncoding; - protected int aisRecordSize; - - // AIS Message Variables - String sentenceType; - int fragmentTotal; - int fragmentNumber; - String sequentialId; - String channel; - double channelFreq; - String rawPayload; - int fillBits; - String checkSum; - - NmeaAisOutput(String outputName, NmeaAisDriver nmeaAisDriver) { - super(outputName, nmeaAisDriver, INITIAL_SAMPLING_PERIOD); - } - - /** - * Returns a {@link SWEBuilders.DataRecordBuilder} pre-populated with the record metadata - * and the {@code nmeaAisMsg} sub-record (flat indices 0–8). Subclasses can chain additional - * fields onto this builder before calling {@code .build()} once, avoiding a per-field - * {@code .build()} call on an already-constructed {@code DataRecord}. - */ - protected SWEBuilders.DataRecordBuilder createRecordBuilder( - String outputName, - String outputLabel, - String outputDescription, - String outputDefinition - ) { - SWEHelper sweFactory = new SWEHelper(); - return sweFactory.createRecord() - .name(outputName) - .label(outputLabel) - .description(outputDescription) - .definition(outputDefinition) - .addField("nmeaAisMsg", sweFactory.createRecord() - .label("NMEA AIS Message Info") - .description("Data Record for the general NMEA AIS message") - .definition(SWEHelper.getPropertyUri("NmeaAisMsg")) - .addField("sampleTime", sweFactory.createTime() - .asSamplingTimeIsoUTC() - .label("Sample Time") - .description("Time of data collection") - .definition("SampleTime")) - .addField("sentenceType", sweFactory.createText() - .label("Sentence Type") - .description("AIS VHF Data-link Message") - .definition(SWEHelper.getPropertyUri("SentenceType"))) - .addField("fragmentCount", sweFactory.createQuantity() - .label("Fragment Count") - .description("Total number of fragments in this message") - .definition(SWEHelper.getPropertyUri("FragmentCount"))) - .addField("fragmentNumber", sweFactory.createQuantity() - .label("Fragment Number") - .description("Number of fragment per message") - .definition(SWEHelper.getPropertyUri("FragmentNumber"))) - .addField("sequentialId", sweFactory.createText() - .label("Sequential Id") - .description("Id to link multipart messages") - .definition(SWEHelper.getPropertyUri("SequentialId"))) - .addField("channel", sweFactory.createText() - .label("AIS Channel") - .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") - .definition(SWEHelper.getPropertyUri("Channel"))) - .addField("frequency", sweFactory.createText() - .label("AIS Channel Frequency") - .description("AIS channel used: A (161.975 MHz) or B (162.025 MHz)") - .definition(SWEHelper.getPropertyUri("Frequency"))) - .addField("rawPayload", sweFactory.createText() - .label("Raw PayLoad") - .description("Encoded AIS payload") - .definition(SWEHelper.getPropertyUri("RawPayload"))) - .addField("fillBits", sweFactory.createQuantity() - .label("Fill Bits") - .description("Padding bits added to final payload") - .definition(SWEHelper.getPropertyUri("FillBits"))) - .addField("checkSum", sweFactory.createText() - .label("Check Sum") - .description("NMEA checksum (hex)") - .definition(SWEHelper.getPropertyUri("CheckSum"))) - ); - } - - /** - * Initializes the data structure for the output, defining the fields, their ordering, and data types. - */ - void doInit(String outputName, String outputLabel, String outputDescription, String outputDefinition) { - aisRecord = createRecordBuilder(outputName, outputLabel, outputDescription, outputDefinition).build(); - aisRecordSize = aisRecord.getNumFields()-1; // 9 leaf fields in nmeaAisMsg sub-record (flat indices 0–8) - dataEncoding = new GeoPosHelper().newTextEncoding(",", "\n"); - } - - - @Override - public DataComponent getRecordDescription() { - return aisRecord; - } - - @Override - public DataEncoding getRecommendedEncoding() { - return dataEncoding; - } - - - public void setAisMsgData(String nmeaAisMsg){ - // Step 1: Parse NMEA fields of NMEA AIS Sentence (ex. nmea = !AIVDM,1,1,,A,15Muan<000qm2=2CavBWSCL20@2?,0*6A) - String[] nmea = nmeaAisMsg.split(","); - sentenceType = nmea[0]; - fragmentTotal = Integer.parseInt(nmea[1]); - fragmentNumber = Integer.parseInt(nmea[2]); - sequentialId = nmea[3]; - channel = nmea[4]; - channelFreq = Objects.equals(channel, "A") ? 161.975 :162.025; - rawPayload = nmea[5]; - - // Split up last field to get checksum and fill bits - String[] lastField = nmea[6].split("\\*"); // last field of a nmea sentence is a [fill bits and checksum] - fillBits = Integer.parseInt(lastField[0].trim()); - checkSum = lastField[1].trim(); - } - - public void populateNmeaAisDataStructure(DataBlock dataBlock){ - // Populate Parent Class Packet Data - dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); - dataBlock.setStringValue(1, sentenceType); - dataBlock.setIntValue(2, fragmentTotal); - dataBlock.setIntValue(3, fragmentNumber); - dataBlock.setStringValue(4, sequentialId); - dataBlock.setStringValue(5, channel); - dataBlock.setDoubleValue(6, channelFreq); - dataBlock.setStringValue(7, rawPayload); - dataBlock.setIntValue(8, fillBits); - dataBlock.setStringValue(9, checkSum); - } - -} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java deleted file mode 100644 index 4d295d65a..000000000 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputInterface.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.sensorhub.impl.sensor.nmeaais.outputs; - -public interface NmeaAisOutputInterface { - void setData(String nmeaAisMsg, T report); -} - - diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java index f83399ef5..1ffee35cb 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java @@ -1,13 +1,21 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; +import org.vast.swe.SWEBuilders; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPositionClassA extends NmeaAisOutput implements NmeaAisOutputInterface { +public class NmeaAisOutputPositionClassA extends VarRateSensorOutput implements NmeaAisReportInterface { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + private static final String OUTPUT_NAME = "nmeaAisOutputPositionClassA"; private static final String OUTPUT_LABEL = "Position Report Class A"; private static final String OUTPUT_DESCRIPTION = "Class A AIS Position Report"; @@ -16,27 +24,31 @@ public class NmeaAisOutputPositionClassA extends NmeaAisOutput implements NmeaAi private final Object processingLock = new Object(); public NmeaAisOutputPositionClassA(NmeaAisDriver nmeaAisDriver) { - super(OUTPUT_NAME, nmeaAisDriver); + super(OUTPUT_NAME, nmeaAisDriver,1.0); } /** - * Initializes the data structure for the output, defining the fields, their ordering, and data types. + * Initializes the data structure for the output. * - * Flat index map (nmeaAisMsg sub-record occupies 0–8; position fields are top-level siblings): - * 9 = messageId 10 = repeat 11 = mmsi - * 12 = navStatus 13 = rot 14 = sog - * 15 = positionAccuracy - * 16 = latitude (lat component of createLocationVectorLatLon) - * 17 = longitude (lon component of createLocationVectorLatLon) - * 18 = cog 19 = heading 20 = timeStamp - * 21 = smi 22 = raim 23 = commState - * 24 = bits + * Flat index map: + * 0 = messageId 1 = repeat 2 = mmsi + * 3 = navStatus 4 = rot 5 = sog + * 6 = positionAccuracy + * 7 = latitude (lat component of location vector) + * 8 = longitude (lon component of location vector) + * 9 = cog 10 = heading 11 = timeStamp + * 12 = smi 13 = raim 14 = commState + * 15 = bits */ public void doInit() { GeoPosHelper geoFac = new GeoPosHelper(); SWEHelper sweFactory = new SWEHelper(); - aisRecord = createRecordBuilder(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION) + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(OUTPUT_NAME) + .label(OUTPUT_LABEL) + .description(OUTPUT_DESCRIPTION) + .definition(OUTPUT_DEFINITION) .addField("messageId", sweFactory.createQuantity() .label("Message Id") .description("Identifier for this message 1, 2 or 3") @@ -116,45 +128,34 @@ public void doInit() { .addField("bits", sweFactory.createQuantity() .label("Number of Bits") .description("Number of Bits") - .definition(SWEHelper.getPropertyUri("bits"))) - .build(); - aisRecordSize = 9; + .definition(SWEHelper.getPropertyUri("bits"))); + aisReportRecord = recordBuilder.build(); dataEncoding = geoFac.newTextEncoding(",", "\n"); } - /** - * Sets the data for the output and publishes it. - */ @Override - public void setData(String nmeaAisMsg, PositionReportClassA report) { + public void setData(PositionReportClassA report) { synchronized (processingLock) { - setAisMsgData(nmeaAisMsg); + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); - DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); + dataBlock.setIntValue(0, report.messageId); + dataBlock.setIntValue(1, report.repeat); + dataBlock.setStringValue(2, report.mmsi); + dataBlock.setIntValue(3, report.navStatus); + dataBlock.setIntValue(4, report.rot); + dataBlock.setDoubleValue(5, report.sog); + dataBlock.setIntValue(6, report.posAccuracy); + dataBlock.setDoubleValue(7, report.latitude); + dataBlock.setDoubleValue(8, report.longitude); + dataBlock.setDoubleValue(9, report.cog); + dataBlock.setIntValue(10, report.heading); + dataBlock.setIntValue(11, report.timeStamp); + dataBlock.setIntValue(12, report.smi); + dataBlock.setIntValue(13, report.raimFlag); + dataBlock.setIntValue(14, report.commState); + dataBlock.setIntValue(15, report.bits); - // NMEA envelope fields (flat indices 0–8) - populateNmeaAisDataStructure(dataBlock); - - // Decoded payload fields (flat indices 10–25) - dataBlock.setIntValue(aisRecordSize+1, report.messageId); - dataBlock.setIntValue(aisRecordSize + 2, report.repeat); - dataBlock.setStringValue(aisRecordSize + 3, report.mmsi); - dataBlock.setIntValue(aisRecordSize + 4, report.navStatus); - dataBlock.setIntValue(aisRecordSize + 5, report.rot); - dataBlock.setDoubleValue(aisRecordSize + 6, report.sog); - dataBlock.setIntValue(aisRecordSize + 7, report.posAccuracy); - dataBlock.setDoubleValue(aisRecordSize + 8, report.latitude); - dataBlock.setDoubleValue(aisRecordSize + 9, report.longitude); - dataBlock.setDoubleValue(aisRecordSize + 10, report.cog); - dataBlock.setIntValue(aisRecordSize + 11, report.heading); - dataBlock.setIntValue(aisRecordSize + 12, report.timeStamp); - dataBlock.setIntValue(aisRecordSize + 13, report.smi); - dataBlock.setIntValue(aisRecordSize + 14, report.raimFlag); - dataBlock.setIntValue(aisRecordSize + 15, report.commState); - dataBlock.setIntValue(aisRecordSize + 16, report.bits); - - // Register the vessel as a FOI keyed by MMSI String foiUID = parentSensor.addFoi(report.mmsi); latestRecord = dataBlock; @@ -163,4 +164,14 @@ public void setData(String nmeaAisMsg, PositionReportClassA report) { eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassA.this, foiUID, dataBlock)); } } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java index 4d419ed4b..72d4bc842 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java @@ -1,32 +1,50 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; +import org.vast.swe.SWEBuilders; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPositionClassB extends NmeaAisOutput implements NmeaAisOutputInterface { - private static final String OUTPUT_NAME = "nmeaAisOutputPositionClassB"; - private static final String OUTPUT_LABEL = "Position Report Class B"; - private static final String OUTPUT_DESCRIPTION = "Class B AIS Position Report"; - private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputPositionClassB"); +public class NmeaAisOutputPositionClassB extends VarRateSensorOutput implements NmeaAisReportInterface { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; private final Object processingLock = new Object(); public NmeaAisOutputPositionClassB(NmeaAisDriver nmeaAisDriver) { - super(OUTPUT_NAME, nmeaAisDriver); + super("nmeaAisOutputPositionClassB", nmeaAisDriver,1.0); } /** - * Initializes the data structure for the output, defining the fields, their ordering, and data types. + * Initializes the data structure for the output. + * + * Flat index map: + * 0 = messageId 1 = repeat 2 = mmsi + * 3 = sog 4 = positionAccuracy + * 5 = latitude (lat component of location vector) + * 6 = longitude (lon component of location vector) + * 7 = cog 8 = heading 9 = timeStamp + * 10 = unitFlag 11 = displayFlag 12 = dscFlag + * 13 = bandFlag 14 = message22Flag 15 = modeFlag + * 16 = raimFlag 17 = commStateFlag 18 = commState + * 19 = bits */ public void doInit() { GeoPosHelper geoFac = new GeoPosHelper(); SWEHelper sweFactory = new SWEHelper(); - aisRecord = createRecordBuilder(OUTPUT_NAME, OUTPUT_LABEL, OUTPUT_DESCRIPTION, OUTPUT_DEFINITION) + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name("nmeaAisOutputPositionClassB") + .label("Position Report Class B") + .description("Class B AIS Position Report") + .definition(SWEHelper.getPropertyUri("NmeaAisOutputPositionClassB")) .addField("messageId", sweFactory.createQuantity() .label("Message Id") .description("Identifier for this message 1, 2 or 3") @@ -112,49 +130,38 @@ public void doInit() { .addField("bits", sweFactory.createQuantity() .label("Number of Bits") .description("Number of Bits") - .definition(SWEHelper.getPropertyUri("bits"))) - .build(); - aisRecordSize = 9; + .definition(SWEHelper.getPropertyUri("bits"))); + aisReportRecord = recordBuilder.build(); dataEncoding = geoFac.newTextEncoding(",", "\n"); } - /** - * Sets the data for the output and publishes it. - */ @Override - public void setData(String nmeaAisMsg, PositionReportClassB report) { + public void setData(PositionReportClassB report) { synchronized (processingLock) { - setAisMsgData(nmeaAisMsg); - - DataBlock dataBlock = latestRecord == null ? aisRecord.createDataBlock() : latestRecord.renew(); + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); - // NMEA envelope fields (flat indices 0–8) - populateNmeaAisDataStructure(dataBlock); + dataBlock.setIntValue(0, report.messageId); + dataBlock.setIntValue(1, report.repeat); + dataBlock.setStringValue(2, report.mmsi); + dataBlock.setDoubleValue(3, report.sog); + dataBlock.setIntValue(4, report.posAccuracy); + dataBlock.setDoubleValue(5, report.latitude); + dataBlock.setDoubleValue(6, report.longitude); + dataBlock.setDoubleValue(7, report.cog); + dataBlock.setIntValue(8, report.heading); + dataBlock.setIntValue(9, report.timeStamp); + dataBlock.setIntValue(10, report.unitFlag); + dataBlock.setIntValue(11, report.displayFlag); + dataBlock.setIntValue(12, report.dscFlag); + dataBlock.setIntValue(13, report.bandFlag); + dataBlock.setIntValue(14, report.message22Flag); + dataBlock.setIntValue(15, report.modeFlag); + dataBlock.setIntValue(16, report.raimFlag); + dataBlock.setIntValue(17, report.commStateFlag); + dataBlock.setIntValue(18, report.commState); + dataBlock.setIntValue(19, report.bits); - // Decoded payload fields (flat indices 10–25) - dataBlock.setIntValue(aisRecordSize+1, report.messageId); - dataBlock.setIntValue(aisRecordSize + 2, report.repeat); - dataBlock.setStringValue(aisRecordSize + 3, report.mmsi); - dataBlock.setDoubleValue(aisRecordSize + 4, report.sog); - dataBlock.setIntValue(aisRecordSize + 5, report.posAccuracy); - dataBlock.setDoubleValue(aisRecordSize + 6, report.latitude); - dataBlock.setDoubleValue(aisRecordSize + 7, report.longitude); - dataBlock.setDoubleValue(aisRecordSize + 8, report.cog); - dataBlock.setIntValue(aisRecordSize + 9, report.heading); - dataBlock.setIntValue(aisRecordSize + 10, report.timeStamp); - dataBlock.setIntValue(aisRecordSize + 11, report.unitFlag); - dataBlock.setIntValue(aisRecordSize + 12, report.displayFlag); - dataBlock.setIntValue(aisRecordSize + 13, report.dscFlag); - dataBlock.setIntValue(aisRecordSize + 14, report.bandFlag); - dataBlock.setIntValue(aisRecordSize + 15, report.message22Flag); - dataBlock.setIntValue(aisRecordSize + 16, report.modeFlag); - dataBlock.setIntValue(aisRecordSize + 17, report.raimFlag); - dataBlock.setIntValue(aisRecordSize + 18, report.commStateFlag); - dataBlock.setIntValue(aisRecordSize + 19, report.commState); - dataBlock.setIntValue(aisRecordSize + 20, report.bits); - - // Register the vessel as a FOI keyed by MMSI String foiUID = parentSensor.addFoi(report.mmsi); latestRecord = dataBlock; @@ -163,4 +170,14 @@ public void setData(String nmeaAisMsg, PositionReportClassB report) { eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassB.this, foiUID, dataBlock)); } } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java new file mode 100644 index 000000000..79756af00 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java @@ -0,0 +1,5 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +public interface NmeaAisReportInterface { + void setData(T report); +} From 45161bf035458bc3b2d7b83a3ded668284abd1fe Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Wed, 20 May 2026 16:15:14 -0500 Subject: [PATCH 05/14] added Message Id Descriptions for each Report --- .../impl/sensor/nmeaais/NmeaAisDriver.java | 14 +++--- .../impl/sensor/nmeaais/NmeaAisHandler.java | 12 +++++- .../outputs/NmeaAisOutputPositionClassA.java | 35 ++++++++------- .../outputs/NmeaAisOutputPositionClassB.java | 43 +++++++++++-------- ...ges.java => NmeaAisOutputRawMessages.java} | 6 +-- .../reportschemas/MessageIdDescriptions.java | 38 ++++++++++++++++ .../reportschemas/PositionReportClassA.java | 1 + .../reportschemas/PositionReportClassB.java | 1 + 8 files changed, 105 insertions(+), 45 deletions(-) rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/{NmeaAidOutputRawMessages.java => NmeaAisOutputRawMessages.java} (97%) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index be54436df..621e919b3 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -13,7 +13,7 @@ import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; -import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAidOutputRawMessages; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputRawMessages; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassA; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassB; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; @@ -37,7 +37,7 @@ public class NmeaAisDriver extends AbstractSensorModule { // GLOBAL VARIABLES FOR SENSOR OPERATION NmeaAisHandler nmeaAisHandler; - NmeaAidOutputRawMessages nmeaAidOutputRawMessages; + NmeaAisOutputRawMessages nmeaAisOutputRawMessages; NmeaAisOutputPositionClassA nmeaAisOutputPositionClassA; NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; @@ -60,9 +60,9 @@ public void doInit() throws SensorHubException { generateXmlID(XML_PREFIX, config.serialNumber); // INITIALIZE OUTPUT - nmeaAidOutputRawMessages = new NmeaAidOutputRawMessages(this); - addOutput(nmeaAidOutputRawMessages, false); - nmeaAidOutputRawMessages.doInit(); + nmeaAisOutputRawMessages = new NmeaAisOutputRawMessages(this); + addOutput(nmeaAisOutputRawMessages, false); + nmeaAisOutputRawMessages.doInit(); nmeaAisOutputPositionClassA = new NmeaAisOutputPositionClassA(this); addOutput(nmeaAisOutputPositionClassA, false); @@ -160,14 +160,14 @@ public String addFoi(String mmsi) { * Called by {@link NmeaAisHandler} for every parsed message regardless of type. */ void publishRawMessage(String nmeaAisMsg) { - nmeaAidOutputRawMessages.setData(nmeaAisMsg); + nmeaAisOutputRawMessages.setData(nmeaAisMsg); } /** * Forwards a decoded position report to the position output for publishing. * Called by {@link NmeaAisHandler} — keeps the handler decoupled from the Outputs package. */ - void publishPositionAReport(PositionReportClassA report) { + void publishPositionAReport( PositionReportClassA report) { nmeaAisOutputPositionClassA.setData(report); } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index a12452675..d6ce745ce 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -1,5 +1,6 @@ package org.sensorhub.impl.sensor.nmeaais; +import org.sensorhub.impl.sensor.nmeaais.reportschemas.MessageIdDescriptions; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; @@ -9,6 +10,7 @@ public class NmeaAisHandler { String nmeaAisMsg; String rawPayload; + MessageIdDescriptions reportDescription = new MessageIdDescriptions(); private final NmeaAisDriver nmeaAisDriver; @@ -81,10 +83,17 @@ public void parsePayload(String payload) { } } + private String getReportDescription(int messageId) { + if (messageId < 1 || messageId > reportDescription.descriptions.length) { + return "Unknown message type " + messageId; + } + return reportDescription.descriptions[messageId - 1]; + } + public PositionReportClassA parsePositionAReport(String payload) { PositionReportClassA report = new PositionReportClassA(); - report.messageId = extractBits(payload, 0, 6); + report.description = getReportDescription(report.messageId); report.repeat = extractBits(payload, 6, 2); report.mmsi = String.format("%09d", extractBits(payload, 8, 30)); report.navStatus = extractBits(payload, 38, 4); @@ -108,6 +117,7 @@ public PositionReportClassA parsePositionAReport(String payload) { public PositionReportClassB parsePositionBReport(String payload) { PositionReportClassB report = new PositionReportClassB(); report.messageId = extractBits(payload, 0, 6); + report.description = getReportDescription(report.messageId); report.repeat = extractBits(payload, 6, 2); report.mmsi = String.format("%09d", extractBits(payload, 8, 30)); report.sog = extractBits(payload, 46, 10) / 10.0; // 38-46 are spare diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java index 1ffee35cb..8aff5c200 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java @@ -53,6 +53,10 @@ public void doInit() { .label("Message Id") .description("Identifier for this message 1, 2 or 3") .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) .addField("repeat", sweFactory.createQuantity() .label("Repeat Indicator") .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") @@ -140,21 +144,22 @@ public void setData(PositionReportClassA report) { DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); dataBlock.setIntValue(0, report.messageId); - dataBlock.setIntValue(1, report.repeat); - dataBlock.setStringValue(2, report.mmsi); - dataBlock.setIntValue(3, report.navStatus); - dataBlock.setIntValue(4, report.rot); - dataBlock.setDoubleValue(5, report.sog); - dataBlock.setIntValue(6, report.posAccuracy); - dataBlock.setDoubleValue(7, report.latitude); - dataBlock.setDoubleValue(8, report.longitude); - dataBlock.setDoubleValue(9, report.cog); - dataBlock.setIntValue(10, report.heading); - dataBlock.setIntValue(11, report.timeStamp); - dataBlock.setIntValue(12, report.smi); - dataBlock.setIntValue(13, report.raimFlag); - dataBlock.setIntValue(14, report.commState); - dataBlock.setIntValue(15, report.bits); + dataBlock.setStringValue(1, report.description); + dataBlock.setIntValue(2, report.repeat); + dataBlock.setStringValue(3, report.mmsi); + dataBlock.setIntValue(4, report.navStatus); + dataBlock.setIntValue(5, report.rot); + dataBlock.setDoubleValue(6, report.sog); + dataBlock.setIntValue(7, report.posAccuracy); + dataBlock.setDoubleValue(8, report.latitude); + dataBlock.setDoubleValue(9, report.longitude); + dataBlock.setDoubleValue(10, report.cog); + dataBlock.setIntValue(11, report.heading); + dataBlock.setIntValue(12, report.timeStamp); + dataBlock.setIntValue(13, report.smi); + dataBlock.setIntValue(14, report.raimFlag); + dataBlock.setIntValue(15, report.commState); + dataBlock.setIntValue(16, report.bits); String foiUID = parentSensor.addFoi(report.mmsi); diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java index 72d4bc842..5e94f4062 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java @@ -49,6 +49,10 @@ public void doInit() { .label("Message Id") .description("Identifier for this message 1, 2 or 3") .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) .addField("repeat", sweFactory.createQuantity() .label("Repeat Indicator") .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") @@ -142,25 +146,26 @@ public void setData(PositionReportClassB report) { DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); dataBlock.setIntValue(0, report.messageId); - dataBlock.setIntValue(1, report.repeat); - dataBlock.setStringValue(2, report.mmsi); - dataBlock.setDoubleValue(3, report.sog); - dataBlock.setIntValue(4, report.posAccuracy); - dataBlock.setDoubleValue(5, report.latitude); - dataBlock.setDoubleValue(6, report.longitude); - dataBlock.setDoubleValue(7, report.cog); - dataBlock.setIntValue(8, report.heading); - dataBlock.setIntValue(9, report.timeStamp); - dataBlock.setIntValue(10, report.unitFlag); - dataBlock.setIntValue(11, report.displayFlag); - dataBlock.setIntValue(12, report.dscFlag); - dataBlock.setIntValue(13, report.bandFlag); - dataBlock.setIntValue(14, report.message22Flag); - dataBlock.setIntValue(15, report.modeFlag); - dataBlock.setIntValue(16, report.raimFlag); - dataBlock.setIntValue(17, report.commStateFlag); - dataBlock.setIntValue(18, report.commState); - dataBlock.setIntValue(19, report.bits); + dataBlock.setStringValue(1, report.description); + dataBlock.setIntValue(2, report.repeat); + dataBlock.setStringValue(3, report.mmsi); + dataBlock.setDoubleValue(4, report.sog); + dataBlock.setIntValue(5, report.posAccuracy); + dataBlock.setDoubleValue(6, report.latitude); + dataBlock.setDoubleValue(7, report.longitude); + dataBlock.setDoubleValue(8, report.cog); + dataBlock.setIntValue(9, report.heading); + dataBlock.setIntValue(10, report.timeStamp); + dataBlock.setIntValue(11, report.unitFlag); + dataBlock.setIntValue(12, report.displayFlag); + dataBlock.setIntValue(13, report.dscFlag); + dataBlock.setIntValue(14, report.bandFlag); + dataBlock.setIntValue(15, report.message22Flag); + dataBlock.setIntValue(16, report.modeFlag); + dataBlock.setIntValue(17, report.raimFlag); + dataBlock.setIntValue(18, report.commStateFlag); + dataBlock.setIntValue(19, report.commState); + dataBlock.setIntValue(20, report.bits); String foiUID = parentSensor.addFoi(report.mmsi); diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputRawMessages.java similarity index 97% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputRawMessages.java index cfa2b2e94..f942a7d2e 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAidOutputRawMessages.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputRawMessages.java @@ -13,13 +13,13 @@ import java.util.Objects; -public class NmeaAidOutputRawMessages extends VarRateSensorOutput { +public class NmeaAisOutputRawMessages extends VarRateSensorOutput { private DataRecord aisReportRecord; private DataEncoding dataEncoding; private final Object processingLock = new Object(); - public NmeaAidOutputRawMessages(NmeaAisDriver nmeaAisDriver) { + public NmeaAisOutputRawMessages(NmeaAisDriver nmeaAisDriver) { super("nmeaAisOutputRawMessages", nmeaAisDriver, 1.); } @@ -118,7 +118,7 @@ public void setData(String nmeaAisMsg) { latestRecord = dataBlock; latestRecordTime = System.currentTimeMillis(); updateSamplingPeriod(latestRecordTime); - eventHandler.publish(new DataEvent(latestRecordTime, NmeaAidOutputRawMessages.this, dataBlock)); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputRawMessages.this, dataBlock)); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java new file mode 100644 index 000000000..80e08cb8f --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java @@ -0,0 +1,38 @@ +package org.sensorhub.impl.sensor.nmeaais.reportschemas; + +public class MessageIdDescriptions { + + public final String[] descriptions = { + "Scheduled position report; Class A shipborne mobile equipment", + "Assigned scheduled position report; Class A shipborne mobile equipment", + "Special position report, response to interrogation; Class A shipborne mobile equipment", + "Position, UTC, date and current slot number of base station", + "Scheduled static and voyage related vessel data report, Class A shipborne mobile equipment", + "Binary data for addressed communication", + "Acknowledgement of received addressed binary data", + "Binary data for broadcast communication", + "Position report for airborne stations involved in SAR operations only", + "Request UTC and date", + "Current UTC and date if available", + "Safety related data for addressed communication", + "Acknowledgement of received addressed safety related message", + "Safety related data for broadcast communication", + "Request for a specific message type can result in multiple responses from one or several stations", + "Assignment of a specific report behaviour by competent authority using a Base station", + "DGNSS corrections provided by a base station", + "Standard position report for Class B shipborne mobile equipment to be used instead of Messages 1, 2, 3", + "No longer required. Extended position report for Class B shipborne mobile equipment; contains additional static information", + "Reserve slots for Base station(s)", + "Position and status report for aids-to-navigation", + "Management of channels and transceiver modes by a Base station", + "Assignment of a specific report behaviour by competent authority using a Base station to a specific group of mobiles", + "Additional data assigned to an MMSI Part A: Name Part B: Static Data", + "Short unscheduled binary data transmission Broadcast or addressed", + "Scheduled binary data transmission Broadcast or addressed", + "Class A and Class B \"SO\" shipborne mobile equipment outside base station coverage" + }; + + + +} + diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java index 93c6e6836..32c17eeee 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java @@ -5,6 +5,7 @@ */ public class PositionReportClassA { public int messageId; // 1, 2, or 3 + public String description; public int repeat; // 0–3 public String mmsi; // 9-digit MMSI, zero-padded public int navStatus; // 0–15 diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java index d33c0c829..f4990b851 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java @@ -5,6 +5,7 @@ */ public class PositionReportClassB { public int messageId; + public String description; public int repeat; public String mmsi; public double sog; From 4dce3387c0dfcd9f7cadfd69da452385ee9b66bd Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Fri, 22 May 2026 11:28:27 -0500 Subject: [PATCH 06/14] updated ReadMe --- .../sensorhub-driver-nmeaais/README.md | 153 ++++-------------- 1 file changed, 32 insertions(+), 121 deletions(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/README.md b/sensors/positioning/sensorhub-driver-nmeaais/README.md index 7f3122295..b7feb712b 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/README.md +++ b/sensors/positioning/sensorhub-driver-nmeaais/README.md @@ -1,121 +1,32 @@ -# Kraken SDR -The KrakenSDR is a five-channel, phase-coherent software-defined radio (SDR) -built using RTL-SDR components, designed primarily for radio direction finding. -It utilizes five synchronized RTL-SDR receivers to achieve accurate -[beamforming](https://www.techtarget.com/searchnetworking/definition/beamforming) -and direction-of-arrival estimation. This allows users to locate the source of -radio signals, making it useful for applications like locating interference, -tracking assets, and even search and rescue efforts. - -# Hardware Setup -- KrakenSDR -- VK-162 USB GPS - - -# MANUAL SETUP with Fresh Image of RPi4 -## Expected Process: -For the following steps to work, it is assumed that you're starting with a fresh image of a raspberry pi 4. These -steps can be broken into (4) parts: -- Initial Pi setup -- Installation of [Kraken's Heimdall Firmware](https://github.com/krakenrf/heimdall_daq_fw) to handle synchronization of all 5 antennas and serve data -- Installation of [Kraken DoA DSP](https://github.com/krakenrf/krakensdr_doa) for DoA Digital Signal Processing and establishing DoA - -To begin these steps, logon to your Raspberry Pi and begin: - -## Setup Pi -1. SSH into your pi and update / install dependencies: - ``` - sudo apt update && sudo apt upgrade - ``` -2. Make sure java is installed on raspberry pi (latest 21 was used during testing) - ``` - sudo apt install openjdk-21-jdk - ``` -3. Install prerequisites to turn RTL2832U chip of the Kraken Device into SDR - ``` - sudo apt-get install libusb-dev libusb-1.0-0-dev build-essential cmake git - ``` - - Purge old RTL-SDR and librtlsdr install - ``` - sudo apt purge librtlsdr* - sudo rm -rvf /usr/lib/librtlsdr* /usr/include/rtl-sdr* /usr/local/lib/librtlsdr* /usr/local/include/rtl-sdr* - ``` -## Install HeIMDALL DAQ Firmware -Follow the [Manual Step by Step Install](https://github.com/krakenrf/heimdall_daq_fw?tab=readme-ov-file#manual-step-by-step-install) instructions -to install the HeIMDALL DAQ Firmware. Depending on your setup, take special car in following the instructions. For example, if you -are using a Raspberry Pi, make sure to follow the ARM instructions. - -## Install DoA DSP Software -Follow the [Manual Install](https://github.com/krakenrf/krakensdr_doa?tab=readme-ov-file#manual-installation-from-a-fresh-os) instructions -to install teh DoA Data Signal Processing Software. - -### Additional Info for GPS Integration -For ***Section 4***, I used a [VK-162](https://www.amazon.com/Navigation-External-Receiver-Raspberry-Geekstory/dp/B078Y52FGQ) GPS. -To set this up properly, i found the following [YouTube Tutorial](https://www.youtube.com/watch?v=A1zmhxcUOxw). However, you should be able -to type these commands in your Raspberry Pi's terminal: -1. Install GPSD -```commandline -sudo apt-get install gpsd gpsd-clients -pip3 install gpsd-py3 -``` -2. Stop current GPSD service, rebind to the correct serial, and then restart it. - -``` -sudo systemctl stop gpsd.socket -sudo systemctl disable gpsd.socket -``` -3. Update config StreamAdd -```commandline -sudo nano /lib/systemd/system/gpsd.socket -``` -Update ```ListenStream=127.0.0.1:2947``` to ```0.0.0.0:2947``` and save settings. - -4. Kill any ongoing process and rebind gpsd to serial port, most likely ttyAMC0 -``` -sudo killall gpsd -sudo gpsd /dev/ttyACM0 -F /var/run/gpsd.socket -``` -5. Feel free to test by typing ```gpsmon``` in your terminal - -### Additional Help for Remote Control -According to the `gui_run.sh` shell script located in the *krakensdr_doa* directory, the web-server is -created either using php (if remote control in not enabled) or miniserve (if remote control is enabled). - -To update this, navigate to `krakensdr_doa/_share/settings.json` and update the `en_remote_control` value to *true* - -Sometimes, miniserve must be set manaully. If you are not getting data from 8081, try manually setting up miniserve using the following command in the : -```commandline -miniserve -i 0.0.0.0 -p 8081 -P -u --on-duplicate-files overwrite -- _share -``` - -this allows the remote server to be updatable. If you continue to run into errors with the above command, make sure any process -using port 8081 has been terminated: -```java -// Check if 8081 is being used: -sudo lsof -i :8081 - -// Terminate existing process: -sudo kill -9 $(sudo lsof -t -i :8081) -``` - -### More Troubleshooting -If you find that the OSH node is still not updating the KrakenSDR's settings, check the permissions of the `_share` directory -and make sure you have write access: -```java -//Check Permissions: -ls -ld /home/user/krakensdr/krakensdr_doa/_share - -// Make sure the ownership is correct and change if needed: -sudo chown -R user:user /home/user/krakensdr/krakensdr_doa/_share - -// Update permissions: -chmod -R u+rw /home/user/krakensdr/krakensdr_doa/_share - -``` - - -## Helpful Resources -- [KrakenSDR Wiki](https://github.com/krakenrf/krakensdr_docs/wiki/) -- [Kraken Pi Image](https://github.com/krakenrf/krakensdr_doa/releases) -- [Kraken DOA video](https://www.youtube.com/watch?v=3ugAT5BLBc0) -- [DOA QUICKSTART](https://github.com/krakenrf/krakensdr_docs/wiki/02.-Direction-Finding-Quickstart-Guide) \ No newline at end of file +# NMEA AIS Message Decoder Driver +The purpose of this driver is to decode standard NMEA AIS Messages in the standard format: + +```!AIVDM,1,1,,A,H52lOI@<4pU>0lTpu:000000000,2*55 ``` + +NMEA Message structure looks like this: + +Field | Value | Meaning +-- | -- | -- +Sentence type | !AIVDM | AIS VHF Data-link Message +Fragment count | 1 | Total number of fragments in this message +Fragment number | 1 | This is fragment 1 +Sequential ID | `` (blank) | Used to link multipart messages +Channel | A | AIS channel A (161.975 MHz) or B (162.025 MHz) +Payload | H52lOI@<4pU>0lTpu:000000000 | Encoded AIS payload +Fill bits | 2 | Padding bits added to final payload +Checksum | 55 | NMEA checksum + +This driver outputs the AIS Messages into Class A Reports, Class B reports, ATON Reports, AIS Addressed Binary Messages, and AIS Binary Broadcast Messsages as identified +https://www.navcen.uscg.gov/ais-messages. + +Setup +This driver was tested using the following pre-requestites: +## Hardware +- [ShipXplorer AIS Dongle](https://www.shipxplorer.com/ais-dongle) +- [ShipXplorer AIS Antenna](https://www.shipxplorer.com/ais-antenna) + +## Software +- Install [AIS Catcher](https://jvde-github.github.io/AIS-catcher-docs/) + +Local Setup (AIS-Catcher is running on local machine) +- \ No newline at end of file From cba811c023044782686d03d13fd199ecb26fce19 Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Fri, 22 May 2026 12:14:12 -0500 Subject: [PATCH 07/14] Updated to use Generic CommProvider --- .../sensorhub-driver-nmeaais/build.gradle | 5 +- .../impl/sensor/nmeaais/NmeaAisConfig.java | 7 +- .../impl/sensor/nmeaais/NmeaAisDriver.java | 86 ++++++++++--------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/build.gradle b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle index ceb2c7dbb..edaf9e76d 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/build.gradle +++ b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle @@ -4,11 +4,8 @@ version = '1.0.0' dependencies { implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion -// implementation project(':sensorhub-comm-rxtx') testImplementation('junit:junit:4.13.1') -// embeddedImpl 'com.neuronrobotics:nrjavaserial:5.2.1' -// embeddedImpl 'commons-net:commons-net:3.9.0' -// embeddedImpl 'com.google.code.gson:gson:2.10.1' + } // exclude tests requiring connection to the sensor diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java index e0effd342..97318706a 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java @@ -11,6 +11,7 @@ ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.nmeaais; +import org.sensorhub.api.comm.CommProviderConfig; import org.sensorhub.api.config.DisplayInfo; import org.sensorhub.api.sensor.SensorConfig; @@ -26,8 +27,6 @@ public class NmeaAisConfig extends SensorConfig { @DisplayInfo(desc = "Serial number or unique identifier") public String serialNumber = "myShipXplorer"; - @DisplayInfo.Required - @DisplayInfo(label = "UDP Port", desc = "Local port to receive AIS NMEA sentences from AIS-Catcher (default: 10110)") - public int udpPort = 10110; - + @DisplayInfo(desc = "Communication settings for receiving AIS NMEA sentences (e.g. UDP, TCP, serial)") + public CommProviderConfig commSettings; } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index 621e919b3..9b7cbb1bc 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -11,6 +11,7 @@ ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.nmeaais; +import org.sensorhub.api.comm.ICommProvider; import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputRawMessages; @@ -19,11 +20,9 @@ import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; import org.vast.ogc.om.MovingFeature; + import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; +import java.io.InputStream; /** * Driver implementation for the sensor. @@ -41,13 +40,7 @@ public class NmeaAisDriver extends AbstractSensorModule { NmeaAisOutputPositionClassA nmeaAisOutputPositionClassA; NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; - static final int MAX_PACKET_SIZE = 4096; - -// static final String test1 = "!AIVDM,1,1,,A,15NfK=PP00qm21jCarCv4?wf20S4,0*13"; -// static final String test2 = "!AIVDM,1,1,,B,35NNm0dP@Vqm19HCbhs commProvider; volatile boolean started; // INITIALIZE @@ -76,34 +69,51 @@ public void doInit() throws SensorHubException { nmeaAisHandler = new NmeaAisHandler(this); } - @Override public void doStart() throws SensorHubException { super.doStart(); + + // Init comm provider — recreated here so it picks up any config changes made via the UI + if (commProvider == null) { + if (config.commSettings == null) + throw new SensorHubException("No communication settings specified"); + try { + var moduleReg = getParentHub().getModuleRegistry(); + commProvider = (ICommProvider) moduleReg.loadSubModule(config.commSettings, true); + commProvider.start(); + } catch (Exception e) { + commProvider = null; + throw e; + } + } + + // Get input stream — read byte-by-byte to avoid BufferedReader's large internal buffer, + // which would delay output until ~100 UDP packets accumulate before returning any lines. + final InputStream inputStream; try { - // SO_REUSEADDR allows binding to a port that another process (e.g. AIS-Catcher) is also sending to, - // preventing "Address already in use" when multiple consumers subscribe to the same UDP feed. - socket = new DatagramSocket(null); - socket.setReuseAddress(true); - socket.bind(new InetSocketAddress(config.udpPort)); - getLogger().info("Listening for AIS data on UDP port {}", config.udpPort); + inputStream = commProvider.getInputStream(); + getLogger().info("Connected to AIS data stream"); } catch (IOException e) { - throw new SensorHubException("Cannot bind UDP socket on port " + config.udpPort, e); + throw new SensorHubException("Error opening AIS input stream", e); } Thread t = new Thread(() -> { - byte[] buf = new byte[MAX_PACKET_SIZE]; - DatagramPacket packet = new DatagramPacket(buf, buf.length); - while (started) { - try { - socket.receive(packet); - String aisNmeaMsg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim(); - nmeaAisHandler.handleNmeaAisMessage(aisNmeaMsg); - - } catch (IOException e) { - if (started) - getLogger().error("Error reading AIS UDP packet", e); + StringBuilder sb = new StringBuilder(); + try { + int b; + while (started && (b = inputStream.read()) != -1) { + if (b == '\n') { + String line = sb.toString().trim(); + sb.setLength(0); + if (!line.isEmpty()) + nmeaAisHandler.handleNmeaAisMessage(line); + } else if (b != '\r') { + sb.append((char) b); + } } + } catch (IOException e) { + if (started) + getLogger().error("Error reading AIS data stream", e); } }); @@ -117,9 +127,9 @@ public void doStart() throws SensorHubException { public void doStop() throws SensorHubException { started = false; - if (socket != null) { - socket.close(); - socket = null; + if (commProvider != null) { + commProvider.stop(); + commProvider = null; } super.doStop(); @@ -127,14 +137,9 @@ public void doStop() throws SensorHubException { @Override public boolean isConnected() { - return socket != null && !socket.isClosed(); + return commProvider != null; } - /** - * Registers an AIS vessel as a Feature of Interest (FOI) keyed by its MMSI. - * Subsequent calls with the same MMSI are no-ops; the existing FOI UID is returned. - */ - /** * Registers an AIS vessel as a Feature of Interest (FOI) keyed by its MMSI. * Subsequent calls with the same MMSI are no-ops; the existing FOI UID is returned. @@ -167,12 +172,11 @@ void publishRawMessage(String nmeaAisMsg) { * Forwards a decoded position report to the position output for publishing. * Called by {@link NmeaAisHandler} — keeps the handler decoupled from the Outputs package. */ - void publishPositionAReport( PositionReportClassA report) { + void publishPositionAReport(PositionReportClassA report) { nmeaAisOutputPositionClassA.setData(report); } void publishPositionBReport(PositionReportClassB report) { nmeaAisOutputPositionClassB.setData(report); } - } From 28e1111b98ffbdd2d14025ca9a910486587e2621 Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Fri, 22 May 2026 15:16:37 -0500 Subject: [PATCH 08/14] refractored code to use AisLib Java Library --- .../sensorhub-driver-nmeaais/build.gradle | 2 + .../impl/sensor/nmeaais/NmeaAisDriver.java | 74 ++++-- .../impl/sensor/nmeaais/NmeaAisHandler.java | 213 ++---------------- .../MessageIdDescriptions.java | 2 +- .../outputs/NmeaAisOutputPositionClassA.java | 49 ++-- .../outputs/NmeaAisOutputPositionClassB.java | 57 +++-- .../outputs/NmeaAisReportInterface.java | 4 +- .../reportschemas/PositionReportClassA.java | 25 -- .../reportschemas/PositionReportClassB.java | 29 --- 9 files changed, 128 insertions(+), 327 deletions(-) rename sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/{reportschemas => helpers}/MessageIdDescriptions.java (97%) delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java delete mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/build.gradle b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle index edaf9e76d..570d7fb65 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/build.gradle +++ b/sensors/positioning/sensorhub-driver-nmeaais/build.gradle @@ -5,6 +5,8 @@ version = '1.0.0' dependencies { implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion testImplementation('junit:junit:4.13.1') + implementation 'dk.dma.ais.lib:ais-lib-messages:2.8.5' + implementation 'dk.dma.ais.lib:ais-lib-communication:2.8.5' } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index 9b7cbb1bc..ca9684bd0 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -11,18 +11,22 @@ ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.nmeaais; +import dk.dma.ais.message.AisMessage18; +import dk.dma.ais.message.AisPositionMessage; +import dk.dma.ais.reader.AisReader; +import dk.dma.ais.reader.AisReaders; import org.sensorhub.api.comm.ICommProvider; import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputRawMessages; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassA; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassB; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; import org.vast.ogc.om.MovingFeature; import java.io.IOException; import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; /** * Driver implementation for the sensor. @@ -41,6 +45,7 @@ public class NmeaAisDriver extends AbstractSensorModule { NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; ICommProvider commProvider; + AisReader aisReader; volatile boolean started; // INITIALIZE @@ -65,7 +70,7 @@ public void doInit() throws SensorHubException { addOutput(nmeaAisOutputPositionClassB, false); nmeaAisOutputPositionClassB.doInit(); - // Initialize Parser + // Initialize Handler nmeaAisHandler = new NmeaAisHandler(this); } @@ -87,8 +92,6 @@ public void doStart() throws SensorHubException { } } - // Get input stream — read byte-by-byte to avoid BufferedReader's large internal buffer, - // which would delay output until ~100 UDP packets accumulate before returning any lines. final InputStream inputStream; try { inputStream = commProvider.getInputStream(); @@ -97,7 +100,25 @@ public void doStart() throws SensorHubException { throw new SensorHubException("Error opening AIS input stream", e); } - Thread t = new Thread(() -> { + // DatagramInputStream only overrides the single-byte read(), so BufferedReader + // (used internally by AisReader) would buffer ~8192 bytes before returning any + // lines (~100 UDP packets). Work around this by reading byte-by-byte and piping + // complete lines to AisReader so it still handles fragment reassembly. + PipedInputStream pipedIn; + final PipedOutputStream pipedOut; + try { + pipedIn = new PipedInputStream(65536); + pipedOut = new PipedOutputStream(pipedIn); + } catch (IOException e) { + throw new SensorHubException("Error creating AIS pipe", e); + } + + aisReader = AisReaders.createReaderFromInputStream(pipedIn); + aisReader.registerHandler(aisMessage -> + nmeaAisHandler.handleAisMessage(aisMessage) + ); + + Thread readerThread = new Thread(() -> { StringBuilder sb = new StringBuilder(); try { int b; @@ -105,8 +126,12 @@ public void doStart() throws SensorHubException { if (b == '\n') { String line = sb.toString().trim(); sb.setLength(0); - if (!line.isEmpty()) - nmeaAisHandler.handleNmeaAisMessage(line); + if (!line.isEmpty()) { + publishRawMessage(line); + byte[] bytes = (line + "\n").getBytes(); + pipedOut.write(bytes); + pipedOut.flush(); + } } else if (b != '\r') { sb.append((char) b); } @@ -114,19 +139,27 @@ public void doStart() throws SensorHubException { } catch (IOException e) { if (started) getLogger().error("Error reading AIS data stream", e); + } finally { + try { pipedOut.close(); } catch (IOException ignored) {} } }); started = true; - t.setName("ais-reader"); - t.setDaemon(true); - t.start(); + readerThread.setName("ais-reader"); + readerThread.setDaemon(true); + readerThread.start(); + aisReader.start(); } @Override public void doStop() throws SensorHubException { started = false; + if (aisReader != null) { + aisReader.stop(); + aisReader = null; + } + if (commProvider != null) { commProvider.stop(); commProvider = null; @@ -144,12 +177,12 @@ public boolean isConnected() { * Registers an AIS vessel as a Feature of Interest (FOI) keyed by its MMSI. * Subsequent calls with the same MMSI are no-ops; the existing FOI UID is returned. */ - public String addFoi(String mmsi) { + public String addFoi(int mmsi) { String foiUID = UID_PREFIX + "foi:" + mmsi; if (!foiMap.containsKey(foiUID)) { MovingFeature foi = new MovingFeature(); - foi.setId(mmsi); + foi.setId(Integer.toString(mmsi)); foi.setUniqueIdentifier(foiUID); foi.setName("Vessel " + mmsi); foi.setDescription("AIS vessel with MMSI " + mmsi); @@ -162,21 +195,22 @@ public String addFoi(String mmsi) { /** * Publishes a raw NMEA AIS sentence to the messages output. - * Called by {@link NmeaAisHandler} for every parsed message regardless of type. */ void publishRawMessage(String nmeaAisMsg) { nmeaAisOutputRawMessages.setData(nmeaAisMsg); } /** - * Forwards a decoded position report to the position output for publishing. - * Called by {@link NmeaAisHandler} — keeps the handler decoupled from the Outputs package. + * Forwards a decoded Class A position report to the position output for publishing. */ - void publishPositionAReport(PositionReportClassA report) { - nmeaAisOutputPositionClassA.setData(report); + void publishPositionAReport(AisPositionMessage report, String description) { + nmeaAisOutputPositionClassA.setData(report, description); } - void publishPositionBReport(PositionReportClassB report) { - nmeaAisOutputPositionClassB.setData(report); + /** + * Forwards a decoded Class B position report to the position output for publishing. + */ + void publishPositionBReport(AisMessage18 report, String description) { + nmeaAisOutputPositionClassB.setData(report, description); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index d6ce745ce..2bcb47d69 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -1,211 +1,38 @@ package org.sensorhub.impl.sensor.nmeaais; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.MessageIdDescriptions; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; - -import java.util.HashMap; -import java.util.Map; +import dk.dma.ais.message.AisMessage; +import dk.dma.ais.message.AisMessage18; +import dk.dma.ais.message.AisPositionMessage; +import org.sensorhub.impl.sensor.nmeaais.helpers.MessageIdDescriptions; public class NmeaAisHandler { - String nmeaAisMsg; - String rawPayload; - MessageIdDescriptions reportDescription = new MessageIdDescriptions(); - private final NmeaAisDriver nmeaAisDriver; - - /** - * Buffer for assembling multi-sentence AIS messages. - * Key: sequential ID (field 3 of the NMEA sentence, "1"–"9"). - */ - private final Map fragmentBuffers = new HashMap<>(); + private final MessageIdDescriptions reportDescription = new MessageIdDescriptions(); public NmeaAisHandler(NmeaAisDriver driver) { this.nmeaAisDriver = driver; } - public void handleNmeaAisMessage(String sentence) { - String[] nmea = sentence.split(","); - int fragmentCount = Integer.parseInt(nmea[1]); - int fragmentNumber = Integer.parseInt(nmea[2]); - String sequentialId = nmea[3]; - String payload = nmea[5]; - - // Check Fragment Count to see if there are multiple messages that will need to be combined - if (fragmentCount == 1) { - // Single-sentence message — process immediately - this.nmeaAisMsg = sentence; - this.rawPayload = payload; - parsePayload(payload); - } else { - // Multi-sentence message — buffer until all fragments arrive - reassembleAndProcess(sentence, fragmentCount, fragmentNumber, sequentialId, payload); - } - } - - /** - * Buffers an individual fragment. When all fragments for a sequential ID have - * arrived the payloads are concatenated in order and passed to {@link #parsePayload}. - * The NMEA envelope from the first fragment is used for output metadata. - */ - private void reassembleAndProcess(String sentence, int fragmentCount, int fragmentNumber, - String sequentialId, String payload) { - FragmentBuffer buf = fragmentBuffers.get(sequentialId); - - // If no buffer exists yet, or a stale buffer with a different fragment count is - // sitting there (e.g. a previous message was never completed), start fresh. - if (buf == null || buf.fragmentCount != fragmentCount) { - buf = new FragmentBuffer(fragmentCount, sentence); - fragmentBuffers.put(sequentialId, buf); - } - - buf.addFragment(fragmentNumber, payload); - - if (buf.isComplete()) { - fragmentBuffers.remove(sequentialId); - this.nmeaAisMsg = buf.firstSentence; - this.rawPayload = buf.getCombinedPayload(); - parsePayload(this.rawPayload); - } - } - - public void parsePayload(String payload) { - nmeaAisDriver.publishRawMessage(this.nmeaAisMsg); - - int messageId = extractBits(payload, 0, 6); - if (messageId == 1 || messageId == 2 || messageId == 3) { - PositionReportClassA report = parsePositionAReport(payload); - nmeaAisDriver.publishPositionAReport(report); - } - if (messageId == 18) { - PositionReportClassB report = parsePositionBReport(payload); - nmeaAisDriver.publishPositionBReport(report); + public void handleAisMessage(AisMessage aisMessage) { + int messageId = aisMessage.getMsgId(); + String description = getReportDescription(messageId); + switch (messageId) { + case 1, 2, 3: + nmeaAisDriver.publishPositionAReport((AisPositionMessage) aisMessage, description); + break; + case 4, 11: // AIS Base Station Reports + break; + case 5: // Static/Voyage Data + break; + case 18: // Class B Position Reports + nmeaAisDriver.publishPositionBReport((AisMessage18) aisMessage, description); + break; } } private String getReportDescription(int messageId) { - if (messageId < 1 || messageId > reportDescription.descriptions.length) { + if (messageId < 1 || messageId > reportDescription.descriptions.length) return "Unknown message type " + messageId; - } return reportDescription.descriptions[messageId - 1]; } - - public PositionReportClassA parsePositionAReport(String payload) { - PositionReportClassA report = new PositionReportClassA(); - report.messageId = extractBits(payload, 0, 6); - report.description = getReportDescription(report.messageId); - report.repeat = extractBits(payload, 6, 2); - report.mmsi = String.format("%09d", extractBits(payload, 8, 30)); - report.navStatus = extractBits(payload, 38, 4); - report.rot = signExtend(extractBits(payload, 42, 8), 8); - report.sog = extractBits(payload, 50, 10) / 10.0; - report.posAccuracy = extractBits(payload, 60, 1); - report.longitude = signExtend(extractBits(payload, 61, 28), 28) / 600000.0; - report.latitude = signExtend(extractBits(payload, 89, 27), 27) / 600000.0; - report.cog = extractBits(payload, 116, 12) / 10.0; - report.heading = extractBits(payload, 128, 9); - report.timeStamp = extractBits(payload, 137, 6); - report.smi = extractBits(payload, 143, 2); - report.spare = extractBits(payload, 145, 3); - report.raimFlag = extractBits(payload, 148, 1); - report.commState = extractBits(payload, 149, 19); - report.bits = payload.length() * 6; - - return report; - } - - public PositionReportClassB parsePositionBReport(String payload) { - PositionReportClassB report = new PositionReportClassB(); - report.messageId = extractBits(payload, 0, 6); - report.description = getReportDescription(report.messageId); - report.repeat = extractBits(payload, 6, 2); - report.mmsi = String.format("%09d", extractBits(payload, 8, 30)); - report.sog = extractBits(payload, 46, 10) / 10.0; // 38-46 are spare - report.posAccuracy = extractBits(payload, 56, 1); - report.longitude = signExtend(extractBits(payload, 57, 28), 28) / 600000.0; - report.latitude = signExtend(extractBits(payload, 85, 27), 27) / 600000.0; - report.cog = extractBits(payload, 112, 12) / 10.0; - report.heading = extractBits(payload, 124, 9); - report.timeStamp = extractBits(payload, 133, 6); - report.unitFlag = extractBits(payload, 141, 1); //139-140 are Spare - report.displayFlag = extractBits(payload, 142, 1); - report.dscFlag = extractBits(payload, 143, 1); - report.bandFlag = extractBits(payload, 144, 1); - report.message22Flag = extractBits(payload, 145, 1); - report.modeFlag = extractBits(payload, 146, 1); - report.raimFlag = extractBits(payload, 147, 1); - report.commStateFlag = extractBits(payload, 148, 1); - report.commState = extractBits(payload, 149, 19); - report.bits = payload.length() * 6; - - return report; - } - - /** - * Extracts {@code numBits} bits starting at {@code startBit} from an AIS - * ASCII-armored payload string (6 bits per character, MSB first). - */ - private int extractBits(String payload, int startBit, int numBits) { - int result = 0; - for (int i = 0; i < numBits; i++) { - int bitPos = startBit + i; - int charIndex = bitPos / 6; - int bitInChar = 5 - (bitPos % 6); // MSB first within each 6-bit group - int charVal = payload.charAt(charIndex) - 48; - if (charVal > 40) charVal -= 8; - int bit = (charVal >> bitInChar) & 1; - result = (result << 1) | bit; - } - return result; - } - - /** - * Sign-extends a value that was extracted as an unsigned int from {@code numBits} bits - * into a signed Java int using two's-complement interpretation. - */ - private int signExtend(int value, int numBits) { - if ((value & (1 << (numBits - 1))) != 0) { - value -= (1 << numBits); - } - return value; - } - - // ------------------------------------------------------------------------- - // Fragment reassembly support - // ------------------------------------------------------------------------- - - private static class FragmentBuffer { - final int fragmentCount; - /** NMEA sentence from the first fragment — used for the output's envelope fields. */ - final String firstSentence; - private final String[] payloads; - private int receivedCount; - - FragmentBuffer(int fragmentCount, String firstSentence) { - this.fragmentCount = fragmentCount; - this.firstSentence = firstSentence; - this.payloads = new String[fragmentCount]; - } - - void addFragment(int fragmentNumber, String payload) { - int index = fragmentNumber - 1; // fragment numbers are 1-based - if (payloads[index] == null) { - payloads[index] = payload; - receivedCount++; - } - } - - boolean isComplete() { - return receivedCount == fragmentCount; - } - - /** Concatenates all fragment payloads in order into a single bit string. */ - String getCombinedPayload() { - StringBuilder sb = new StringBuilder(); - for (String p : payloads) { - sb.append(p); - } - return sb.toString(); - } - } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/helpers/MessageIdDescriptions.java similarity index 97% rename from sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java rename to sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/helpers/MessageIdDescriptions.java index 80e08cb8f..9a0e01941 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/MessageIdDescriptions.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/helpers/MessageIdDescriptions.java @@ -1,4 +1,4 @@ -package org.sensorhub.impl.sensor.nmeaais.reportschemas; +package org.sensorhub.impl.sensor.nmeaais.helpers; public class MessageIdDescriptions { diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java index 8aff5c200..609600ab8 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassA.java @@ -1,5 +1,6 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; +import dk.dma.ais.message.*; import net.opengis.swe.v20.DataBlock; import net.opengis.swe.v20.DataComponent; import net.opengis.swe.v20.DataEncoding; @@ -7,12 +8,11 @@ import org.sensorhub.api.data.DataEvent; import org.sensorhub.impl.sensor.VarRateSensorOutput; import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassA; import org.vast.swe.SWEBuilders; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPositionClassA extends VarRateSensorOutput implements NmeaAisReportInterface { +public class NmeaAisOutputPositionClassA extends VarRateSensorOutput implements NmeaAisReportInterface { private DataRecord aisReportRecord; private DataEncoding dataEncoding; @@ -61,7 +61,7 @@ public void doInit() { .label("Repeat Indicator") .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") .definition(SWEHelper.getPropertyUri("repeat"))) - .addField("mmsi", sweFactory.createText() + .addField("mmsi", sweFactory.createQuantity() .label("MMSI Number") .description("MMSI Number") .definition(SWEHelper.getPropertyUri("Mmsi"))) @@ -128,40 +128,35 @@ public void doInit() { .addField("commState", sweFactory.createQuantity() .label("Communication State") .description("visit https://www.navcen.uscg.gov/ais-class-a-reports#CommState") - .definition(SWEHelper.getPropertyUri("CommState"))) - .addField("bits", sweFactory.createQuantity() - .label("Number of Bits") - .description("Number of Bits") - .definition(SWEHelper.getPropertyUri("bits"))); + .definition(SWEHelper.getPropertyUri("CommState"))); aisReportRecord = recordBuilder.build(); dataEncoding = geoFac.newTextEncoding(",", "\n"); } @Override - public void setData(PositionReportClassA report) { + public void setData(AisPositionMessage report, String description) { synchronized (processingLock) { DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); - dataBlock.setIntValue(0, report.messageId); - dataBlock.setStringValue(1, report.description); - dataBlock.setIntValue(2, report.repeat); - dataBlock.setStringValue(3, report.mmsi); - dataBlock.setIntValue(4, report.navStatus); - dataBlock.setIntValue(5, report.rot); - dataBlock.setDoubleValue(6, report.sog); - dataBlock.setIntValue(7, report.posAccuracy); - dataBlock.setDoubleValue(8, report.latitude); - dataBlock.setDoubleValue(9, report.longitude); - dataBlock.setDoubleValue(10, report.cog); - dataBlock.setIntValue(11, report.heading); - dataBlock.setIntValue(12, report.timeStamp); - dataBlock.setIntValue(13, report.smi); - dataBlock.setIntValue(14, report.raimFlag); - dataBlock.setIntValue(15, report.commState); - dataBlock.setIntValue(16, report.bits); + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setIntValue(4, report.getNavStatus()); + dataBlock.setIntValue(5, report.getRot()); + dataBlock.setDoubleValue(6, report.getSog() / 10.0); + dataBlock.setIntValue(7, report.getPosAcc()); + dataBlock.setDoubleValue(8, report.getPos().getLatitudeDouble()); + dataBlock.setDoubleValue(9, report.getPos().getLongitudeDouble()); + dataBlock.setDoubleValue(10, report.getCog() / 10.0); + dataBlock.setIntValue(11, report.getTrueHeading()); + dataBlock.setIntValue(12, report.getUtcSec()); + dataBlock.setIntValue(13, report.getSpecialManIndicator()); + dataBlock.setIntValue(14, report.getRaim()); + dataBlock.setIntValue(15, report.getSyncState()); - String foiUID = parentSensor.addFoi(report.mmsi); + String foiUID = parentSensor.addFoi(report.getUserId()); latestRecord = dataBlock; latestRecordTime = System.currentTimeMillis(); diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java index 5e94f4062..7580fc5f5 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java @@ -1,5 +1,6 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; +import dk.dma.ais.message.AisMessage18; import net.opengis.swe.v20.DataBlock; import net.opengis.swe.v20.DataComponent; import net.opengis.swe.v20.DataEncoding; @@ -7,12 +8,11 @@ import org.sensorhub.api.data.DataEvent; import org.sensorhub.impl.sensor.VarRateSensorOutput; import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; -import org.sensorhub.impl.sensor.nmeaais.reportschemas.PositionReportClassB; import org.vast.swe.SWEBuilders; import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPositionClassB extends VarRateSensorOutput implements NmeaAisReportInterface { +public class NmeaAisOutputPositionClassB extends VarRateSensorOutput implements NmeaAisReportInterface { private DataRecord aisReportRecord; private DataEncoding dataEncoding; @@ -57,7 +57,7 @@ public void doInit() { .label("Repeat Indicator") .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") .definition(SWEHelper.getPropertyUri("repeat"))) - .addField("mmsi", sweFactory.createText() + .addField("mmsi", sweFactory.createQuantity() .label("MMSI Number") .description("MMSI Number") .definition(SWEHelper.getPropertyUri("Mmsi"))) @@ -130,44 +130,39 @@ public void doInit() { .addField("commState", sweFactory.createQuantity() .label("Communication State") .description("OTDMA communication state. Because Class B \"CS\" does not use any Communication State information, this field shall be filled with the following value: 1100000000000000110.") - .definition(SWEHelper.getPropertyUri("CommState"))) - .addField("bits", sweFactory.createQuantity() - .label("Number of Bits") - .description("Number of Bits") - .definition(SWEHelper.getPropertyUri("bits"))); + .definition(SWEHelper.getPropertyUri("CommState"))); aisReportRecord = recordBuilder.build(); dataEncoding = geoFac.newTextEncoding(",", "\n"); } @Override - public void setData(PositionReportClassB report) { + public void setData(AisMessage18 report, String description) { synchronized (processingLock) { DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); - dataBlock.setIntValue(0, report.messageId); - dataBlock.setStringValue(1, report.description); - dataBlock.setIntValue(2, report.repeat); - dataBlock.setStringValue(3, report.mmsi); - dataBlock.setDoubleValue(4, report.sog); - dataBlock.setIntValue(5, report.posAccuracy); - dataBlock.setDoubleValue(6, report.latitude); - dataBlock.setDoubleValue(7, report.longitude); - dataBlock.setDoubleValue(8, report.cog); - dataBlock.setIntValue(9, report.heading); - dataBlock.setIntValue(10, report.timeStamp); - dataBlock.setIntValue(11, report.unitFlag); - dataBlock.setIntValue(12, report.displayFlag); - dataBlock.setIntValue(13, report.dscFlag); - dataBlock.setIntValue(14, report.bandFlag); - dataBlock.setIntValue(15, report.message22Flag); - dataBlock.setIntValue(16, report.modeFlag); - dataBlock.setIntValue(17, report.raimFlag); - dataBlock.setIntValue(18, report.commStateFlag); - dataBlock.setIntValue(19, report.commState); - dataBlock.setIntValue(20, report.bits); + dataBlock.setIntValue(0, report.getUserId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setDoubleValue(4, report.getSog() / 10.0); + dataBlock.setIntValue(5, report.getPosAcc()); + dataBlock.setDoubleValue(6, report.getPos().getLatitudeDouble()); + dataBlock.setDoubleValue(7, report.getPos().getLongitudeDouble()); + dataBlock.setDoubleValue(8, report.getCog() / 10.0); + dataBlock.setIntValue(9, report.getTrueHeading()); + dataBlock.setIntValue(10, report.getUtcSec()); + dataBlock.setIntValue(11, report.getClassBUnitFlag()); + dataBlock.setIntValue(12, report.getClassBDisplayFlag()); + dataBlock.setIntValue(13, report.getClassBDscFlag()); + dataBlock.setIntValue(14, report.getClassBBandFlag()); + dataBlock.setIntValue(15, report.getClassBMsg22Flag()); + dataBlock.setIntValue(16, report.getModeFlag()); + dataBlock.setIntValue(17, report.getRaim()); + dataBlock.setIntValue(18, report.getCommStateSelectorFlag()); + dataBlock.setIntValue(19, report.getCommState()); - String foiUID = parentSensor.addFoi(report.mmsi); + String foiUID = parentSensor.addFoi(report.getUserId()); latestRecord = dataBlock; latestRecordTime = System.currentTimeMillis(); diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java index 79756af00..3b3bdaa0e 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisReportInterface.java @@ -1,5 +1,7 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; +import dk.dma.ais.message.AisMessage; +import dk.dma.ais.message.AisPositionMessage; public interface NmeaAisReportInterface { - void setData(T report); + void setData(T msgTyp, String description); } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java deleted file mode 100644 index 32c17eeee..000000000 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassA.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.sensorhub.impl.sensor.nmeaais.reportschemas; - -/** - * Holds decoded fields from an AIS Class A Position Report (message types 1, 2, 3). - */ -public class PositionReportClassA { - public int messageId; // 1, 2, or 3 - public String description; - public int repeat; // 0–3 - public String mmsi; // 9-digit MMSI, zero-padded - public int navStatus; // 0–15 - public int rot; // signed, -128 to +127 deg/min - public double sog; // speed over ground in knots (raw / 10.0) - public int posAccuracy; // 0 = low, 1 = high - public double longitude; // decimal degrees (raw / 600000.0) - public double latitude; // decimal degrees (raw / 600000.0) - public double cog; // course over ground in degrees (raw / 10.0) - public int heading; // true heading 0–359, 511 = not available - public int timeStamp; // UTC second 0–59, 60/61/62/63 = special - public int smi; // special manoeuvre indicator 0–2 - public int spare; // 3 bits, reserved - public int raimFlag; // 0 = not in use, 1 = in use - public int commState; // 19-bit communication state - public int bits; // total payload bits (always 168 for Class A) -} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java deleted file mode 100644 index f4990b851..000000000 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/reportschemas/PositionReportClassB.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sensorhub.impl.sensor.nmeaais.reportschemas; - -/** - * Holds decoded fields from an AIS Class A Position Report (message types 1, 2, 3). - */ -public class PositionReportClassB { - public int messageId; - public String description; - public int repeat; - public String mmsi; - public double sog; - public int posAccuracy; - public double longitude; - public double latitude; - public double cog; - public int heading; - public int timeStamp; - public int unitFlag; - public int displayFlag; - public int dscFlag; - public int bandFlag; - public int message22Flag; - public int modeFlag; - public int raimFlag; - public int commStateFlag; - public int commState; - public int bits; - -} From 9a07ba2d86a1701f9f3006c9797bdf30794ffd6c Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Fri, 22 May 2026 16:09:49 -0500 Subject: [PATCH 09/14] added all primary outputs to driver --- .../sensorhub-driver-nmeaais/README.md | 296 ++++++++++++++++-- .../impl/sensor/nmeaais/NmeaAisDriver.java | 68 +++- .../impl/sensor/nmeaais/NmeaAisHandler.java | 55 +++- .../outputs/NmeaAisOutputAidNavigation.java | 175 +++++++++++ .../outputs/NmeaAisOutputBaseStation.java | 149 +++++++++ .../outputs/NmeaAisOutputPositionClassB.java | 198 +++++++++--- .../NmeaAisOutputStaticDataClassB.java | 164 ++++++++++ .../outputs/NmeaAisOutputStaticVoyage.java | 205 ++++++++++++ 8 files changed, 1231 insertions(+), 79 deletions(-) create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputAidNavigation.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputBaseStation.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticDataClassB.java create mode 100644 sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticVoyage.java diff --git a/sensors/positioning/sensorhub-driver-nmeaais/README.md b/sensors/positioning/sensorhub-driver-nmeaais/README.md index b7feb712b..b136670b0 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/README.md +++ b/sensors/positioning/sensorhub-driver-nmeaais/README.md @@ -1,32 +1,280 @@ # NMEA AIS Message Decoder Driver -The purpose of this driver is to decode standard NMEA AIS Messages in the standard format: -```!AIVDM,1,1,,A,H52lOI@<4pU>0lTpu:000000000,2*55 ``` +Decodes live AIS (Automatic Identification System) NMEA sentences received over any OSH-supported communication channel (UDP, TCP, serial) and publishes structured observation data for each vessel and aid-to-navigation. -NMEA Message structure looks like this: +## NMEA Sentence Structure -Field | Value | Meaning --- | -- | -- -Sentence type | !AIVDM | AIS VHF Data-link Message -Fragment count | 1 | Total number of fragments in this message -Fragment number | 1 | This is fragment 1 -Sequential ID | `` (blank) | Used to link multipart messages -Channel | A | AIS channel A (161.975 MHz) or B (162.025 MHz) -Payload | H52lOI@<4pU>0lTpu:000000000 | Encoded AIS payload -Fill bits | 2 | Padding bits added to final payload -Checksum | 55 | NMEA checksum +Each AIS transmission is a comma-delimited NMEA sentence: -This driver outputs the AIS Messages into Class A Reports, Class B reports, ATON Reports, AIS Addressed Binary Messages, and AIS Binary Broadcast Messsages as identified -https://www.navcen.uscg.gov/ais-messages. +``` +!AIVDM,1,1,,A,15Muan<000qm2=2CavBWSCL20@2?,0*6A +``` -Setup -This driver was tested using the following pre-requestites: -## Hardware -- [ShipXplorer AIS Dongle](https://www.shipxplorer.com/ais-dongle) -- [ShipXplorer AIS Antenna](https://www.shipxplorer.com/ais-antenna) +| Field | Example | Meaning | +|-------|---------|---------| +| Sentence type | `!AIVDM` | AIS VHF Data-link Message | +| Fragment count | `1` | Total fragments in this message | +| Fragment number | `1` | This fragment's index | +| Sequential ID | *(blank)* | Links multi-part messages | +| Channel | `A` | AIS Channel A (161.975 MHz) or B (162.025 MHz) | +| Payload | `15Muan<000q...` | 6-bit ASCII-armored AIS payload | +| Fill bits | `0` | Padding bits added to final payload | +| Checksum | `6A` | NMEA XOR checksum | -## Software -- Install [AIS Catcher](https://jvde-github.github.io/AIS-catcher-docs/) +--- -Local Setup (AIS-Catcher is running on local machine) -- \ No newline at end of file +## Hardware Requirements + +- [ShipXplorer AIS Dongle](https://www.shipxplorer.com/ais-dongle) (or any RTL-SDR / AIS receiver) +- [ShipXplorer AIS Antenna](https://www.shipxplorer.com/ais-antenna) (or suitable VHF antenna) + +## Software Requirements + +- [AIS-Catcher](https://jvde-github.github.io/AIS-catcher-docs/) — decodes RTL-SDR radio input and forwards NMEA sentences over UDP + +--- + +## Setup + +### 1. Install and run AIS-Catcher + +Start AIS-Catcher and forward sentences to your OSH node over UDP: + +```bash +# Forward to localhost port 10110 +AIS-catcher -u 127.0.0.1 10110 +``` + +For a remote OSH node replace `127.0.0.1` with the node's IP address. + +### 2. Add the driver in OSH Admin Panel + +1. Navigate to **Sensor Drivers** → **Add Driver** +2. Select **NMEA AIS Message Decoder Driver** +3. Fill in: + - **Serial Number** — any unique identifier for this driver instance (e.g. `AIS-001`) + - **Communication Settings** → select **UDP2 Comm Provider** + - **Local Address** — the address OSH should bind to (e.g. `127.0.0.1`) + - **Local Port** — must match the port used in AIS-Catcher (e.g. `10110`) + - Leave Remote Host/Port empty — the driver only *receives* data, it does not send + +> **Note:** Configure *Local Port*, not *Remote Port*. Remote Host/Port is for outbound connections; this driver only receives inbound UDP packets. + +### 4. Start the driver + +Click **Start**. The driver connects to the UDP socket, reads incoming sentences, and begins publishing to all outputs as messages arrive. + +--- + +## Outputs + +The driver registers **7 outputs**. Each output creates a Feature of Interest (FOI) keyed by MMSI the first time a vessel is seen. + +### 1. NMEA AIS Messages (`nmeaAisOutputRawMessages`) + +Publishes every raw sentence as received — before decoding. Useful for logging, replay, and debugging. + +| Field | Type | Description | +|-------|------|-------------| +| sampleTime | Time | System time sentence was received | +| sentenceType | Text | e.g. `!AIVDM` | +| fragmentCount | Integer | Total fragments in message | +| fragmentNumber | Integer | This fragment's index | +| sequentialId | Text | Multi-part link ID | +| channel | Text | `A` or `B` | +| frequency | Double (MHz) | 161.975 (A) or 162.025 (B) | +| rawPayload | Text | Encoded AIS payload | +| fillBits | Integer | Padding bit count | +| checkSum | Text | NMEA checksum | + +--- + +### 2. Position Report Class A (`nmeaAisOutputPositionClassA`) + +Decoded Class A shipborne position reports. Published for message types **1**, **2**, and **3** — all three carry an identical field layout. + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | 1 = Scheduled, 2 = Assigned, 3 = Interrogation response | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator (0–3) | +| mmsi | Integer | Maritime Mobile Service Identity | +| navStatus | Integer | Navigational status (0 = underway, 1 = at anchor, etc.) | +| rot | Integer | Rate of turn (–128 to +127; –128 = not available) | +| sog | Double (kn) | Speed over ground | +| positionAccuracy | Integer | 1 = high (≤10 m), 0 = low | +| location | Lat/Lon | Position in decimal degrees | +| cog | Double (°) | Course over ground | +| heading | Integer (°) | True heading (511 = not available) | +| timeStamp | Time | UTC second of fix | +| smi | Integer | Special manoeuvre indicator | +| raim | Integer | RAIM flag | +| commState | Integer | Communication state | + +--- + +### 3. Position Report Class B (`nmeaAisOutputPositionClassB`) + +Decoded Class B position reports. Handles both **type 18** (Standard CS) and **type 19** (Extended CS). Type 19 additionally carries vessel name, ship type, and dimensions; those fields are empty/zero for type 18 records. + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | 18 = Standard, 19 = Extended | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator | +| mmsi | Integer | MMSI | +| sog | Double (kn) | Speed over ground | +| positionAccuracy | Integer | 1 = high, 0 = low | +| location | Lat/Lon | Position in decimal degrees | +| cog | Double (°) | Course over ground | +| heading | Integer (°) | True heading (511 = not available) | +| timeStamp | Time | UTC second of fix | +| unitFlag | Integer | 0 = SOTDMA unit, 1 = CS unit *(type 18 only)* | +| displayFlag | Integer | Display capability *(type 18 only)* | +| dscFlag | Integer | DSC capability *(type 18 only)* | +| bandFlag | Integer | Band capability *(type 18 only)* | +| message22Flag | Integer | Frequency management flag *(type 18 only)* | +| modeFlag | Integer | 0 = autonomous, 1 = assigned | +| raim | Integer | RAIM flag | +| commStateFlag | Integer | SOTDMA/ITDMA selector *(type 18 only)* | +| commState | Integer | Communication state *(type 18 only)* | +| name | Text | Vessel name *(type 19 only; empty for type 18)* | +| shipType | Integer | Ship type code *(type 19 only)* | +| dimBow | Integer (m) | GPS antenna to bow *(type 19 only)* | +| dimStern | Integer (m) | GPS antenna to stern *(type 19 only)* | +| dimPort | Integer (m) | GPS antenna to port *(type 19 only)* | +| dimStarboard | Integer (m) | GPS antenna to starboard *(type 19 only)* | +| epfd | Integer | EPFD type *(type 19 only)* | +| dte | Integer | Data terminal equipment *(type 19 only)* | +| assignedMode | Integer | Assigned mode flag *(type 19 only)* | + +--- + +### 4. Base Station Report (`nmeaAisOutputBaseStation`) + +UTC/date and position broadcasts from fixed AIS base stations. Published for message types **4** and **11** (identical layout). + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | 4 = UTC/Date Report, 11 = UTC/Date Response | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator | +| mmsi | Integer | Base station MMSI | +| utcYear | Integer | UTC year | +| utcMonth | Integer | UTC month (1–12) | +| utcDay | Integer | UTC day (1–31) | +| utcHour | Integer | UTC hour (0–23; 24 = not available) | +| utcMinute | Integer | UTC minute (0–59) | +| utcSecond | Integer | UTC second (0–59) | +| positionAccuracy | Integer | 1 = high, 0 = low | +| location | Lat/Lon | Base station position | +| epfd | Integer | EPFD type | +| raim | Integer | RAIM flag | + +--- + +### 5. Static and Voyage Data (`nmeaAisOutputStaticVoyage`) + +Vessel identity and voyage information from Class A vessels. Published for message **type 5**. This is a multi-sentence (two-part) message; AISLib reassembles both parts before this output is updated. + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | Always 5 | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator | +| mmsi | Integer | MMSI | +| aisVersion | Integer | AIS version (0 = ITU1371) | +| imoNumber | Integer | IMO number (0 = not available) | +| callSign | Text | Call sign | +| name | Text | Vessel name | +| shipType | Integer | Ship type code | +| dimBow | Integer (m) | GPS antenna to bow | +| dimStern | Integer (m) | GPS antenna to stern | +| dimPort | Integer (m) | GPS antenna to port | +| dimStarboard | Integer (m) | GPS antenna to starboard | +| epfd | Integer | EPFD type | +| etaMonth | Integer | ETA month | +| etaDay | Integer | ETA day | +| etaHour | Integer | ETA hour | +| etaMinute | Integer | ETA minute | +| draught | Double (m) | Maximum static draught | +| destination | Text | Destination port | +| dte | Integer | Data terminal equipment | + +--- + +### 6. Class B Static Data (`nmeaAisOutputStaticDataClassB`) + +Vessel name, callsign, and dimensions from Class B vessels. Published for message **type 24**. + +Type 24 is transmitted in two separate sentences: Part A (name only) and Part B (callsign, ship type, dimensions). The driver caches Part A names by MMSI and publishes a combined record when Part B arrives. If Part B is received before Part A, the name field will be empty until the next transmission cycle. + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | Always 24 | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator | +| mmsi | Integer | MMSI | +| name | Text | Vessel name (from Part A; empty if not yet received) | +| callSign | Text | Call sign (from Part B) | +| shipType | Integer | Ship type code | +| dimBow | Integer (m) | GPS antenna to bow | +| dimStern | Integer (m) | GPS antenna to stern | +| dimPort | Integer (m) | GPS antenna to port | +| dimStarboard | Integer (m) | GPS antenna to starboard | +| vendorId | Text | Manufacturer vendor ID | + +--- + +### 7. Aid-to-Navigation Report (`nmeaAisOutputAidNavigation`) + +Position and status of fixed and floating aids to navigation (buoys, lighthouses, beacons). Published for message **type 21**. + +| Field | Type | Description | +|-------|------|-------------| +| messageId | Integer | Always 21 | +| reportDescription | Text | Human-readable type description | +| repeat | Integer | Repeat indicator | +| mmsi | Integer | Aid MMSI | +| typeOfAidsToNav | Integer | Aid type (1 = unspecified; 2 = reference; 3 = RACON; 4 = fixed; 21–29 = floating; etc.) | +| name | Text | Aid name | +| positionAccuracy | Integer | 1 = high, 0 = low | +| location | Lat/Lon | Aid position | +| dimBow | Integer (m) | Bow dimension | +| dimStern | Integer (m) | Stern dimension | +| dimPort | Integer (m) | Port dimension | +| dimStarboard | Integer (m) | Starboard dimension | +| epfd | Integer | EPFD type | +| utcSecond | Integer | UTC second of report | +| offPositionIndicator | Integer | 0 = on position, 1 = off position | +| raim | Integer | RAIM flag | +| virtualAid | Integer | 0 = physical, 1 = virtual (simulated) | +| assignedMode | Integer | 0 = autonomous, 1 = assigned | + +--- + +## Unhandled Message Types + +The following AIS message types are received and parsed by the radio layer but are **not published** to any output. Each is silently discarded after the raw sentence is recorded in the `nmeaAisOutputRawMessages` output. + +| Type | Name | Reason not output | +|------|------|-------------------| +| 6 | Addressed Binary Message | Application-specific binary payload; no standard decoded fields | +| 7 | Binary Acknowledge | Acknowledgment sequence numbers only; no sensor data | +| 8 | Binary Broadcast Message | Application-specific binary payload | +| 9 | SAR Aircraft Position Report | Position report for SAR aircraft; not currently implemented | +| 10 | UTC/Date Inquiry | Request-only message; no data payload | +| 12 | Addressed Safety-Related Message | Point-to-point text; not broadcast | +| 13 | Safety-Related Acknowledge | Acknowledgment only | +| 14 | Safety-Related Broadcast | Broadcast text alert; not currently implemented | +| 15 | Interrogation | Request-only message | +| 16 | Assignment Mode Command | Administrative base station command | +| 17 | DGNSS Binary Broadcast | Differential GPS corrections; specialized use | +| 20 | Data Link Management | Base station time-slot reservation; no vessel data | +| 22 | Channel Management | Base station VHF frequency management | +| 23 | Group Assignment Command | Base station group command | +| 25 | Single Slot Binary Message | Application-specific binary payload | +| 26 | Multiple Slot Binary Message | Application-specific binary payload | +| 27 | Long-Range AIS Broadcast | Class A/B position report for vessels outside base station coverage; not currently implemented | + +All received sentences — including the above types — are available in the raw messages output for custom processing or logging. diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index ca9684bd0..1ab6f1990 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -12,15 +12,23 @@ package org.sensorhub.impl.sensor.nmeaais; import dk.dma.ais.message.AisMessage18; +import dk.dma.ais.message.AisMessage19; +import dk.dma.ais.message.AisMessage21; +import dk.dma.ais.message.AisMessage4; +import dk.dma.ais.message.AisMessage5; import dk.dma.ais.message.AisPositionMessage; import dk.dma.ais.reader.AisReader; import dk.dma.ais.reader.AisReaders; import org.sensorhub.api.comm.ICommProvider; import org.sensorhub.api.common.SensorHubException; import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputAidNavigation; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputBaseStation; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputRawMessages; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassA; import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputPositionClassB; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputStaticDataClassB; +import org.sensorhub.impl.sensor.nmeaais.outputs.NmeaAisOutputStaticVoyage; import org.vast.ogc.om.MovingFeature; import java.io.IOException; @@ -43,6 +51,10 @@ public class NmeaAisDriver extends AbstractSensorModule { NmeaAisOutputRawMessages nmeaAisOutputRawMessages; NmeaAisOutputPositionClassA nmeaAisOutputPositionClassA; NmeaAisOutputPositionClassB nmeaAisOutputPositionClassB; + NmeaAisOutputBaseStation nmeaAisOutputBaseStation; + NmeaAisOutputStaticVoyage nmeaAisOutputStaticVoyage; + NmeaAisOutputStaticDataClassB nmeaAisOutputStaticDataClassB; + NmeaAisOutputAidNavigation nmeaAisOutputAidNavigation; ICommProvider commProvider; AisReader aisReader; @@ -70,6 +82,22 @@ public void doInit() throws SensorHubException { addOutput(nmeaAisOutputPositionClassB, false); nmeaAisOutputPositionClassB.doInit(); + nmeaAisOutputBaseStation = new NmeaAisOutputBaseStation(this); + addOutput(nmeaAisOutputBaseStation, false); + nmeaAisOutputBaseStation.doInit(); + + nmeaAisOutputStaticVoyage = new NmeaAisOutputStaticVoyage(this); + addOutput(nmeaAisOutputStaticVoyage, false); + nmeaAisOutputStaticVoyage.doInit(); + + nmeaAisOutputStaticDataClassB = new NmeaAisOutputStaticDataClassB(this); + addOutput(nmeaAisOutputStaticDataClassB, false); + nmeaAisOutputStaticDataClassB.doInit(); + + nmeaAisOutputAidNavigation = new NmeaAisOutputAidNavigation(this); + addOutput(nmeaAisOutputAidNavigation, false); + nmeaAisOutputAidNavigation.doInit(); + // Initialize Handler nmeaAisHandler = new NmeaAisHandler(this); } @@ -208,9 +236,47 @@ void publishPositionAReport(AisPositionMessage report, String description) { } /** - * Forwards a decoded Class B position report to the position output for publishing. + * Forwards a decoded Class B standard position report (type 18) for publishing. */ void publishPositionBReport(AisMessage18 report, String description) { nmeaAisOutputPositionClassB.setData(report, description); } + + /** + * Forwards a decoded Class B extended position report (type 19) for publishing. + */ + void publishPositionBExtReport(AisMessage19 report, String description) { + nmeaAisOutputPositionClassB.setData(report, description); + } + + /** + * Forwards a decoded base station report (types 4 and 11) to the base station output for publishing. + */ + void publishBaseStationReport(AisMessage4 report, String description) { + nmeaAisOutputBaseStation.setData(report, description); + } + + /** + * Forwards a decoded static and voyage data report (type 5) to the static/voyage output for publishing. + */ + void publishStaticVoyageReport(AisMessage5 report, String description) { + nmeaAisOutputStaticVoyage.setData(report, description); + } + + /** + * Forwards a decoded aid-to-navigation report (type 21) to the aid nav output for publishing. + */ + void publishAidNavReport(AisMessage21 report, String description) { + nmeaAisOutputAidNavigation.setData(report, description); + } + + /** + * Publishes a combined type 24 Part A + Part B record to the Class B static data output. + */ + void publishStaticDataClassBReport(int mmsi, int repeat, String name, String callSign, + int shipType, int dimBow, int dimStern, int dimPort, + int dimStarboard, String vendorId, String description) { + nmeaAisOutputStaticDataClassB.setData(mmsi, repeat, name, callSign, shipType, + dimBow, dimStern, dimPort, dimStarboard, vendorId, description); + } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index 2bcb47d69..5bb1ec405 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -2,13 +2,26 @@ import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisMessage18; +import dk.dma.ais.message.AisMessage19; +import dk.dma.ais.message.AisMessage21; +import dk.dma.ais.message.AisMessage24; +import dk.dma.ais.message.AisMessage4; +import dk.dma.ais.message.AisMessage5; import dk.dma.ais.message.AisPositionMessage; import org.sensorhub.impl.sensor.nmeaais.helpers.MessageIdDescriptions; +import java.util.HashMap; +import java.util.Map; + public class NmeaAisHandler { private final NmeaAisDriver nmeaAisDriver; private final MessageIdDescriptions reportDescription = new MessageIdDescriptions(); + // Cache of type 24 Part A vessel names keyed by MMSI. + // Part A only carries the name; Part B carries everything else. + // When Part B arrives we combine them into a single published record. + private final Map type24PartANames = new HashMap<>(); + public NmeaAisHandler(NmeaAisDriver driver) { this.nmeaAisDriver = driver; } @@ -17,16 +30,50 @@ public void handleAisMessage(AisMessage aisMessage) { int messageId = aisMessage.getMsgId(); String description = getReportDescription(messageId); switch (messageId) { - case 1, 2, 3: + case 1, 2, 3: // Class A Position Reports nmeaAisDriver.publishPositionAReport((AisPositionMessage) aisMessage, description); break; - case 4, 11: // AIS Base Station Reports + case 4, 11: // Base Station / UTC-Date Response + nmeaAisDriver.publishBaseStationReport((AisMessage4) aisMessage, description); break; - case 5: // Static/Voyage Data + case 5: // Static and Voyage Related Data + nmeaAisDriver.publishStaticVoyageReport((AisMessage5) aisMessage, description); break; - case 18: // Class B Position Reports + case 18: // Class B Standard Position Report nmeaAisDriver.publishPositionBReport((AisMessage18) aisMessage, description); break; + case 19: // Class B Extended Position Report + nmeaAisDriver.publishPositionBExtReport((AisMessage19) aisMessage, description); + break; + case 21: // Aid-to-Navigation Report + nmeaAisDriver.publishAidNavReport((AisMessage21) aisMessage, description); + break; + case 24: // Class B CS Static Data (two-part) + handleType24((AisMessage24) aisMessage, description); + break; + } + } + + private void handleType24(AisMessage24 msg, String description) { + if (msg.getPartNumber() == 0) { + // Part A — store the vessel name and wait for Part B + type24PartANames.put(msg.getUserId(), msg.getName()); + } else { + // Part B — combine with cached Part A name (if available) and publish + String name = type24PartANames.getOrDefault(msg.getUserId(), ""); + nmeaAisDriver.publishStaticDataClassBReport( + msg.getUserId(), + msg.getRepeat(), + name, + msg.getCallsign(), + msg.getShipType(), + msg.getDimBow(), + msg.getDimStern(), + msg.getDimPort(), + msg.getDimStarboard(), + msg.getVendorId(), + description + ); } } diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputAidNavigation.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputAidNavigation.java new file mode 100644 index 000000000..c47b962cc --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputAidNavigation.java @@ -0,0 +1,175 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import dk.dma.ais.message.AisMessage21; +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NmeaAisOutputAidNavigation extends VarRateSensorOutput implements NmeaAisReportInterface { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + + private static final String OUTPUT_NAME = "nmeaAisOutputAidNavigation"; + private static final String OUTPUT_LABEL = "Aid-to-Navigation Report"; + private static final String OUTPUT_DESCRIPTION = "AIS Aid-to-Navigation Report (type 21) — buoys, lighthouses, beacons"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputAidNavigation"); + + private final Object processingLock = new Object(); + + public NmeaAisOutputAidNavigation(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver, 1.0); + } + + /** + * Initializes the data structure for the output. + * + * Flat index map: + * 0 = messageId 1 = reportDescription 2 = repeat + * 3 = mmsi 4 = typeOfAidsToNav 5 = name + * 6 = positionAccuracy + * 7 = latitude (lat component of location vector) + * 8 = longitude (lon component of location vector) + * 9 = dimBow 10 = dimStern 11 = dimPort + * 12 = dimStarboard 13 = epfd 14 = utcSecond + * 15 = offPositionIndicator 16 = raim 17 = virtualAid + * 18 = assignedMode + */ + public void doInit() { + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(OUTPUT_NAME) + .label(OUTPUT_LABEL) + .description(OUTPUT_DESCRIPTION) + .definition(OUTPUT_DEFINITION) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message: 21") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated; 0-3; 0 = default") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createQuantity() + .label("MMSI Number") + .description("MMSI Number of the aid-to-navigation") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("typeOfAidsToNav", sweFactory.createQuantity() + .label("Type of Aid-to-Nav") + .description("1 = Default/unspecified; 2 = Reference point; 3 = RACON; 4 = Fixed structure; 5-20 = Fixed; 21-29 = Floating; 30-31 = Landfall/Coast/Inland") + .definition(SWEHelper.getPropertyUri("TypeOfAidsToNav"))) + .addField("name", sweFactory.createText() + .label("Name of Aid-to-Nav") + .description("Maximum 20 characters; padded with spaces") + .definition(SWEHelper.getPropertyUri("AidToNavName"))) + .addField("positionAccuracy", sweFactory.createQuantity() + .label("Position Accuracy") + .description("1 = high (<= 10 m); 0 = low (> 10 m); 0 = default") + .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) + .addField("location", geoFac.createLocationVectorLatLon() + .label("Location")) + .addField("dimBow", sweFactory.createQuantity() + .label("Dimension to Bow") + .description("Size of the aid-to-navigation, bow to GPS antenna in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimBow"))) + .addField("dimStern", sweFactory.createQuantity() + .label("Dimension to Stern") + .description("Size of the aid-to-navigation, GPS antenna to stern in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStern"))) + .addField("dimPort", sweFactory.createQuantity() + .label("Dimension to Port") + .description("Size of the aid-to-navigation, GPS antenna to port side in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimPort"))) + .addField("dimStarboard", sweFactory.createQuantity() + .label("Dimension to Starboard") + .description("Size of the aid-to-navigation, GPS antenna to starboard side in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStarboard"))) + .addField("epfd", sweFactory.createQuantity() + .label("Type of EPFD") + .description("0 = undefined, 1 = GPS, 2 = GLONASS, 3 = Combined GPS/GLONASS, 4 = Loran-C, 5 = Chayka, 6 = Integrated navigation system, 7 = Surveyed, 8 = Galileo, 15 = internal GNSS") + .definition(SWEHelper.getPropertyUri("Epfd"))) + .addField("utcSecond", sweFactory.createQuantity() + .label("UTC Second") + .description("UTC second when report was generated (0-59); 60 = not available = default") + .definition(SWEHelper.getPropertyUri("UtcSecond"))) + .addField("offPositionIndicator", sweFactory.createQuantity() + .label("Off Position Indicator") + .description("For floating aids-to-navigation: 0 = on position; 1 = off position. Only valid if UTC second is 0-59") + .definition(SWEHelper.getPropertyUri("OffPositionIndicator"))) + .addField("raim", sweFactory.createQuantity() + .label("RAIM Flag") + .description("Receiver autonomous integrity monitoring flag; 0 = RAIM not in use; 1 = RAIM in use") + .definition(SWEHelper.getPropertyUri("Raim"))) + .addField("virtualAid", sweFactory.createQuantity() + .label("Virtual Aid Flag") + .description("0 = default; 1 = virtual aid to navigation simulated by nearby AIS station") + .definition(SWEHelper.getPropertyUri("VirtualAid"))) + .addField("assignedMode", sweFactory.createQuantity() + .label("Assigned Mode Flag") + .description("0 = station operating in autonomous and continuous mode = default; 1 = station operating in assigned mode") + .definition(SWEHelper.getPropertyUri("AssignedMode"))); + + aisReportRecord = recordBuilder.build(); + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + @Override + public void setData(AisMessage21 report, String description) { + synchronized (processingLock) { + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setIntValue(4, report.getAtonType()); + dataBlock.setStringValue(5, report.getName()); + dataBlock.setIntValue(6, report.getPosAcc()); + dataBlock.setDoubleValue(7, report.getPos().getLatitudeDouble()); + dataBlock.setDoubleValue(8, report.getPos().getLongitudeDouble()); + dataBlock.setIntValue(9, report.getDimBow()); + dataBlock.setIntValue(10, report.getDimStern()); + dataBlock.setIntValue(11, report.getDimPort()); + dataBlock.setIntValue(12, report.getDimStarboard()); + dataBlock.setIntValue(13, report.getPosType()); + dataBlock.setIntValue(14, report.getUtcSec()); + dataBlock.setIntValue(15, report.getOffPosition()); + dataBlock.setIntValue(16, report.getRaim()); + dataBlock.setIntValue(17, report.getVirtual()); + dataBlock.setIntValue(18, report.getAssigned()); + + String foiUID = parentSensor.addFoi(report.getUserId()); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputAidNavigation.this, foiUID, dataBlock)); + } + } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputBaseStation.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputBaseStation.java new file mode 100644 index 000000000..d27504a0d --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputBaseStation.java @@ -0,0 +1,149 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import dk.dma.ais.message.AisMessage4; +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NmeaAisOutputBaseStation extends VarRateSensorOutput implements NmeaAisReportInterface { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + + private static final String OUTPUT_NAME = "nmeaAisOutputBaseStation"; + private static final String OUTPUT_LABEL = "Base Station Report"; + private static final String OUTPUT_DESCRIPTION = "AIS Base Station / UTC-Date Response Report (types 4 and 11)"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputBaseStation"); + + private final Object processingLock = new Object(); + + public NmeaAisOutputBaseStation(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver, 1.0); + } + + /** + * Initializes the data structure for the output. + * + * Flat index map: + * 0 = messageId 1 = reportDescription 2 = repeat + * 3 = mmsi 4 = utcYear 5 = utcMonth + * 6 = utcDay 7 = utcHour 8 = utcMinute + * 9 = utcSecond 10 = positionAccuracy + * 11 = latitude (lat component of location vector) + * 12 = longitude (lon component of location vector) + * 13 = epfd 14 = raim + */ + public void doInit() { + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(OUTPUT_NAME) + .label(OUTPUT_LABEL) + .description(OUTPUT_DESCRIPTION) + .definition(OUTPUT_DEFINITION) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message: 4 (UTC/Date Report) or 11 (UTC/Date Response)") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated; 0-3; 0 = default") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createQuantity() + .label("MMSI Number") + .description("MMSI Number of the base station") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("utcYear", sweFactory.createQuantity() + .label("UTC Year") + .description("UTC Year (1-9999, 0 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcYear"))) + .addField("utcMonth", sweFactory.createQuantity() + .label("UTC Month") + .description("UTC Month (1-12, 0 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcMonth"))) + .addField("utcDay", sweFactory.createQuantity() + .label("UTC Day") + .description("UTC Day (1-31, 0 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcDay"))) + .addField("utcHour", sweFactory.createQuantity() + .label("UTC Hour") + .description("UTC Hour (0-23, 24 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcHour"))) + .addField("utcMinute", sweFactory.createQuantity() + .label("UTC Minute") + .description("UTC Minute (0-59, 60 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcMinute"))) + .addField("utcSecond", sweFactory.createQuantity() + .label("UTC Second") + .description("UTC Second (0-59, 60 = not available = default)") + .definition(SWEHelper.getPropertyUri("UtcSecond"))) + .addField("positionAccuracy", sweFactory.createQuantity() + .label("Position Accuracy") + .description("1 = high (<= 10 m); 0 = low (> 10 m); 0 = default") + .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) + .addField("location", geoFac.createLocationVectorLatLon() + .label("Location")) + .addField("epfd", sweFactory.createQuantity() + .label("Type of EPFD") + .description("0 = undefined, 1 = GPS, 2 = GLONASS, 3 = Combined GPS/GLONASS, 4 = Loran-C, 5 = Chayka, 6 = Integrated navigation system, 7 = Surveyed, 8 = Galileo, 15 = internal GNSS") + .definition(SWEHelper.getPropertyUri("Epfd"))) + .addField("raim", sweFactory.createQuantity() + .label("RAIM Flag") + .description("Receiver autonomous integrity monitoring flag; 0 = RAIM not in use; 1 = RAIM in use") + .definition(SWEHelper.getPropertyUri("Raim"))); + + aisReportRecord = recordBuilder.build(); + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + @Override + public void setData(AisMessage4 report, String description) { + synchronized (processingLock) { + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setIntValue(4, report.getUtcYear()); + dataBlock.setIntValue(5, report.getUtcMonth()); + dataBlock.setIntValue(6, report.getUtcDay()); + dataBlock.setIntValue(7, report.getUtcHour()); + dataBlock.setIntValue(8, report.getUtcMinute()); + dataBlock.setIntValue(9, report.getUtcSecond()); + dataBlock.setIntValue(10, report.getPosAcc()); + dataBlock.setDoubleValue(11, report.getPos().getLatitudeDouble()); + dataBlock.setDoubleValue(12, report.getPos().getLongitudeDouble()); + dataBlock.setIntValue(13, report.getPosType()); + dataBlock.setIntValue(14, report.getRaim()); + + String foiUID = parentSensor.addFoi(report.getUserId()); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputBaseStation.this, foiUID, dataBlock)); + } + } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java index 7580fc5f5..ddc15ac3c 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputPositionClassB.java @@ -1,6 +1,7 @@ package org.sensorhub.impl.sensor.nmeaais.outputs; import dk.dma.ais.message.AisMessage18; +import dk.dma.ais.message.AisMessage19; import net.opengis.swe.v20.DataBlock; import net.opengis.swe.v20.DataComponent; import net.opengis.swe.v20.DataEncoding; @@ -12,29 +13,39 @@ import org.vast.swe.SWEHelper; import org.vast.swe.helper.GeoPosHelper; -public class NmeaAisOutputPositionClassB extends VarRateSensorOutput implements NmeaAisReportInterface { +/** + * Output for Class B AIS position reports. + * Handles both type 18 (Standard CS Position) and type 19 (Extended CS Position). + * Type 19 records additionally carry vessel name, ship type, and dimensions; + * those fields are empty/zero for type 18 records. + */ +public class NmeaAisOutputPositionClassB extends VarRateSensorOutput { private DataRecord aisReportRecord; private DataEncoding dataEncoding; private final Object processingLock = new Object(); public NmeaAisOutputPositionClassB(NmeaAisDriver nmeaAisDriver) { - super("nmeaAisOutputPositionClassB", nmeaAisDriver,1.0); + super("nmeaAisOutputPositionClassB", nmeaAisDriver, 1.0); } /** * Initializes the data structure for the output. * - * Flat index map: - * 0 = messageId 1 = repeat 2 = mmsi - * 3 = sog 4 = positionAccuracy - * 5 = latitude (lat component of location vector) - * 6 = longitude (lon component of location vector) - * 7 = cog 8 = heading 9 = timeStamp - * 10 = unitFlag 11 = displayFlag 12 = dscFlag - * 13 = bandFlag 14 = message22Flag 15 = modeFlag - * 16 = raimFlag 17 = commStateFlag 18 = commState - * 19 = bits + * Flat index map (common to types 18 and 19): + * 0 = messageId 1 = reportDescription 2 = repeat + * 3 = mmsi 4 = sog 5 = positionAccuracy + * 6 = latitude (lat component of location vector) + * 7 = longitude (lon component of location vector) + * 8 = cog 9 = heading 10 = timeStamp + * 11 = unitFlag 12 = displayFlag 13 = dscFlag + * 14 = bandFlag 15 = message22Flag 16 = modeFlag + * 17 = raim 18 = commStateFlag 19 = commState + * + * Additional fields populated only for type 19 (empty/zero for type 18): + * 20 = name 21 = shipType 22 = dimBow + * 23 = dimStern 24 = dimPort 25 = dimStarboard + * 26 = epfd 27 = dte 28 = assignedMode */ public void doInit() { GeoPosHelper geoFac = new GeoPosHelper(); @@ -43,11 +54,13 @@ public void doInit() { SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() .name("nmeaAisOutputPositionClassB") .label("Position Report Class B") - .description("Class B AIS Position Report") + .description("Class B AIS Position Report — types 18 (Standard) and 19 (Extended). " + + "Name, ship type, and dimension fields are only populated for type 19.") .definition(SWEHelper.getPropertyUri("NmeaAisOutputPositionClassB")) + // --- fields common to type 18 and 19 --- .addField("messageId", sweFactory.createQuantity() .label("Message Id") - .description("Identifier for this message 1, 2 or 3") + .description("18 = Standard Class B CS Position Report; 19 = Extended Class B CS Position Report") .definition(SWEHelper.getPropertyUri("MessageId"))) .addField("reportDescription", sweFactory.createText() .label("Report Description") @@ -55,7 +68,7 @@ public void doInit() { .definition(SWEHelper.getPropertyUri("ReportDescription"))) .addField("repeat", sweFactory.createQuantity() .label("Repeat Indicator") - .description("Used by the repeater to indicate how many times a message has been repeated. See Section 4.6.1, Annex 2; 0-3; 0 = default; 3 = do not repeat any more.") + .description("Used by the repeater to indicate how many times a message has been repeated; 0-3; 0 = default") .definition(SWEHelper.getPropertyUri("repeat"))) .addField("mmsi", sweFactory.createQuantity() .label("MMSI Number") @@ -63,86 +76,117 @@ public void doInit() { .definition(SWEHelper.getPropertyUri("Mmsi"))) .addField("sog", sweFactory.createQuantity() .label("Speed Over Ground") - .description("Speed over ground in knots (0–102.2 knots, 102.3 = not available, 102.3+ should not be used)") + .description("Speed over ground in knots (0–102.2 knots; 102.3 = not available)") .uom("[kn_i]") .definition(SWEHelper.getPropertyUri("SpeedOverGround"))) .addField("positionAccuracy", sweFactory.createQuantity() .label("Position Accuracy") - .description( - "1 = high (<= 10 m)\n" + - "0 = low (> 10 m)\n" + - "0 = default" - ) + .description("1 = high (<= 10 m); 0 = low (> 10 m); 0 = default") .definition(SWEHelper.getPropertyUri("PositionAccuracy"))) .addField("location", geoFac.createLocationVectorLatLon() .label("Location")) .addField("cog", sweFactory.createQuantity() .label("COG") - .description("Course over ground in 1/10 = (0-3599). 3600 (E10h) = not available = default. 3601-4095 should not be used") + .description("Course over ground in degrees (0–359.9); 360 = not available = default") .definition(SWEHelper.getPropertyUri("Cog"))) .addField("heading", sweFactory.createQuantity() .label("True Heading") .uom("deg") - .description("Degrees (0-359) (511 indicates not available = default)") + .description("Degrees (0-359); 511 = not available = default") .definition(SWEHelper.getPropertyUri("heading"))) .addField("timeStamp", sweFactory.createTime() .label("Time Stamp") - .description("UTC second when the report was generated by the electronic position system (EPFS) (0-59, or 60 if time stamp is not available, which should also be the default value, or 61 if positioning system is in manual input mode, or 62 if electronic position fixing system operates in estimated (dead reckoning) mode, or 63 if the positioning system is inoperative)") + .description("UTC second when the report was generated (0-59); 60 = not available = default") .definition(SWEHelper.getPropertyUri("TimeStamp"))) .addField("unitFlag", sweFactory.createQuantity() .label("Class B Unit Flag") - .description("0 (Class B unit) or 1 (Class B CS unit)") + .description("0 = Class B SOTDMA unit; 1 = Class B CS unit") .definition(SWEHelper.getPropertyUri("UnitFlag"))) .addField("displayFlag", sweFactory.createQuantity() .label("Class B Display Flag") - .description("0 = No display available; not capable of displaying Message 12 and 14\n" + - "1 = Equipped with integrated display displaying Message 12 and 14") + .description("0 = No display available; 1 = Equipped with display for Message 12 and 14") .definition(SWEHelper.getPropertyUri("DisplayFlag"))) .addField("dscFlag", sweFactory.createQuantity() .label("Class B DSC Flag") - .description("0 = Not equipped with DSC function\n" + - "1 = Equipped with DSC function (dedicated or time-shared)") + .description("0 = Not equipped with DSC function; 1 = Equipped with DSC function") .definition(SWEHelper.getPropertyUri("DscFlag"))) .addField("bandFlag", sweFactory.createQuantity() .label("Class B Band Flag") - .description("0 = Capable of operating over the upper 525 kHz band of the marine band\n" + - "1 = Capable of operating over the whole marine band (irrelevant if \"Class B Message 22 flag\" is 0)") + .description("0 = Capable of operating over upper 525 kHz band; 1 = Capable of operating over whole marine band") .definition(SWEHelper.getPropertyUri("BandFlag"))) .addField("message22Flag", sweFactory.createQuantity() .label("Class B Message 22 Flag") - .description("0 = No frequency management via Message 22 , operating on AIS1, AIS2 only\n" + - "1 = Frequency management via Message 22") + .description("0 = No frequency management via Message 22; 1 = Frequency management via Message 22") .definition(SWEHelper.getPropertyUri("Message22Flag"))) .addField("modeFlag", sweFactory.createQuantity() .label("Mode Flag") - .description("0 = Station operating in autonomous and continuous mode = default\n" + - "1 = Station operating in assigned mode") + .description("0 = Autonomous and continuous mode = default; 1 = Assigned mode") .definition(SWEHelper.getPropertyUri("ModeFlag"))) .addField("raim", sweFactory.createQuantity() - .label("RAIM-flag") - .description("Receiver autonomous integrity monitoring (RAIM) flag of electronic position fixing device; 0 = RAIM not in use = default; 1 = RAIM in use. See Table") + .label("RAIM Flag") + .description("0 = RAIM not in use = default; 1 = RAIM in use") .definition(SWEHelper.getPropertyUri("Raim"))) .addField("commStateFlag", sweFactory.createQuantity() .label("Communication State Selector Flag") - .description("0 = SOTDMA communication state follows\n" + - "1 = ITDMA communication state follows (always 1 for Class-B \"CS\")") + .description("0 = SOTDMA communication state follows; 1 = ITDMA (always 1 for Class B CS)") .definition(SWEHelper.getPropertyUri("CommStateFlag"))) .addField("commState", sweFactory.createQuantity() .label("Communication State") - .description("OTDMA communication state. Because Class B \"CS\" does not use any Communication State information, this field shall be filled with the following value: 1100000000000000110.") - .definition(SWEHelper.getPropertyUri("CommState"))); + .description("SOTDMA or ITDMA communication state") + .definition(SWEHelper.getPropertyUri("CommState"))) + // --- additional fields populated only for type 19 --- + .addField("name", sweFactory.createText() + .label("Vessel Name") + .description("Vessel name (type 19 only; empty for type 18)") + .definition(SWEHelper.getPropertyUri("VesselName"))) + .addField("shipType", sweFactory.createQuantity() + .label("Ship Type") + .description("Ship type per ITU-R M.1371-5 Table 53 (type 19 only; 0 for type 18)") + .definition(SWEHelper.getPropertyUri("ShipType"))) + .addField("dimBow", sweFactory.createQuantity() + .label("Dimension to Bow") + .description("Distance from GPS antenna to bow in metres (type 19 only)") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimBow"))) + .addField("dimStern", sweFactory.createQuantity() + .label("Dimension to Stern") + .description("Distance from GPS antenna to stern in metres (type 19 only)") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStern"))) + .addField("dimPort", sweFactory.createQuantity() + .label("Dimension to Port") + .description("Distance from GPS antenna to port side in metres (type 19 only)") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimPort"))) + .addField("dimStarboard", sweFactory.createQuantity() + .label("Dimension to Starboard") + .description("Distance from GPS antenna to starboard side in metres (type 19 only)") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStarboard"))) + .addField("epfd", sweFactory.createQuantity() + .label("Type of EPFD") + .description("Electronic position fixing device type (type 19 only)") + .definition(SWEHelper.getPropertyUri("Epfd"))) + .addField("dte", sweFactory.createQuantity() + .label("DTE") + .description("Data terminal equipment available; 0 = available; 1 = not available (type 19 only)") + .definition(SWEHelper.getPropertyUri("Dte"))) + .addField("assignedMode", sweFactory.createQuantity() + .label("Assigned Mode Flag") + .description("0 = autonomous mode = default; 1 = assigned mode (type 19 only)") + .definition(SWEHelper.getPropertyUri("AssignedMode"))); aisReportRecord = recordBuilder.build(); dataEncoding = geoFac.newTextEncoding(",", "\n"); } - @Override + /** Publishes a type 18 (Standard CS) record. Type-19-specific fields are set to zero/empty. */ public void setData(AisMessage18 report, String description) { synchronized (processingLock) { DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); - dataBlock.setIntValue(0, report.getUserId()); - dataBlock.setStringValue(1, description); + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); dataBlock.setIntValue(2, report.getRepeat()); dataBlock.setIntValue(3, report.getUserId()); dataBlock.setDoubleValue(4, report.getSog() / 10.0); @@ -151,7 +195,7 @@ public void setData(AisMessage18 report, String description) { dataBlock.setDoubleValue(7, report.getPos().getLongitudeDouble()); dataBlock.setDoubleValue(8, report.getCog() / 10.0); dataBlock.setIntValue(9, report.getTrueHeading()); - dataBlock.setIntValue(10, report.getUtcSec()); + dataBlock.setIntValue(10, report.getUtcSec()); dataBlock.setIntValue(11, report.getClassBUnitFlag()); dataBlock.setIntValue(12, report.getClassBDisplayFlag()); dataBlock.setIntValue(13, report.getClassBDscFlag()); @@ -161,16 +205,70 @@ public void setData(AisMessage18 report, String description) { dataBlock.setIntValue(17, report.getRaim()); dataBlock.setIntValue(18, report.getCommStateSelectorFlag()); dataBlock.setIntValue(19, report.getCommState()); + // type-19-only fields — not present in type 18 + dataBlock.setStringValue(20, ""); + dataBlock.setIntValue(21, 0); + dataBlock.setIntValue(22, 0); + dataBlock.setIntValue(23, 0); + dataBlock.setIntValue(24, 0); + dataBlock.setIntValue(25, 0); + dataBlock.setIntValue(26, 0); + dataBlock.setIntValue(27, 0); + dataBlock.setIntValue(28, 0); + + publish(dataBlock, report.getUserId()); + } + } - String foiUID = parentSensor.addFoi(report.getUserId()); + /** Publishes a type 19 (Extended CS) record. All fields including name, ship type, and dimensions are populated. */ + public void setData(AisMessage19 report, String description) { + synchronized (processingLock) { + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setDoubleValue(4, report.getSog() / 10.0); + dataBlock.setIntValue(5, report.getPosAcc()); + dataBlock.setDoubleValue(6, report.getPos().getLatitudeDouble()); + dataBlock.setDoubleValue(7, report.getPos().getLongitudeDouble()); + dataBlock.setDoubleValue(8, report.getCog() / 10.0); + dataBlock.setIntValue(9, report.getTrueHeading()); + dataBlock.setIntValue(10, report.getUtcSec()); + // type 19 does not carry Class B CS radio flags — set to 0 + dataBlock.setIntValue(11, 0); + dataBlock.setIntValue(12, 0); + dataBlock.setIntValue(13, 0); + dataBlock.setIntValue(14, 0); + dataBlock.setIntValue(15, 0); + dataBlock.setIntValue(16, report.getModeFlag()); + dataBlock.setIntValue(17, report.getRaim()); + dataBlock.setIntValue(18, 0); + dataBlock.setIntValue(19, 0); + // type-19-only fields + dataBlock.setStringValue(20, report.getName()); + dataBlock.setIntValue(21, report.getShipType()); + dataBlock.setIntValue(22, report.getDimBow()); + dataBlock.setIntValue(23, report.getDimStern()); + dataBlock.setIntValue(24, report.getDimPort()); + dataBlock.setIntValue(25, report.getDimStarboard()); + dataBlock.setIntValue(26, report.getPosType()); + dataBlock.setIntValue(27, report.getDte()); + dataBlock.setIntValue(28, report.getModeFlag()); - latestRecord = dataBlock; - latestRecordTime = System.currentTimeMillis(); - updateSamplingPeriod(latestRecordTime); - eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassB.this, foiUID, dataBlock)); + publish(dataBlock, report.getUserId()); } } + private void publish(DataBlock dataBlock, int userId) { + String foiUID = parentSensor.addFoi(userId); + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputPositionClassB.this, foiUID, dataBlock)); + } + @Override public DataComponent getRecordDescription() { return aisReportRecord; diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticDataClassB.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticDataClassB.java new file mode 100644 index 000000000..7dea70a09 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticDataClassB.java @@ -0,0 +1,164 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +/** + * Output for AIS Type 24 — Class B CS Static Data. + * + * Type 24 is transmitted in two separate sentences: + * Part A (partNumber = 0): MMSI + vessel name + * Part B (partNumber = 1): MMSI + callsign + ship type + dimensions + vendor ID + * + * The handler caches Part A names by MMSI and combines them with Part B on arrival. + * A record is published when Part B is received; the name field will be empty if + * the corresponding Part A has not yet been seen. + * + * Flat index map: + * 0 = messageId 1 = reportDescription 2 = repeat + * 3 = mmsi 4 = name 5 = callSign + * 6 = shipType 7 = dimBow 8 = dimStern + * 9 = dimPort 10 = dimStarboard 11 = vendorId + */ +public class NmeaAisOutputStaticDataClassB extends VarRateSensorOutput { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + + private static final String OUTPUT_NAME = "nmeaAisOutputStaticDataClassB"; + private static final String OUTPUT_LABEL = "Class B Static Data"; + private static final String OUTPUT_DESCRIPTION = "AIS Class B CS Static Data Report (type 24) — vessel name, callsign, ship type, and dimensions"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputStaticDataClassB"); + + private final Object processingLock = new Object(); + + public NmeaAisOutputStaticDataClassB(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver, 1.0); + } + + public void doInit() { + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(OUTPUT_NAME) + .label(OUTPUT_LABEL) + .description(OUTPUT_DESCRIPTION) + .definition(OUTPUT_DEFINITION) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message: 24") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated; 0-3; 0 = default") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createQuantity() + .label("MMSI Number") + .description("MMSI Number of the vessel") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("name", sweFactory.createText() + .label("Vessel Name") + .description("Vessel name from Part A (20 characters max); empty if Part A not yet received") + .definition(SWEHelper.getPropertyUri("VesselName"))) + .addField("callSign", sweFactory.createText() + .label("Call Sign") + .description("Call sign (7 x 6-bit ASCII characters, padded with spaces)") + .definition(SWEHelper.getPropertyUri("CallSign"))) + .addField("shipType", sweFactory.createQuantity() + .label("Ship Type") + .description("Ship type per ITU-R M.1371-5 Table 53; 0 = not available or no ship = default") + .definition(SWEHelper.getPropertyUri("ShipType"))) + .addField("dimBow", sweFactory.createQuantity() + .label("Dimension to Bow") + .description("Distance from GPS antenna to bow in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimBow"))) + .addField("dimStern", sweFactory.createQuantity() + .label("Dimension to Stern") + .description("Distance from GPS antenna to stern in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStern"))) + .addField("dimPort", sweFactory.createQuantity() + .label("Dimension to Port") + .description("Distance from GPS antenna to port side in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimPort"))) + .addField("dimStarboard", sweFactory.createQuantity() + .label("Dimension to Starboard") + .description("Distance from GPS antenna to starboard side in metres; 0 = not available = default") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStarboard"))) + .addField("vendorId", sweFactory.createText() + .label("Vendor ID") + .description("Unique identification of the Unit by the manufacturer") + .definition(SWEHelper.getPropertyUri("VendorId"))); + + aisReportRecord = recordBuilder.build(); + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + /** + * Publishes a combined Part A + Part B record. + * + * @param mmsi MMSI from the Part B sentence + * @param repeat Repeat indicator from Part B + * @param name Vessel name from cached Part A; empty string if Part A not yet received + * @param callSign Call sign from Part B + * @param shipType Ship type from Part B + * @param dimBow Dimension to bow from Part B + * @param dimStern Dimension to stern from Part B + * @param dimPort Dimension to port from Part B + * @param dimStarboard Dimension to starboard from Part B + * @param vendorId Vendor ID from Part B + * @param description Message type description + */ + public void setData(int mmsi, int repeat, String name, String callSign, int shipType, + int dimBow, int dimStern, int dimPort, int dimStarboard, + String vendorId, String description) { + synchronized (processingLock) { + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + + dataBlock.setIntValue(0, 24); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, repeat); + dataBlock.setIntValue(3, mmsi); + dataBlock.setStringValue(4, name); + dataBlock.setStringValue(5, callSign); + dataBlock.setIntValue(6, shipType); + dataBlock.setIntValue(7, dimBow); + dataBlock.setIntValue(8, dimStern); + dataBlock.setIntValue(9, dimPort); + dataBlock.setIntValue(10, dimStarboard); + dataBlock.setStringValue(11, vendorId); + + String foiUID = parentSensor.addFoi(mmsi); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputStaticDataClassB.this, foiUID, dataBlock)); + } + } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticVoyage.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticVoyage.java new file mode 100644 index 000000000..65ceb3158 --- /dev/null +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/outputs/NmeaAisOutputStaticVoyage.java @@ -0,0 +1,205 @@ +package org.sensorhub.impl.sensor.nmeaais.outputs; + +import dk.dma.ais.message.AisMessage5; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.VarRateSensorOutput; +import org.sensorhub.impl.sensor.nmeaais.NmeaAisDriver; +import org.vast.swe.SWEBuilders; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NmeaAisOutputStaticVoyage extends VarRateSensorOutput implements NmeaAisReportInterface { + private DataRecord aisReportRecord; + private DataEncoding dataEncoding; + + private static final String OUTPUT_NAME = "nmeaAisOutputStaticVoyage"; + private static final String OUTPUT_LABEL = "Static and Voyage Related Data"; + private static final String OUTPUT_DESCRIPTION = "AIS Static and Voyage Related Data Report (type 5)"; + private static final String OUTPUT_DEFINITION = SWEHelper.getPropertyUri("NmeaAisOutputStaticVoyage"); + + private final Object processingLock = new Object(); + + public NmeaAisOutputStaticVoyage(NmeaAisDriver nmeaAisDriver) { + super(OUTPUT_NAME, nmeaAisDriver, 1.0); + } + + /** + * Initializes the data structure for the output. + * + * Flat index map: + * 0 = messageId 1 = reportDescription 2 = repeat + * 3 = mmsi 4 = aisVersion 5 = imoNumber + * 6 = callSign 7 = name 8 = shipType + * 9 = dimBow 10 = dimStern 11 = dimPort + * 12 = dimStarboard 13 = epfd 14 = etaMonth + * 15 = etaDay 16 = etaHour 17 = etaMinute + * 18 = draught 19 = destination 20 = dte + */ + public void doInit() { + GeoPosHelper geoFac = new GeoPosHelper(); + SWEHelper sweFactory = new SWEHelper(); + + SWEBuilders.DataRecordBuilder recordBuilder = sweFactory.createRecord() + .name(OUTPUT_NAME) + .label(OUTPUT_LABEL) + .description(OUTPUT_DESCRIPTION) + .definition(OUTPUT_DEFINITION) + .addField("messageId", sweFactory.createQuantity() + .label("Message Id") + .description("Identifier for this message: 5") + .definition(SWEHelper.getPropertyUri("MessageId"))) + .addField("reportDescription", sweFactory.createText() + .label("Report Description") + .description("Describes the report based on the Message Id provided") + .definition(SWEHelper.getPropertyUri("ReportDescription"))) + .addField("repeat", sweFactory.createQuantity() + .label("Repeat Indicator") + .description("Used by the repeater to indicate how many times a message has been repeated; 0-3; 0 = default") + .definition(SWEHelper.getPropertyUri("repeat"))) + .addField("mmsi", sweFactory.createQuantity() + .label("MMSI Number") + .description("MMSI Number of the vessel") + .definition(SWEHelper.getPropertyUri("Mmsi"))) + .addField("aisVersion", sweFactory.createQuantity() + .label("AIS Version") + .description("0 = ITU1371; 1-3 = future editions") + .definition(SWEHelper.getPropertyUri("AisVersion"))) + .addField("imoNumber", sweFactory.createQuantity() + .label("IMO Number") + .description("1-999999999; 0 = not available = default") + .definition(SWEHelper.getPropertyUri("ImoNumber"))) + .addField("callSign", sweFactory.createText() + .label("Call Sign") + .description("7 x 6 bit ASCII characters, padded with spaces after") + .definition(SWEHelper.getPropertyUri("CallSign"))) + .addField("name", sweFactory.createText() + .label("Vessel Name") + .description("Maximum 20 characters; padded with spaces after. Indicate \"Not available\" if not known") + .definition(SWEHelper.getPropertyUri("VesselName"))) + .addField("shipType", sweFactory.createQuantity() + .label("Ship Type") + .description("0 = not available or no ship = default; 1-99 per ITU-R M.1371-5 Table 53") + .definition(SWEHelper.getPropertyUri("ShipType"))) + .addField("dimBow", sweFactory.createQuantity() + .label("Dimension to Bow") + .description("Distance from GPS antenna to bow in metres; 0 = not available = default; 511 = 511 m or greater") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimBow"))) + .addField("dimStern", sweFactory.createQuantity() + .label("Dimension to Stern") + .description("Distance from GPS antenna to stern in metres; 0 = not available = default; 511 = 511 m or greater") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStern"))) + .addField("dimPort", sweFactory.createQuantity() + .label("Dimension to Port") + .description("Distance from GPS antenna to port side in metres; 0 = not available = default; 63 = 63 m or greater") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimPort"))) + .addField("dimStarboard", sweFactory.createQuantity() + .label("Dimension to Starboard") + .description("Distance from GPS antenna to starboard side in metres; 0 = not available = default; 63 = 63 m or greater") + .uom("m") + .definition(SWEHelper.getPropertyUri("DimStarboard"))) + .addField("epfd", sweFactory.createQuantity() + .label("Type of EPFD") + .description("0 = undefined, 1 = GPS, 2 = GLONASS, 3 = Combined GPS/GLONASS, 4 = Loran-C, 5 = Chayka, 6 = Integrated navigation system, 7 = Surveyed, 8 = Galileo, 15 = internal GNSS") + .definition(SWEHelper.getPropertyUri("Epfd"))) + .addField("etaMonth", sweFactory.createQuantity() + .label("ETA Month") + .description("Estimated time of arrival month; 1-12; 0 = not available = default") + .definition(SWEHelper.getPropertyUri("EtaMonth"))) + .addField("etaDay", sweFactory.createQuantity() + .label("ETA Day") + .description("Estimated time of arrival day; 1-31; 0 = not available = default") + .definition(SWEHelper.getPropertyUri("EtaDay"))) + .addField("etaHour", sweFactory.createQuantity() + .label("ETA Hour") + .description("Estimated time of arrival hour; 0-23; 24 = not available = default") + .definition(SWEHelper.getPropertyUri("EtaHour"))) + .addField("etaMinute", sweFactory.createQuantity() + .label("ETA Minute") + .description("Estimated time of arrival minute; 0-59; 60 = not available = default") + .definition(SWEHelper.getPropertyUri("EtaMinute"))) + .addField("draught", sweFactory.createQuantity() + .label("Maximum Present Static Draught") + .description("In metres; 1/10 m steps; 0 = not available = default; 25.5 m = draught is 25.5 m or greater") + .uom("m") + .definition(SWEHelper.getPropertyUri("Draught"))) + .addField("destination", sweFactory.createText() + .label("Destination") + .description("Maximum 20 characters; padded with spaces after. Indicate \"Not available\" if not known") + .definition(SWEHelper.getPropertyUri("Destination"))) + .addField("dte", sweFactory.createQuantity() + .label("DTE") + .description("Data terminal equipment (DTE) available; 0 = available; 1 = not available = default") + .definition(SWEHelper.getPropertyUri("Dte"))); + + aisReportRecord = recordBuilder.build(); + dataEncoding = geoFac.newTextEncoding(",", "\n"); + } + + @Override + public void setData(AisMessage5 report, String description) { + synchronized (processingLock) { + DataBlock dataBlock = latestRecord == null ? aisReportRecord.createDataBlock() : latestRecord.renew(); + + dataBlock.setIntValue(0, report.getMsgId()); + dataBlock.setStringValue(1, description); + dataBlock.setIntValue(2, report.getRepeat()); + dataBlock.setIntValue(3, report.getUserId()); + dataBlock.setIntValue(4, report.getVersion()); + dataBlock.setLongValue(5, report.getImo()); + dataBlock.setStringValue(6, report.getCallsign()); + dataBlock.setStringValue(7, report.getName()); + dataBlock.setIntValue(8, report.getShipType()); + dataBlock.setIntValue(9, report.getDimBow()); + dataBlock.setIntValue(10, report.getDimStern()); + dataBlock.setIntValue(11, report.getDimPort()); + dataBlock.setIntValue(12, report.getDimStarboard()); + dataBlock.setIntValue(13, report.getPosType()); + + // ETA is stored as a packed long; getEtaDate() converts to java.util.Date + int etaMonth = 0, etaDay = 0, etaHour = 24, etaMinute = 60; + Date etaDate = report.getEtaDate(); + if (etaDate != null) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTime(etaDate); + etaMonth = cal.get(Calendar.MONTH) + 1; + etaDay = cal.get(Calendar.DAY_OF_MONTH); + etaHour = cal.get(Calendar.HOUR_OF_DAY); + etaMinute = cal.get(Calendar.MINUTE); + } + dataBlock.setIntValue(14, etaMonth); + dataBlock.setIntValue(15, etaDay); + dataBlock.setIntValue(16, etaHour); + dataBlock.setIntValue(17, etaMinute); + dataBlock.setDoubleValue(18, report.getDraught() / 10.0); + dataBlock.setStringValue(19, report.getDest()); + dataBlock.setIntValue(20, report.getDte()); + + String foiUID = parentSensor.addFoi(report.getUserId()); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + updateSamplingPeriod(latestRecordTime); + eventHandler.publish(new DataEvent(latestRecordTime, NmeaAisOutputStaticVoyage.this, foiUID, dataBlock)); + } + } + + @Override + public DataComponent getRecordDescription() { + return aisReportRecord; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} From fcd59975e35bb8c04b6b24fee86554951d3f13bf Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 26 May 2026 08:30:20 -0500 Subject: [PATCH 10/14] updated depricated stop() method to stopReader() --- .../java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index 1ab6f1990..2164ce285 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -184,7 +184,7 @@ public void doStop() throws SensorHubException { started = false; if (aisReader != null) { - aisReader.stop(); + aisReader.stopReader(); aisReader = null; } From 132dba9c400d54ea1bb4bdd1b2bb9df3fd4c7124 Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 26 May 2026 08:31:37 -0500 Subject: [PATCH 11/14] added default logging in handler switch statement for unknown ids --- .../java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java index 5bb1ec405..ef1c73290 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisHandler.java @@ -51,6 +51,8 @@ public void handleAisMessage(AisMessage aisMessage) { case 24: // Class B CS Static Data (two-part) handleType24((AisMessage24) aisMessage, description); break; + default: + nmeaAisDriver.getLogger().debug("No Output has been created to capture AIS reports with a Message ID of {}",messageId); } } From e754a7a0ae732fae4b73a11b10af5986af8ff3c6 Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 26 May 2026 08:35:09 -0500 Subject: [PATCH 12/14] remved shipXplorer default in Config --- .../java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java index 97318706a..ef3970b52 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisConfig.java @@ -25,7 +25,7 @@ public class NmeaAisConfig extends SensorConfig { */ @DisplayInfo.Required @DisplayInfo(desc = "Serial number or unique identifier") - public String serialNumber = "myShipXplorer"; + public String serialNumber = "myNmeaAisDevice"; @DisplayInfo(desc = "Communication settings for receiving AIS NMEA sentences (e.g. UDP, TCP, serial)") public CommProviderConfig commSettings; From 5f1d4c88fca39b6883ec51cbbb5ccafa229a566c Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 26 May 2026 08:56:27 -0500 Subject: [PATCH 13/14] updated ReadMe --- .../sensorhub-driver-nmeaais/README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/README.md b/sensors/positioning/sensorhub-driver-nmeaais/README.md index b136670b0..64f0d427d 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/README.md +++ b/sensors/positioning/sensorhub-driver-nmeaais/README.md @@ -21,6 +21,8 @@ Each AIS transmission is a comma-delimited NMEA sentence: | Fill bits | `0` | Padding bits added to final payload | | Checksum | `6A` | NMEA XOR checksum | +**Supported sentence types:** This driver only processes sentences beginning with `!AIVDM` (messages received from other vessels) or `!AIVDO` (own-vessel transmissions). All other NMEA sentences arriving on the configured port — such as GPS fix sentences (`$GPGGA`, `$GPRMC`) — are silently ignored. This means the driver can safely share a UDP port carrying mixed NMEA traffic without producing errors or garbled output. + --- ## Hardware Requirements @@ -28,15 +30,23 @@ Each AIS transmission is a comma-delimited NMEA sentence: - [ShipXplorer AIS Dongle](https://www.shipxplorer.com/ais-dongle) (or any RTL-SDR / AIS receiver) - [ShipXplorer AIS Antenna](https://www.shipxplorer.com/ais-antenna) (or suitable VHF antenna) -## Software Requirements +## External Software Requirements +This driver does not directly receive or decode AIS radio signals from SDR hardware. +Instead, it expects already-decoded NMEA 0183 AIS messages to be forwarded to the driver over a supported network interface +Because of this, users must run external AIS decoding or forwarding software capable of: -- [AIS-Catcher](https://jvde-github.github.io/AIS-catcher-docs/) — decodes RTL-SDR radio input and forwards NMEA sentences over UDP +- Receiving AIS signals from SDR hardware or another AIS source +- Decoding the AIS transmissions into NMEA 0183 AIS sentences +- Forwarding the decoded messages to this OSH driver +Examples of compatible software include: +- [AIS-Catcher](https://jvde-github.github.io/AIS-catcher-docs/) — decodes RTL-SDR radio input and forwards NMEA sentences over UDP +- AIS Dispatcher --- ## Setup -### 1. Install and run AIS-Catcher +### 1. Install and run AIS-Catcher (or other AIS decoding software) Start AIS-Catcher and forward sentences to your OSH node over UDP: @@ -278,3 +288,6 @@ The following AIS message types are received and parsed by the radio layer but a | 27 | Long-Range AIS Broadcast | Class A/B position report for vessels outside base station coverage; not currently implemented | All received sentences — including the above types — are available in the raw messages output for custom processing or logging. + +## Additional Resources +- [USCG Navigation Center](https://www.navcen.uscg.gov/ais-messages) \ No newline at end of file From 09dce8b0a85d65e7761e76da9a19ceca702e0b3f Mon Sep 17 00:00:00 2001 From: BillBrown341 Date: Tue, 26 May 2026 08:57:41 -0500 Subject: [PATCH 14/14] added catch for non-AIS NMEA messages --- .../java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java index 2164ce285..b2fa6beeb 100644 --- a/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java +++ b/sensors/positioning/sensorhub-driver-nmeaais/src/main/java/org/sensorhub/impl/sensor/nmeaais/NmeaAisDriver.java @@ -154,7 +154,8 @@ public void doStart() throws SensorHubException { if (b == '\n') { String line = sb.toString().trim(); sb.setLength(0); - if (!line.isEmpty()) { + if (!line.isEmpty() && + (line.startsWith("!AIVDM") || line.startsWith("!AIVDO"))) { publishRawMessage(line); byte[] bytes = (line + "\n").getBytes(); pipedOut.write(bytes);