How to Test NMEA Speaking Hardware Devices

Page content

The Hardware Testing Challenge

When developing hardware, the challenging phase begins after the prototype is designed and PCBs are manufactured. The easy part—conceptualization and board design—is behind you. What follows is infinitely harder:

  • Making boards electrically alive and verifying proper operation with electrical jigs and test equipment
  • Programming and validating firmware behavior
  • Testing the complete system’s functionality under real-world conditions
  • Building the infrastructure—both hardware and software—to support comprehensive testing

For devices communicating via NMEA protocols, this challenge is compounded by the complexity of marine electronics standards and the difficulty of simulating real maritime data streams.

Understanding NMEA Protocols

NMEA (National Marine Electronics Association) protocols define how marine electronics communicate. There are three main standards, each with different characteristics:

NMEA 0183: The Text-Based Standard

NMEA 0183 is the legacy standard you’ll find on many boats. It uses plain ASCII text sentences transmitted serially over RS-422 connections, typically at 4,800 or 9,600 baud. Common sentence types include:

  • GGA: Global Positioning System fix data
  • RMC: Recommended Minimum Navigation Information
  • VTG: Track Made Good and Ground Speed
  • MWV: Wind Speed and Angle
  • VHW: Water Speed and Heading
  • MDA: Meteorological Composite

The human-readable nature of NMEA 0183 makes it easy to debug but bandwidth-inefficient.

NMEA 2000: The Modern Binary Standard

NMEA 2000 uses a CAN bus architecture with binary encoding. Data is organized into PGNs (Parameter Group Numbers), each carrying specific types of information:

  • 127250: Vessel Heading
  • 128259: Speed (through water and over ground)
  • 128267: Water Depth
  • 129025: Position (rapid update)
  • 129026: Course Over Ground & Speed Over Ground
  • 129029: GNSS Position Data
  • 130306: Wind Data
  • 130310: Environmental Parameters

NMEA 2000 is more efficient, faster, and better suited for modern boat systems, but requires proper CAN bus setup and binary protocol handling.

NmeaJson: JSON-Based Implementation

NmeaJson provides a JSON-based approach to maritime data, offering a balance between human readability (like NMEA 0183) and structured data (like NMEA 2000). This makes it ideal for IP-based systems and modern IoT applications.

Testing NMEA Devices Over IP

When your device can communicate over IP instead of traditional serial or CAN connections, testing becomes dramatically simpler. You can:

  • Run tests without specialized hardware
  • Simulate devices in software
  • Test protocol conversion between standards
  • Validate data integrity across different encoding schemes
  • Create reproducible test scenarios

This is where two specialized Rust crates become invaluable: nmea_codec and nmea_router.

Introducing nmea_codec: Pure Protocol Handling

The nmea_codec crate is a comprehensive library for encoding and decoding NMEA protocols. It focuses purely on codec operations—converting between raw bytes/text and structured data—without any networking complexity.

Key Characteristics

Three Independent Modules:

  • nmea0183: Text-based protocol handling
  • nmea2000: Binary CAN bus protocol with PGN support
  • nmea_json: JSON-based implementation

Design Philosophy:

  • Zero-copy efficient memory management for embedded systems
  • Synchronous API (no async runtime required)
  • Pure codec operations—no business logic or networking
  • Protocol-agnostic approach for maximum flexibility

Supported NMEA 2000 PGNs

The codec handles a comprehensive set of PGNs:

  • 127250, 128259, 128267, 128776-128778 (Windlass control)
  • 129025, 129026, 129029 (Position and navigation)
  • 130306, 130310 (Environmental and wind data)

Why This Architecture?

By separating protocol encoding/decoding from networking and routing logic, nmea_codec becomes:

  • Lightweight for embedded systems
  • Testable without external dependencies
  • Reusable across multiple applications
  • Suitable for hardware-in-the-loop testing

Introducing nmea_router: Flexible Message Distribution

The nmea_router crate takes protocol handling to the next level, providing configurable routing and protocol conversion for testing scenarios.

What nmea_router Does

It accepts messages from multiple input sources, converts them between NMEA standards, and broadcasts to multiple output destinations:

