diff --git a/.gitignore b/.gitignore index 92463fca..4dcef64c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,9 @@ tests/py/*.bin tests/ts/*.bin *_test_data.bin +# Wireshark dissector test files +wireshark/sample_packets/ + # Debug and temporary files debug*.c debug*.exe diff --git a/README.md b/README.md index 3467343d..065dc745 100644 --- a/README.md +++ b/README.md @@ -807,6 +807,10 @@ for (size_t i = 0; i < encoder.size(); i++) { } ``` +## Wireshark Dissector (Experimental) + +An experimental Wireshark Lua dissector is available for protocol analysis and debugging of struct-frame packets. See [wireshark/README.md](wireshark/README.md) for installation and usage instructions. + ## Additional Documentation - **[Array Implementation Guide](ARRAY_IMPLEMENTATION.md)** - Documentation of array features, syntax, and generated code examples across all languages diff --git a/wireshark/CRC_IMPLEMENTATION.md b/wireshark/CRC_IMPLEMENTATION.md new file mode 100644 index 00000000..cb7f248c --- /dev/null +++ b/wireshark/CRC_IMPLEMENTATION.md @@ -0,0 +1,148 @@ +# CRC Calculation in Struct Frame + +This document explains how the Fletcher-16 checksum is calculated in the Struct Frame protocol. + +## Fletcher-16 Algorithm + +The Fletcher-16 checksum used by Struct Frame is a simple two-byte checksum algorithm: + +```python +def fletcher16(data): + """Calculate Fletcher-16 checksum.""" + sum1 = 0 + sum2 = 0 + for byte in data: + sum1 = (sum1 + byte) % 256 + sum2 = (sum2 + sum1) % 256 + return sum1, sum2 +``` + +## What Data Is Included in the CRC + +The CRC is calculated over **all frame data after the start bytes, excluding the CRC itself**. + +### BasicDefault (Profile.STANDARD) + +Frame structure: `[0x90] [0x71] [LEN] [MSG_ID] [PAYLOAD...] [CRC1] [CRC2]` + +CRC is calculated over: `[LEN] [MSG_ID] [PAYLOAD...]` +- Start offset: 2 (after the two start bytes) +- End offset: frame_length - 2 (before the CRC bytes) + +**Example:** +``` +Frame: 90 71 04 2a 01 02 03 04 38 fe + │ │ └───── CRC data ──────┘ └─┘ + │ │ CRC + └───┘ Start bytes (excluded from CRC) + +CRC input: [0x04, 0x2A, 0x01, 0x02, 0x03, 0x04] +Fletcher-16: sum1=0x38, sum2=0xFE +``` + +### BasicExtended (Profile.BULK) + +Frame structure: `[0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD...] [CRC1] [CRC2]` + +CRC is calculated over: `[LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD...]` +- Start offset: 2 (after the two start bytes) +- End offset: frame_length - 2 (before the CRC bytes) + +**Example:** +``` +Frame: 90 74 04 00 01 2a 01 02 03 04 39 0c + │ │ └──────── CRC data ────────┘ └─┘ + │ │ CRC + └───┘ Start bytes (excluded from CRC) + +CRC input: [0x04, 0x00, 0x01, 0x2A, 0x01, 0x02, 0x03, 0x04] +Fletcher-16: sum1=0x39, sum2=0x0C +``` + +### BasicExtendedMultiSystemStream (Profile.NETWORK) + +Frame structure: `[0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD...] [CRC1] [CRC2]` + +CRC is calculated over: `[SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD...]` +- Start offset: 2 (after the two start bytes) +- End offset: frame_length - 2 (before the CRC bytes) + +**Example:** +``` +Frame: 90 78 05 01 02 04 00 01 2a 01 02 03 04 41 5f + │ │ └────────────── CRC data ──────────┘ └─┘ + │ │ CRC + └───┘ Start bytes (excluded from CRC) + +CRC input: [0x05, 0x01, 0x02, 0x04, 0x00, 0x01, 0x2A, 0x01, 0x02, 0x03, 0x04] +Fletcher-16: sum1=0x41, sum2=0x5F +``` + +### TinyMinimal (Profile.SENSOR) + +Frame structure: `[0x70] [MSG_ID] [PAYLOAD...]` + +This profile has **no CRC**. The minimal overhead is achieved by omitting error detection. + +## Implementation Notes + +### In the Wireshark Dissector + +The dissector calculates the CRC over the same range: + +```lua +-- For Basic frames +local crc_start_offset = 2 -- After start bytes +local crc_data_len = offset - crc_start_offset +local crc_data = buffer(crc_start_offset, crc_data_len) +local calc_crc1, calc_crc2 = fletcher16(crc_data) +``` + +### In the Test Generator + +The test packet generator follows the same pattern: + +```python +# For BasicDefault +frame = bytearray([0x90, 0x71]) # Start bytes +frame.append(len(payload)) # Length +frame.append(msg_id) # Message ID +frame.extend(payload) # Payload data + +# Calculate CRC over everything after start bytes +crc_data = bytes([len(payload), msg_id]) + payload +crc1, crc2 = fletcher16(crc_data) + +frame.extend([crc1, crc2]) # Add CRC +``` + +## Validation + +The `validate_packets.py` script verifies that CRC calculations are correct by: + +1. Reading the generated binary packet +2. Extracting the CRC data range (after start bytes, before CRC) +3. Calculating Fletcher-16 over that range +4. Comparing with the received CRC bytes + +All tests pass, confirming the implementation is correct. + +## Reference Implementation + +The authoritative implementation is in the generated Python parser code (`parser.py`): + +```python +# From generated code +crc_start = header_size # 2 for Basic, 1 for Tiny +crc_end = len(self.buffer) - config.crc_bytes +calc_crc = fletcher_checksum(self.buffer, crc_start, crc_end) +recv_crc = (self.buffer[-2], self.buffer[-1]) +if calc_crc != recv_crc: + # CRC validation failed + self.reset() +``` + +This confirms that: +- CRC starts after the header (start bytes) +- CRC ends before the CRC bytes themselves +- CRC includes: payload header fields + message ID + payload data diff --git a/wireshark/README.md b/wireshark/README.md new file mode 100644 index 00000000..4fd37534 --- /dev/null +++ b/wireshark/README.md @@ -0,0 +1,267 @@ +# Wireshark Dissector for Struct Frame + +This directory contains a Wireshark Lua dissector for the Struct Frame protocol. The dissector can decode all standard frame format profiles used in struct-frame communication. + +## Features + +- **Automatic Protocol Detection**: Detects Basic and Tiny frame formats based on start bytes +- **All Standard Profiles Supported**: + - Standard (BasicDefault) - General Serial/UART + - Sensor (TinyMinimal) - Low-Bandwidth + - IPC (NoneMinimal) - Trusted Inter-Process (requires configuration) + - Bulk (BasicExtended) - Large Data Transfers + - Network (BasicExtendedMultiSystemStream) - Multi-Node Mesh +- **Field-by-field Decoding**: Displays all frame fields including: + - Start bytes (0x90 for Basic, 0x70-0x78 for Tiny) + - Sequence numbers + - System/Component IDs for routing + - Length fields (8-bit or 16-bit) + - Package IDs + - Message IDs + - Payload data + - CRC checksums with validation +- **CRC Validation**: Calculates and validates Fletcher-16 checksums + +## Installation + +### Method 1: User Plugin Directory (Recommended) + +1. Copy `struct_frame.lua` to your Wireshark plugins directory: + - **Windows**: `%APPDATA%\Wireshark\plugins\` + - **Linux**: `~/.local/lib/wireshark/plugins/` + - **macOS**: `~/.wireshark/plugins/` + +2. Create the directory if it doesn't exist: + ```bash + # Linux/macOS + mkdir -p ~/.local/lib/wireshark/plugins/ + cp struct_frame.lua ~/.local/lib/wireshark/plugins/ + + # Or for global installation (requires sudo) + # sudo cp struct_frame.lua /usr/lib/wireshark/plugins/ + ``` + +3. Restart Wireshark + +### Method 2: Load from Command Line + +You can also load the dissector directly when starting Wireshark: + +```bash +wireshark -X lua_script:struct_frame.lua +``` + +## Verification + +To verify the dissector is loaded correctly: + +1. Open Wireshark +2. Go to **Help → About Wireshark → Plugins** +3. Look for `struct_frame.lua` in the list +4. Check the console output for "Struct Frame dissector loaded successfully" + +Alternatively, check the Wireshark console: +- Go to **View → Internals → Lua** +- Look for the success message + +## Usage + +### Automatic Detection + +The dissector will automatically detect Struct Frame packets in: +- **UDP** traffic +- **TCP** traffic +- Packets with User DLT 147 + +It looks for the characteristic start bytes: +- `0x90` followed by `0x70-0x78` (Basic frames) +- `0x70-0x78` as the first byte (Tiny frames) + +### Capture Filter + +To capture only Struct Frame traffic, use: + +``` +udp port 14550 +``` + +(Replace `14550` with your actual port number) + +### Display Filter + +Use these display filters to view specific Struct Frame traffic: + +``` +# All Struct Frame packets +struct_frame + +# Specific profile +struct_frame.profile_name contains "Standard" + +# Specific message ID +struct_frame.message_id == 42 + +# Invalid CRC +struct_frame.crc_status contains "Invalid" + +# Specific system ID (for Network profile) +struct_frame.system_id == 1 +``` + +### Decoding Existing Captures + +If you have existing packet captures: + +1. Open the PCAP file in Wireshark +2. Right-click on a packet +3. Select **Decode As...** +4. Choose **Struct Frame** as the protocol + +## Frame Format Reference + +### Basic Frame Profiles + +| Profile | Start Bytes | Overhead | Structure | +|---------|-------------|----------|-----------| +| BasicDefault (Standard) | `0x90 0x71` | 6 bytes | `[0x90] [0x71] [LEN] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]` | +| BasicExtended (Bulk) | `0x90 0x74` | 8 bytes | `[0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]` | +| BasicExtendedMultiSystemStream (Network) | `0x90 0x78` | 11 bytes | `[0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]` | + +### Tiny Frame Profiles + +| Profile | Start Byte | Overhead | Structure | +|---------|------------|----------|-----------| +| TinyMinimal (Sensor) | `0x70` | 2 bytes | `[0x70] [MSG_ID] [PAYLOAD]` | +| TinyDefault | `0x71` | 5 bytes | `[0x71] [LEN] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]` | + +### Payload Type Encoding + +The second start byte (for Basic) or the single start byte (for Tiny) encodes the payload type: + +- `0x70` = Minimal +- `0x71` = Default +- `0x72` = ExtendedMsgIds +- `0x73` = ExtendedLength +- `0x74` = Extended +- `0x75` = SysComp +- `0x76` = Seq +- `0x77` = MultiSystemStream +- `0x78` = ExtendedMultiSystemStream + +## Example Packet Dissection + +### Standard Profile (BasicDefault) + +``` +Frame: [0x90] [0x71] [0x04] [0x2A] [0x01, 0x02, 0x03, 0x04] [0x7F] [0x8A] + +Dissected as: + Struct Frame Protocol + Start Byte 1: 0x90 + Start Byte 2: 0x71 + Header Type: Basic + Payload Type: Default + Profile Name: Standard (General Serial/UART) + Length: 4 + Message ID: 42 + Payload Data: 01:02:03:04 + CRC Byte 1: 0x7F + CRC Byte 2: 0x8A + CRC Status: Valid +``` + +### Network Profile (BasicExtendedMultiSystemStream) + +``` +Frame: [0x90] [0x78] [0x05] [0x01] [0x02] [0x04] [0x00] [0x01] [0x2A] [0x01, 0x02, 0x03, 0x04] [CRC1] [CRC2] + +Dissected as: + Struct Frame Protocol + Start Byte 1: 0x90 + Start Byte 2: 0x78 + Header Type: Basic + Payload Type: ExtendedMultiSystemStream + Profile Name: Network (Multi-Node Mesh) + Sequence Number: 5 + System ID: 1 + Component ID: 2 + Length Low: 4 + Length High: 0 + Length: 4 (generated) + Package ID: 1 + Message ID: 42 + Payload Data: 01:02:03:04 + CRC Byte 1: ... + CRC Byte 2: ... + CRC Status: Valid +``` + +## Troubleshooting + +### Dissector Not Loading + +1. Check Wireshark console for errors: **View → Internals → Lua** +2. Verify the file is in the correct plugins directory +3. Make sure the file has `.lua` extension +4. Restart Wireshark completely + +### Packets Not Being Decoded + +1. Verify the packets have the correct start bytes +2. Check if another dissector is claiming the packets first +3. Use "Decode As..." to force Struct Frame dissection +4. Enable heuristic dissectors: **Analyze → Enabled Protocols** and check "struct_frame" + +### CRC Always Shows Invalid + +1. Verify you're using the correct profile (CRC is only present in certain payload types) +2. Check that the captured data is complete and not corrupted +3. Compare with known-good test data + +## Creating Test Data + +To create test PCAP files for validation: + +1. Generate struct-frame packets using the Python/TypeScript SDK +2. Capture traffic with: + ```bash + tcpdump -i any -w struct_frame_test.pcap udp port 14550 + ``` +3. Open in Wireshark to verify dissection + +Example Python code to generate test packets: +```python +from struct_frame.frame_formats import Profile, get_profile + +# Get the Standard profile +profile = get_profile(Profile.STANDARD) + +# Create a test message +# [0x90] [0x71] [0x04] [0x2A] [0x01, 0x02, 0x03, 0x04] [CRC1] [CRC2] +``` + +## Limitations + +- **None frames** (no start bytes) cannot be auto-detected and require manual configuration +- **Third-party protocols** (UBX, MAVLink) are not yet implemented in the dissector +- Large payloads (>64KB) may cause performance issues in Wireshark + +## Contributing + +To add support for additional frame formats or improve the dissector: + +1. Edit `struct_frame.lua` +2. Add payload type definitions to `payload_types` and `payload_structures` +3. Update the field parsing logic in the main dissector function +4. Test with sample packets +5. Submit a pull request to the struct-frame repository + +## References + +- [Struct Frame Documentation](https://struct-frame.mylonics.com/) +- [Framing Guide](https://struct-frame.mylonics.com/user-guide/framing/) +- [Wireshark Lua API](https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html) + +## License + +This dissector is part of the struct-frame project and is licensed under the same terms as the main project. diff --git a/wireshark/generate_test_packets.py b/wireshark/generate_test_packets.py new file mode 100755 index 00000000..fb06b5eb --- /dev/null +++ b/wireshark/generate_test_packets.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Generate sample Struct Frame packets for testing the Wireshark dissector. + +This script creates example packets for each standard profile and saves them +as raw binary files and PCAP files that can be opened in Wireshark. +""" + +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from struct_frame.frame_formats import Profile, get_profile + + +def fletcher16(data): + """Calculate Fletcher-16 checksum.""" + sum1 = 0 + sum2 = 0 + for byte in data: + sum1 = (sum1 + byte) % 256 + sum2 = (sum2 + sum1) % 256 + return sum1, sum2 + + +def create_basic_default_packet(msg_id=42, payload=b'\x01\x02\x03\x04'): + """ + Create a BasicDefault (Standard) profile packet. + Format: [0x90] [0x71] [LEN] [MSG_ID] [PAYLOAD] [CRC1] [CRC2] + """ + # Start bytes + frame = bytearray([0x90, 0x71]) + + # Length (payload only, not including header/crc) + frame.append(len(payload)) + + # Message ID + frame.append(msg_id) + + # Add payload + frame.extend(payload) + + # Calculate Fletcher-16 over length, msg_id, and payload + crc_data = bytes([len(payload), msg_id]) + payload + crc1, crc2 = fletcher16(crc_data) + + # Add CRC + frame.extend([crc1, crc2]) + + return bytes(frame) + + +def create_tiny_minimal_packet(msg_id=42, payload=b'\x01\x02\x03\x04'): + """ + Create a TinyMinimal (Sensor) profile packet. + Format: [0x70] [MSG_ID] [PAYLOAD] + """ + frame = bytearray([0x70]) + frame.append(msg_id) + frame.extend(payload) + return bytes(frame) + + +def create_basic_extended_packet(msg_id=42, pkg_id=1, payload=b'\x01\x02\x03\x04'): + """ + Create a BasicExtended (Bulk) profile packet. + Format: [0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2] + """ + # Start bytes + frame = bytearray([0x90, 0x74]) + + # Length (16-bit, little endian) + length = len(payload) + len_lo = length & 0xFF + len_hi = (length >> 8) & 0xFF + frame.extend([len_lo, len_hi]) + + # Package ID + frame.append(pkg_id) + + # Message ID + frame.append(msg_id) + + # Add payload + frame.extend(payload) + + # Calculate Fletcher-16 over length, pkg_id, msg_id, and payload + crc_data = bytes([len_lo, len_hi, pkg_id, msg_id]) + payload + crc1, crc2 = fletcher16(crc_data) + + # Add CRC + frame.extend([crc1, crc2]) + + return bytes(frame) + + +def create_basic_extended_multi_system_stream_packet( + msg_id=42, pkg_id=1, sys_id=1, comp_id=2, seq=5, payload=b'\x01\x02\x03\x04' +): + """ + Create a BasicExtendedMultiSystemStream (Network) profile packet. + Format: [0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2] + """ + # Start bytes + frame = bytearray([0x90, 0x78]) + + # Sequence number + frame.append(seq) + + # System ID and Component ID + frame.extend([sys_id, comp_id]) + + # Length (16-bit, little endian) + length = len(payload) + len_lo = length & 0xFF + len_hi = (length >> 8) & 0xFF + frame.extend([len_lo, len_hi]) + + # Package ID + frame.append(pkg_id) + + # Message ID + frame.append(msg_id) + + # Add payload + frame.extend(payload) + + # Calculate Fletcher-16 over everything after start bytes + crc_data = bytes([seq, sys_id, comp_id, len_lo, len_hi, pkg_id, msg_id]) + payload + crc1, crc2 = fletcher16(crc_data) + + # Add CRC + frame.extend([crc1, crc2]) + + return bytes(frame) + + +def create_pcap_file(packets, filename): + """ + Create a PCAP file with UDP packets containing the given payloads. + + This creates a minimal PCAP file that can be opened in Wireshark. + Uses User DLT 147 for struct-frame packets. + """ + import struct + import time + + # PCAP Global Header + # Magic number, version, timezone, accuracy, snaplen, network (User DLT 147) + pcap_header = struct.pack('= 0x70 and first_byte <= 0x78 then + -- Tiny frame (1 start byte) + header_type = "Tiny" + start_bytes = 1 + payload_type_offset = first_byte - 0x70 + else + -- Could be None frame (no start bytes) or unknown + -- TODO: None frames cannot be auto-detected since they have no start bytes. + -- They would require manual configuration or external context. + return + end + + -- Validate payload type + if payload_type_offset < 0 or payload_type_offset > 8 then + return + end + + local payload_type_name = payload_types[payload_type_offset] + if not payload_type_name then + return + end + + local structure = payload_structures[payload_type_offset] + if not structure then + return + end + + -- Create protocol tree + local subtree = tree:add(struct_frame_proto, buffer(), "Struct Frame Protocol") + + -- Determine profile name + local profile_name = header_type .. payload_type_name + local display_name = profile_names[profile_name] or profile_name + + -- Add header information + if header_type == "Basic" then + subtree:add(f_start1, buffer(0, 1)) + subtree:add(f_start2, buffer(1, 1)) + offset = 2 + elseif header_type == "Tiny" then + subtree:add(f_start1, buffer(0, 1)):append_text(" (Tiny frame)") + offset = 1 + end + + subtree:add(f_header_type, header_type) + subtree:add(f_payload_type, payload_type_name) + subtree:add(f_profile_name, display_name) + + -- Parse payload header fields according to structure + local payload_length = nil + local crc_start_offset = nil + + -- Sequence number + if structure.has_sequence then + if offset + 1 > length then return end + subtree:add(f_sequence, buffer(offset, 1)) + offset = offset + 1 + end + + -- System ID and Component ID + if structure.has_system_id then + if offset + 1 > length then return end + subtree:add(f_system_id, buffer(offset, 1)) + offset = offset + 1 + end + + if structure.has_component_id then + if offset + 1 > length then return end + subtree:add(f_component_id, buffer(offset, 1)) + offset = offset + 1 + end + + -- Length field + if structure.has_length then + if structure.length_bytes == 1 then + if offset + 1 > length then return end + payload_length = buffer(offset, 1):uint() + subtree:add(f_length, buffer(offset, 1)) + offset = offset + 1 + elseif structure.length_bytes == 2 then + if offset + 2 > length then return end + local len_lo = buffer(offset, 1):uint() + local len_hi = buffer(offset + 1, 1):uint() + payload_length = len_lo + (len_hi * 256) + subtree:add(f_length_lo, buffer(offset, 1)) + subtree:add(f_length_hi, buffer(offset + 1, 1)) + local combined_length = subtree:add(f_length, payload_length) + combined_length:set_generated() + offset = offset + 2 + end + end + + -- Package ID + if structure.has_package_id then + if offset + 1 > length then return end + subtree:add(f_package_id, buffer(offset, 1)) + offset = offset + 1 + end + + -- Message ID + if offset + 1 > length then return end + local msg_id = buffer(offset, 1):uint() + subtree:add(f_message_id, buffer(offset, 1)) + offset = offset + 1 + + -- Payload data + local expected_payload_len = payload_length or (length - offset) + if structure.has_crc then + expected_payload_len = expected_payload_len - 2 + end + + if payload_length then + crc_start_offset = start_bytes + if offset + payload_length > length then + -- Incomplete frame + pinfo.cols.info = string.format("%s (incomplete)", display_name) + return + end + + if payload_length > 0 then + subtree:add(f_payload, buffer(offset, payload_length)) + offset = offset + payload_length + end + else + -- Minimal format - payload extends to end (minus CRC if present) + local remaining = length - offset + if structure.has_crc then + remaining = remaining - 2 + end + + if remaining > 0 then + subtree:add(f_payload, buffer(offset, remaining)) + offset = offset + remaining + end + end + + -- CRC + if structure.has_crc then + if offset + 2 > length then return end + + subtree:add(f_crc1, buffer(offset, 1)) + subtree:add(f_crc2, buffer(offset + 1, 1)) + + local received_crc1 = buffer(offset, 1):uint() + local received_crc2 = buffer(offset + 1, 1):uint() + + -- Calculate CRC over the data (excluding start bytes and CRC itself) + if crc_start_offset then + local crc_data_len = offset - crc_start_offset + local crc_data = buffer(crc_start_offset, crc_data_len) + local calc_crc1, calc_crc2 = fletcher16(crc_data) + + if calc_crc1 == received_crc1 and calc_crc2 == received_crc2 then + subtree:add(f_crc_status, "Valid"):set_generated() + else + subtree:add(f_crc_status, string.format("Invalid (expected: 0x%02X 0x%02X)", calc_crc1, calc_crc2)):set_generated() + end + end + + offset = offset + 2 + end + + -- Update info column + pinfo.cols.info = string.format("%s - Msg ID: %d", display_name, msg_id) +end + +-- Register the dissector +-- Register on a custom DLT for PCAP files +-- User DLT 0 = 147 in Wireshark (fixed mapping, see Wireshark documentation) +local wtap_encap_table = DissectorTable.get("wtap_encap") +local USER_DLT = 147 -- User DLT 0 +wtap_encap_table:add(USER_DLT, struct_frame_proto) + +-- Also register as a heuristic dissector for UDP +function heuristic_checker(buffer, pinfo, tree) + -- Check if this looks like a struct-frame packet + if buffer:len() < 2 then + return false + end + + local first_byte = buffer(0, 1):uint() + + -- Check for Basic frame (0x90 followed by 0x70-0x78) + if first_byte == 0x90 then + if buffer:len() < 2 then return false end + local second_byte = buffer(1, 1):uint() + if second_byte >= 0x70 and second_byte <= 0x78 then + struct_frame_proto.dissector(buffer, pinfo, tree) + return true + end + end + + -- Check for Tiny frame (0x70-0x78) + if first_byte >= 0x70 and first_byte <= 0x78 then + struct_frame_proto.dissector(buffer, pinfo, tree) + return true + end + + return false +end + +-- Register heuristic dissector for UDP +struct_frame_proto:register_heuristic("udp", heuristic_checker) + +-- Also register for TCP +struct_frame_proto:register_heuristic("tcp", heuristic_checker) + +-- Optional: Register on specific UDP/TCP ports +-- Uncomment and modify the port number as needed for your application +-- local udp_port = DissectorTable.get("udp.port") +-- udp_port:add(YOUR_PORT_NUMBER, struct_frame_proto) + +print("Struct Frame dissector loaded successfully") diff --git a/wireshark/validate_packets.py b/wireshark/validate_packets.py new file mode 100755 index 00000000..8e9e20b4 --- /dev/null +++ b/wireshark/validate_packets.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +""" +Validate the generated test packets match expected frame format. + +This script reads the generated binary files and verifies they have +the correct structure according to the struct-frame protocol. +""" + +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + + +def fletcher16(data): + """Calculate Fletcher-16 checksum.""" + sum1 = 0 + sum2 = 0 + for byte in data: + sum1 = (sum1 + byte) % 256 + sum2 = (sum2 + sum1) % 256 + return sum1, sum2 + + +def validate_basic_default_packet(packet): + """Validate a BasicDefault packet structure.""" + print(f"\nValidating BasicDefault packet ({len(packet)} bytes):") + print(f" Packet: {packet.hex(' ')}") + + # Check minimum length + if len(packet) < 6: + print(" ✗ FAIL: Packet too short") + return False + + # Check start bytes + if packet[0] != 0x90: + print(f" ✗ FAIL: Start byte 1 should be 0x90, got 0x{packet[0]:02X}") + return False + if packet[1] != 0x71: + print(f" ✗ FAIL: Start byte 2 should be 0x71, got 0x{packet[1]:02X}") + return False + + print(" ✓ Start bytes: 0x90 0x71") + + # Get length + payload_length = packet[2] + print(f" ✓ Payload length: {payload_length}") + + # Get message ID + msg_id = packet[3] + print(f" ✓ Message ID: {msg_id}") + + # Check total length + expected_total = 4 + payload_length + 2 # header + payload + CRC + if len(packet) != expected_total: + print(f" ✗ FAIL: Expected {expected_total} bytes, got {len(packet)}") + return False + + # Validate CRC + crc_data = packet[2:4+payload_length] # length + msg_id + payload + crc1, crc2 = fletcher16(bytes(crc_data)) + + received_crc1 = packet[4 + payload_length] + received_crc2 = packet[4 + payload_length + 1] + + if received_crc1 == crc1 and received_crc2 == crc2: + print(f" ✓ CRC: 0x{crc1:02X} 0x{crc2:02X} (valid)") + return True + else: + print(f" ✗ FAIL: CRC mismatch - expected 0x{crc1:02X} 0x{crc2:02X}, got 0x{received_crc1:02X} 0x{received_crc2:02X}") + return False + + +def validate_tiny_minimal_packet(packet): + """Validate a TinyMinimal packet structure.""" + print(f"\nValidating TinyMinimal packet ({len(packet)} bytes):") + print(f" Packet: {packet.hex(' ')}") + + # Check minimum length + if len(packet) < 2: + print(" ✗ FAIL: Packet too short") + return False + + # Check start byte + if packet[0] != 0x70: + print(f" ✗ FAIL: Start byte should be 0x70, got 0x{packet[0]:02X}") + return False + + print(" ✓ Start byte: 0x70") + + # Get message ID + msg_id = packet[1] + print(f" ✓ Message ID: {msg_id}") + + # Minimal format has no CRC or length field + payload_length = len(packet) - 2 + print(f" ✓ Payload length: {payload_length} (inferred)") + + return True + + +def validate_basic_extended_packet(packet): + """Validate a BasicExtended packet structure.""" + print(f"\nValidating BasicExtended packet ({len(packet)} bytes):") + print(f" Packet: {packet.hex(' ')}") + + # Check minimum length + if len(packet) < 8: + print(" ✗ FAIL: Packet too short") + return False + + # Check start bytes + if packet[0] != 0x90: + print(f" ✗ FAIL: Start byte 1 should be 0x90, got 0x{packet[0]:02X}") + return False + if packet[1] != 0x74: + print(f" ✗ FAIL: Start byte 2 should be 0x74, got 0x{packet[1]:02X}") + return False + + print(" ✓ Start bytes: 0x90 0x74") + + # Get length (16-bit, little endian) + payload_length = packet[2] + (packet[3] << 8) + print(f" ✓ Payload length: {payload_length}") + + # Get package ID + pkg_id = packet[4] + print(f" ✓ Package ID: {pkg_id}") + + # Get message ID + msg_id = packet[5] + print(f" ✓ Message ID: {msg_id}") + + # Check total length + expected_total = 6 + payload_length + 2 # header + payload + CRC + if len(packet) != expected_total: + print(f" ✗ FAIL: Expected {expected_total} bytes, got {len(packet)}") + return False + + # Validate CRC + crc_data = packet[2:6+payload_length] # len_lo, len_hi, pkg_id, msg_id, payload + crc1, crc2 = fletcher16(bytes(crc_data)) + + received_crc1 = packet[6 + payload_length] + received_crc2 = packet[6 + payload_length + 1] + + if received_crc1 == crc1 and received_crc2 == crc2: + print(f" ✓ CRC: 0x{crc1:02X} 0x{crc2:02X} (valid)") + return True + else: + print(f" ✗ FAIL: CRC mismatch - expected 0x{crc1:02X} 0x{crc2:02X}, got 0x{received_crc1:02X} 0x{received_crc2:02X}") + return False + + +def validate_basic_extended_multi_system_stream_packet(packet): + """Validate a BasicExtendedMultiSystemStream packet structure.""" + print(f"\nValidating BasicExtendedMultiSystemStream packet ({len(packet)} bytes):") + print(f" Packet: {packet.hex(' ')}") + + # Check minimum length + if len(packet) < 11: + print(" ✗ FAIL: Packet too short") + return False + + # Check start bytes + if packet[0] != 0x90: + print(f" ✗ FAIL: Start byte 1 should be 0x90, got 0x{packet[0]:02X}") + return False + if packet[1] != 0x78: + print(f" ✗ FAIL: Start byte 2 should be 0x78, got 0x{packet[1]:02X}") + return False + + print(" ✓ Start bytes: 0x90 0x78") + + # Get sequence + seq = packet[2] + print(f" ✓ Sequence: {seq}") + + # Get system and component IDs + sys_id = packet[3] + comp_id = packet[4] + print(f" ✓ System ID: {sys_id}, Component ID: {comp_id}") + + # Get length (16-bit, little endian) + payload_length = packet[5] + (packet[6] << 8) + print(f" ✓ Payload length: {payload_length}") + + # Get package ID + pkg_id = packet[7] + print(f" ✓ Package ID: {pkg_id}") + + # Get message ID + msg_id = packet[8] + print(f" ✓ Message ID: {msg_id}") + + # Check total length + expected_total = 9 + payload_length + 2 # header + payload + CRC + if len(packet) != expected_total: + print(f" ✗ FAIL: Expected {expected_total} bytes, got {len(packet)}") + return False + + # Validate CRC + crc_data = packet[2:9+payload_length] # seq, sys, comp, len_lo, len_hi, pkg_id, msg_id, payload + crc1, crc2 = fletcher16(bytes(crc_data)) + + received_crc1 = packet[9 + payload_length] + received_crc2 = packet[9 + payload_length + 1] + + if received_crc1 == crc1 and received_crc2 == crc2: + print(f" ✓ CRC: 0x{crc1:02X} 0x{crc2:02X} (valid)") + return True + else: + print(f" ✗ FAIL: CRC mismatch - expected 0x{crc1:02X} 0x{crc2:02X}, got 0x{received_crc1:02X} 0x{received_crc2:02X}") + return False + + +def main(): + """Validate all generated test packets.""" + + sample_dir = os.path.join(os.path.dirname(__file__), 'sample_packets') + + if not os.path.exists(sample_dir): + print(f"Error: {sample_dir} does not exist") + print("Run generate_test_packets.py first") + return 1 + + print("=" * 60) + print("Validating Struct Frame Test Packets") + print("=" * 60) + + all_passed = True + + # Test 1: Standard Profile (BasicDefault) + try: + with open(os.path.join(sample_dir, 'standard.bin'), 'rb') as f: + packet = f.read() + if not validate_basic_default_packet(packet): + all_passed = False + except Exception as e: + print(f"\n✗ FAIL: Could not validate standard.bin: {e}") + all_passed = False + + # Test 2: Sensor Profile (TinyMinimal) + try: + with open(os.path.join(sample_dir, 'sensor.bin'), 'rb') as f: + packet = f.read() + if not validate_tiny_minimal_packet(packet): + all_passed = False + except Exception as e: + print(f"\n✗ FAIL: Could not validate sensor.bin: {e}") + all_passed = False + + # Test 3: Bulk Profile (BasicExtended) + try: + with open(os.path.join(sample_dir, 'bulk.bin'), 'rb') as f: + packet = f.read() + if not validate_basic_extended_packet(packet): + all_passed = False + except Exception as e: + print(f"\n✗ FAIL: Could not validate bulk.bin: {e}") + all_passed = False + + # Test 4: Network Profile (BasicExtendedMultiSystemStream) + try: + with open(os.path.join(sample_dir, 'network.bin'), 'rb') as f: + packet = f.read() + if not validate_basic_extended_multi_system_stream_packet(packet): + all_passed = False + except Exception as e: + print(f"\n✗ FAIL: Could not validate network.bin: {e}") + all_passed = False + + # Summary + print("\n" + "=" * 60) + if all_passed: + print("✓ ALL TESTS PASSED") + print("=" * 60) + return 0 + else: + print("✗ SOME TESTS FAILED") + print("=" * 60) + return 1 + + +if __name__ == '__main__': + sys.exit(main())