How to Test NMEA Speaking Hardware Devices
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 handlingnmea2000: Binary CAN bus protocol with PGN supportnmea_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.,
$PZWLCfor anchor windlass control,$PSRFfor SiRF,$GPUBLfor 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:
- Receive the generic proprietary data from
nmea_codec - Decode it into structured, type-safe data
- Optionally convert it to JSON for easier inspection and routing
- 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
Proprietarymessages - 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:
-
Create a Handler Crate: Implement
ProprietaryHandlerfor your vendor’s messagescargo new my_vendor_nmea_handlers -
Implement the Trait: Define
decode()andencode()methods for each sentence type -
Register in Your Test Setup:
let handler = Arc::new(MyVendorHandler::default()); bus.proprietary_registry().lock().unwrap().register(handler); -
Process Events: Register a
MessageRegistryto handleon_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:
- Register handlers for both proprietary systems
- Send simulated GPS fixes via standard NMEA
- Send windlass commands and verify the response via
$PZWLCencoding - Monitor windlass status via
$PZWLOdecoding - 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:
- Accept NMEA 0183 GPS data via TCP and route it to the display
- Convert NMEA 0183 wind data to NMEA 2000 and send it
- Accept NMEA 2000 binary position data via UDP and validate it displays correctly
- Test that the display correctly handles simultaneous data from multiple protocols
- 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?