Input Sources:

  • NMEA2K (CAN bus)
  • UDP multicast or unicast
  • TCP connections
  • Unix sockets
  • NMEA 0183 (serial)

Output Destinations:

  • UDP (unicast or multicast)
  • TCP clients
  • NMEA 0183 (serial)
  • NMEA2K (CAN bus)

Configuration-Driven Approach

Instead of hardcoding routing logic, nmea_router uses JSON configuration files. This allows you to:

  • Change routing behavior without recompilation
  • Test multiple scenarios by swapping configuration files
  • Share configurations with team members
  • Version control test scenarios

Testing Scenarios with NMEA Devices

Scenario 1: Device as Message Source

Your DUT (Device Under Test) generates NMEA messages that need validation. The router accepts these messages and routes them to:

  • A logging service that records all messages
  • A validation service that checks for protocol compliance
  • A visualization tool for real-time monitoring
  • Another device that needs to process the data

Configuration Example:

{
  "listeners": [
    {
      "type": "tcp",
      "address": "127.0.0.1:10000",
      "protocol": "Nmea0183"
    }
  ],
  "broadcasters": [
    {
      "type": "udp",
      "address": "127.0.0.1:5005",
      "protocol": "Nmea0183"
    },
    {
      "type": "file",
      "path": "./nmea_log.txt",
      "protocol": "Nmea0183"
    }
  ]
}

In this setup, your device connects via TCP, and its messages are simultaneously broadcast via UDP and logged to a file.

Scenario 2: Device as Message Target

Your DUT consumes NMEA messages and you need to test its response to various data. The router generates test messages from different sources and delivers them to the device:

  • Simulate a GPS source with position data (GGA sentences)
  • Simulate wind instruments with wind angle and speed (MWV sentences)
  • Simulate depth sounders with water depth (DBT sentences)
  • Test device behavior under various data conditions

Configuration Example:

{
  "listeners": [
    {
      "type": "tcp",
      "address": "127.0.0.1:10001",
      "protocol": "Nmea0183"
    }
  ],
  "broadcasters": [
    {
      "type": "tcp",
      "address": "192.168.1.100:4000",
      "protocol": "Nmea0183"
    }
  ]
}

Test data flows in via TCP on port 10001, gets routed to your device at 192.168.1.100:4000.

Scenario 3: Protocol Conversion Testing

This is where nmea_router becomes particularly powerful. You can:

  • Accept NMEA 0183 messages from legacy equipment
  • Convert them to NMEA 2000 binary format
  • Broadcast to modern systems
  • Verify data integrity through the conversion process
{
  "listeners": [
    {
      "type": "serial",
      "port": "/dev/ttyUSB0",
      "baudrate": 9600,
      "protocol": "Nmea0183"
    }
  ],
  "broadcasters": [
    {
      "type": "udp",
      "address": "127.0.0.1:2000",
      "protocol": "Nmea2000",
      "binary_encoding": true
    }
  ]
}

This configuration reads NMEA 0183 from a serial port and broadcasts the converted NMEA 2000 binary data via UDP.

Proprietary Handlers Registration: Extending NMEA with Vendor-Specific Features

While NMEA 0183 and NMEA 2000 cover the vast majority of marine electronics use cases, real-world devices often include proprietary extensions that vendors use to provide specialized features. A fish finder might have custom sonar data formats, an autopilot might report internal state through vendor-specific sentences, or an anchor windlass system might transmit control and status messages using proprietary formats.

The challenge is supporting these vendor-specific extensions while maintaining compatibility with standard NMEA message routing. This is where nmea_router’s proprietary handler system comes in.

The Problem: Vendor Extensions Beyond Standards

Standard NMEA protocols provide a foundation, but they don’t cover everything:

  • Vendor-Specific Sentences: Equipment manufacturers extend NMEA 0183 with proprietary sentence types (e.g., $PZWLC for anchor windlass control, $PSRF for SiRF, $GPUBL for u-blox)
  • Custom PGN Extensions: Vendors might define custom PGNs (Parameter Group Numbers) in the private range (130816-131072) for device-specific telemetry
  • Device-Specific States: Internal equipment state that doesn’t map to standard NMEA fields
  • Vendor Metadata: Calibration data, firmware versions, or diagnostic information

Without extensibility, your router would either ignore these messages (losing data) or fail parsing (breaking the entire message stream).

The Solution: Pluggable Handler Architecture

nmea_router provides a comprehensive handler registration system that allows vendors to implement custom message types. By implementing the ProprietaryHandler trait, vendors can register handlers that decode and encode their proprietary sentences, integrating them seamlessly into the message routing pipeline.

How Proprietary Handlers Work

The system uses a two-tier architecture:

Tier 1 - Codec Parsing: nmea_codec decodes incoming NMEA 0183 sentences. When it encounters a sentence starting with $P (proprietary marker), it parses it into a generic Proprietary variant containing the tag, prefix, and comma-separated fields.

Tier 2 - Specialized Handler: nmea_router’s ProprietaryRegistry holds handlers implementing the ProprietaryHandler trait. These handlers:

  1. Receive the generic proprietary data from nmea_codec
  2. Decode it into structured, type-safe data
  3. Optionally convert it to JSON for easier inspection and routing
  4. Can re-encode it back to NMEA sentences for different output formats
Raw sentence        → nmea_codec parses        → ProprietaryRegistry
$PZWLC,0,1,...     → Proprietary{tag, fields} → WindlassHandler.decode()
                                                → ProcessedPacket::Json

Practical Example: Windlass Handler

Imagine you’re testing an anchor windlass system that communicates via proprietary NMEA 0183 sentences. The manufacturer defines three sentence types:

  • $PZWLC - Windlass Control (send commands)
  • $PZWLO - Windlass Operating Status (operational feedback)
  • $PZWLM - Windlass Monitoring (voltage, current, diagnostics)

Here’s how you’d implement and register the handler:

Step 1: Implement the ProprietaryHandler Trait

use nmea_router::proprietary::ProprietaryHandler;
use async_trait::async_trait;
use nmea_codec::types::ProcessedPacket;

#[derive(Default)]
pub struct WindlassHandler;

#[async_trait]
impl ProprietaryHandler for WindlassHandler {
    fn prefix(&self) -> &str {
        "PZ"  // Sentence prefix for this vendor
    }

    fn tags(&self) -> Vec<&str> {
        vec!["PZWLC", "PZWLO", "PZWLM"]  // All sentence types this handler supports
    }

    async fn decode(&self, tag: &str, fields: &[String]) -> anyhow::Result<ProcessedPacket> {
        match tag {
            "PZWLC" => {
                // Parse windlass control data from comma-separated fields
                let windlass_id = fields.get(1)?.parse::<u8>()?;
                let direction = fields.get(2)?.parse::<u8>()?;
                let speed = fields.get(5)?.parse::<u8>()?;

                // Create structured data and convert to JSON
                let json = serde_json::json!({
                    "message_type": "windlass_control",
                    "windlass_id": windlass_id,
                    "direction": direction,
                    "speed": speed,
                });

                Ok(ProcessedPacket::Json(json))
            }
            "PZWLO" => {
                // Similar parsing for operating status
                // ...
            }
            "PZWLM" => {
                // Similar parsing for monitoring data
                // ...
            }
            _ => Err(anyhow::anyhow!("Unknown windlass tag: {}", tag))
        }
    }

    async fn encode(&self, tag: &str, json: &serde_json::Value) -> anyhow::Result<String> {
        match tag {
            "PZWLC" => {
                let windlass_id = json["windlass_id"].as_u64().unwrap_or(0);
                let direction = json["direction"].as_u64().unwrap_or(0);
                let speed = json["speed"].as_u64().unwrap_or(0);

                // Construct the NMEA sentence with checksum
                let sentence = format!("$PZWLC,0,{},{},0,0,{},0,0,0,0,0.0,0",
                    windlass_id, direction, speed);

                // Add NMEA checksum
                let checksum = calculate_checksum(&sentence);
                Ok(format!("{}*{:02X}", sentence, checksum))
            }
            _ => Err(anyhow::anyhow!("Cannot encode tag: {}", tag))
        }
    }
}

Step 2: Register the Handler in Your Router

// In your nmea_router main.rs
let bus = NmeaBus::new(config);

// Get the proprietary registry and register the handler
let prop_registry = bus.proprietary_registry();
let mut registry = prop_registry.lock().unwrap();
registry.register(Arc::new(WindlassHandler::default()));

info!("Registered windlass handler for PZWLC, PZWLO, PZWLM");

Message Flow in Practice

When your windlass device sends data:

Raw input: $PZWLC,0,1,0,0,50,0,0,0,0,5.0,0*1A

1. TCP listener receives the sentence
2. nmea_codec parses it: Proprietary { tag: "PZWLC", prefix: "PZ", fields: ["0", "1", ...] }
3. NmeaBus publishes to registered MessageRegistry implementations
4. For specialized processing: ProprietaryRegistry.decode() is called
5. WindlassHandler.decode() converts to JSON:
   {
     "message_type": "windlass_control",
     "windlass_id": 1,
     "direction": 0,
     "speed": 50
   }
6. The JSON can be:
   - Logged for analysis
   - Routed to other systems
   - Re-encoded to different formats
   - Used to trigger automated test sequences

Integration Points: Where Handlers Connect

At the Parser Level (nmea_codec):

  • Handles the initial decode of proprietary sentences
  • Extracts prefix, tag, and fields automatically
  • No vendor-specific logic needed at this level

At the Handler Level (nmea_router):

  • Implementations use the generic data from nmea_codec
  • Convert to strongly-typed, domain-specific structures
  • Optionally emit JSON for easier handling in tests

At the Message Registry Level (nmea_router):

  • Registries can listen to proprietary messages
  • Can filter by sentence tag or prefix
  • Enables event-driven testing workflows

Testing Benefits of Extensible Handlers

This architecture provides several advantages for hardware testing:

Vendor Implementation Flexibility:

  • Vendors implement handlers in their own crates
  • No changes needed to nmea_router core
  • Multiple vendors can coexist without conflicts
  • Handlers can be versioned independently from the router

Type-Safe Processing:

  • Decode vendor data into Rust types (not just strings)
  • Compile-time validation of message structure
  • IDE autocomplete and refactoring support
  • Clear error handling for malformed messages

Complete Data Coverage:

  • Capture all device output, including proprietary messages
  • All data (standard and vendor-specific) in unified event stream
  • Easier to correlate events across different message types

Testing Workflows:

  • Send test sequences: Implement encode() to generate NMEA sentences
  • Receive and validate: Implement decode() to verify device output
  • Simulate multiple vendors: Register multiple handlers simultaneously
  • Test protocol conversion: Convert proprietary data between formats

Graceful Degradation:

  • Unknown proprietary sentences are parsed as generic Proprietary messages
  • System doesn’t crash on unexpected vendor formats
  • Handlers can be added retroactively
  • Test harness continues operating even with unrecognized proprietary data

Enabling Proprietary Handlers in Your Tests

To add proprietary handler support:

  1. Create a Handler Crate: Implement ProprietaryHandler for your vendor’s messages

    cargo new my_vendor_nmea_handlers
    
  2. Implement the Trait: Define decode() and encode() methods for each sentence type

  3. Register in Your Test Setup:

    let handler = Arc::new(MyVendorHandler::default());
    bus.proprietary_registry().lock().unwrap().register(handler);
    
  4. Process Events: Register a MessageRegistry to handle on_proprietary() callbacks:

    pub async fn on_proprietary(&self, tag: &str, fields: &[String]) {
        // Log, validate, or route the proprietary message
        println!("Received {}: {:?}", tag, fields);
    }
    

Real-World Example: Testing a Navigation System

Suppose you’re testing a marine navigation system that integrates:

  • Standard NMEA 0183 GPS (GGA, RMC)
  • Proprietary windlass control ($PZWLC, $PZWLO)
  • Proprietary autopilot status ($PAPC for another vendor)

Your test suite would:

  1. Register handlers for both proprietary systems
  2. Send simulated GPS fixes via standard NMEA
  3. Send windlass commands and verify the response via $PZWLC encoding
  4. Monitor windlass status via $PZWLO decoding
  5. Verify the navigation system correctly integrates all three data sources

All processing happens in software—no need for actual windlass hardware or GPS receiver on your test bench.

Binary vs. JSON Encoding for NMEA 2000

When routing NMEA 2000 messages over IP, you have choices:

Binary Mode:

  • ~12-16 bytes per message
  • Highly efficient for high-frequency data streams
  • Ideal for production use
  • Requires binary protocol understanding for debugging

JSON Mode:

  • ~100-500 bytes per message
  • Human-readable for development and debugging
  • Easier to visualize and log
  • Better for development and testing phases

The router automatically detects the input format, so you can send either binary or JSON encoded messages and the system adapts.

Setting Up Your Test Infrastructure

Step 1: Install the Tools

Add nmea_router to your testing environment. As the crates are developed, they’ll provide both library and CLI interfaces.

Step 2: Create Test Configurations

Write JSON configuration files for each test scenario:

  • Normal operation with valid data
  • Edge cases (boundary values, missing fields)
  • Error conditions (malformed messages, timing issues)
  • Protocol conversion scenarios

Step 3: Simulate Message Sources

Create simple message generators for common scenarios:

// Pseudo-code example
let position_msg = nmea0183::sentence::GGA {
    timestamp: "121356",
    latitude: 49.2827,
    ns_indicator: 'N',
    longitude: 123.1207,
    ew_indicator: 'W',
    gps_quality: 1,
    num_satellites: 12,
    hdop: 0.9,
    altitude: 35.0,
    // ... other fields
};

// Send to router via configured interface
send_message_to_device(position_msg);

Step 4: Validate Device Response

Monitor the device’s output through the router’s logging and monitoring capabilities. Verify:

  • Messages are received and processed
  • Output messages are correctly formatted
  • Data values make logical sense
  • Timing and sequencing is appropriate

Benefits of This Testing Approach

Removes Hardware Dependencies:

  • Test without physical NMEA devices present
  • No need for specialized marine electronics on your test bench
  • Simulate failure modes that are dangerous or expensive to reproduce

Enables Continuous Integration:

  • Automated testing of NMEA protocol handling
  • Protocol conversion validation in CI/CD pipelines
  • Regression testing for firmware updates

Supports Protocol Flexibility:

  • Test devices that mix multiple NMEA standards
  • Verify protocol conversion accuracy
  • Validate cross-standard data consistency

Accelerates Development:

  • Parallel testing of multiple scenarios
  • No waiting for real-world conditions (wind, waves, etc.)
  • Easy reproduction of specific conditions

Improves Debugging:

  • All messages logged for analysis
  • Human-readable JSON option for complex scenarios
  • Clear visibility into protocol behavior

Real-World Example: Testing a Navigation Display

Imagine you’re developing a marine navigation display that needs to work with:

  • Legacy boats with NMEA 0183 (serial GPS, wind, depth)
  • Modern boats with NMEA 2000 (CAN bus)
  • IP-connected shore stations

Your test suite would:

  1. Accept NMEA 0183 GPS data via TCP and route it to the display
  2. Convert NMEA 0183 wind data to NMEA 2000 and send it
  3. Accept NMEA 2000 binary position data via UDP and validate it displays correctly
  4. Test that the display correctly handles simultaneous data from multiple protocols
  5. Verify protocol conversion maintains data accuracy within acceptable tolerances

All of this happens in software, without a single boat or specialized hardware on the test bench.

Conclusion

Hardware testing doesn’t have to mean struggling with physical devices, specialized jigs, and unreproducible conditions. By moving NMEA communication to IP-based protocols and leveraging specialized tools like nmea_codec and nmea_router, you can build a comprehensive software-based testing infrastructure.

The key insight is that protocol handling—encoding, decoding, and routing—is pure logic that can be thoroughly tested in software. This approach:

  • Reduces development time by orders of magnitude
  • Enables automated testing and CI/CD integration
  • Allows simulation of edge cases and failure modes
  • Provides clear visibility into protocol behavior
  • Supports the complexity of modern marine electronics with multiple standards

Whether you’re building navigation systems, instrumentation, or any device that speaks NMEA protocols, this testing approach gives you the tools to build it right the first time—without the hardware headaches.

The era of software-in-the-loop testing for marine electronics is here. The question is: are you ready to test smarter?