348 lines
12 KiB
Markdown
348 lines
12 KiB
Markdown
# UDP1553 Protocol Implementation
|
||
|
||
## Overview
|
||
Complete implementation of the MIL-STD-1553-over-UDP protocol used by GrifoScope for radar communication.
|
||
|
||
## Architecture
|
||
|
||
### Protocol Layers
|
||
```
|
||
Application Layer: BusController (commands, requests, callbacks)
|
||
↓
|
||
Protocol Layer: UDP1553Packet, MessageBlock, Headers
|
||
↓
|
||
Message Layer: MsgA1-A8, MsgB1-B8 (payload data)
|
||
↓
|
||
Network Layer: UDP sockets (port 51553 RT, port 61553 BC)
|
||
```
|
||
|
||
## Module: `pybusmonitor1553/lib1553/udp1553.py`
|
||
|
||
### UDP1553 Packet Structure
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ UDP1553Header (64 bytes) │
|
||
│ - marker1553: 0x1553 │
|
||
│ - otype: BC=0x4342, RT=0x5452 │
|
||
│ - fcounter, mcounter, scounter │
|
||
│ - ltt (local timetag µs) │
|
||
│ - errors, flags │
|
||
└─────────────────────────────────────────┘
|
||
┌─────────────────────────────────────────┐
|
||
│ MessageBlock #1 (74 bytes) │
|
||
│ ┌─────────────────────────────────────┐ │
|
||
│ │ MARKER_MSG_BEGIN (0x3C3C) │ │
|
||
│ │ CommandWord (16-bit) │ │
|
||
│ │ Payload Data (32 words × 2 bytes) │ │
|
||
│ │ StatusWord (16-bit) │ │
|
||
│ │ ~CommandWord (inverted for check) │ │
|
||
│ │ MARKER_MSG_END (0x3E3E) │ │
|
||
│ └─────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────┘
|
||
│ ... (additional messages) ... │
|
||
┌─────────────────────────────────────────┐
|
||
│ MARKER_END (0x5315) │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### Command Word Format (MIL-STD-1553)
|
||
|
||
**Bit Layout (MSB-first, 16 bits total):**
|
||
```
|
||
Bits [0-4]: RT Address (0-31) - Remote Terminal identifier
|
||
Bit [5]: TR (Transmit/Receive) - 0=BC→RT, 1=RT→BC
|
||
Bits [6-10]: Subaddress (0-31) - Message type (1=A1/B1, 2=A2/B2, etc.)
|
||
Bits [11-15]: Word Count (0-31) - Data words (0 = 32 words)
|
||
```
|
||
|
||
**Example:** RT=20, TR=0 (BC→RT), SA=1, WC=0 (32 words)
|
||
```
|
||
Binary (MSB-first): 10100 0 00001 00000 = 0xA020
|
||
Wire format (LE): bytes [0x20, 0xA0]
|
||
```
|
||
|
||
**Critical Implementation Note:**
|
||
The Command/Status Words use `BigEndianStructure` for bitfield interpretation (MSB-first per 1553 standard), but wire transmission is little-endian. The `get_wire_value()` method performs byte swapping:
|
||
```python
|
||
wire_value = ((self.raw & 0xFF) << 8) | ((self.raw >> 8) & 0xFF)
|
||
```
|
||
|
||
### Status Word Format
|
||
|
||
**Bit Layout (MSB-first, 16 bits total):**
|
||
```
|
||
Bits [0-4]: RT Address
|
||
Bit [5]: Message Error (me)
|
||
Bit [6]: Instrumentation (instr)
|
||
Bit [7]: Service Request (sr)
|
||
Bits [8-10]: Reserved
|
||
Bit [11]: Broadcast Cmd Received Status (brcs)
|
||
Bit [12]: Busy
|
||
Bit [13]: Subsystem Flag (sf)
|
||
Bit [14]: Dynamic Bus Control Acceptance (bca)
|
||
Bit [15]: Terminal Flag (tf)
|
||
```
|
||
|
||
## Endianness Summary
|
||
|
||
| Component | Endianness | Reason |
|
||
|-----------|------------|--------|
|
||
| **UDP Header** | Little Endian | C struct default, ctypes.LittleEndianStructure |
|
||
| **Control Words (wire)** | Little Endian | Network transmission format |
|
||
| **Control Words (bitfields)** | Big Endian | MIL-STD-1553 MSB-first convention |
|
||
| **Message Payload** | Big Endian | MessageBase.pack() uses struct.pack('>H') |
|
||
| **Markers** | Little Endian | Packet structure markers (0x1553, 0x3C3C, 0x5315) |
|
||
|
||
**Key Insight:**
|
||
The complexity arises from mixing:
|
||
- C struct layout (little-endian headers)
|
||
- 1553 bit numbering (MSB=bit 0)
|
||
- Network byte order (payload big-endian per radar ICD)
|
||
|
||
## Module: `pybusmonitor1553/core/bus_controller.py`
|
||
|
||
### BusController API
|
||
|
||
High-level API for Bus Controller operations:
|
||
|
||
```python
|
||
from pybusmonitor1553.core.bus_controller import BusController
|
||
from pybusmonitor1553.lib1553.messages import MsgA1, MsgB7
|
||
|
||
# Create and connect
|
||
bc = BusController(rt_ip="192.168.1.100", rt_port=51553)
|
||
bc.connect()
|
||
|
||
# Send A command (BC→RT)
|
||
msg_a1 = MsgA1()
|
||
msg_a1.master_mode = MasterMode.TWS
|
||
msg_a1.symbol_intensity = 75
|
||
bc.send_command(subaddress=1, message=msg_a1)
|
||
|
||
# Request B data (RT→BC)
|
||
msg_b7 = bc.request_data(subaddress=7, message_class=MsgB7)
|
||
print(f"Radar mode: {msg_b7.master_mode}")
|
||
|
||
# Register callback for continuous monitoring
|
||
def on_b7_update(msg):
|
||
print(f"Status: mode={msg.master_mode}, valid={msg.valid_data_flag}")
|
||
|
||
bc.register_callback("B7", on_b7_update)
|
||
bc.start_polling() # Background thread polls B messages at 50Hz/6.25Hz
|
||
```
|
||
|
||
### BusControllerConfig
|
||
|
||
```python
|
||
@dataclass
|
||
class BusControllerConfig:
|
||
# Network
|
||
rt_ip: str = "192.168.1.100"
|
||
rt_port: int = 51553
|
||
bc_port: int = 61553
|
||
|
||
# Bus parameters
|
||
rt_address: int = 20
|
||
|
||
# Polling rates (Hz)
|
||
fast_rate: float = 50.0 # B1-B3 (TWS targets)
|
||
slow_rate: float = 6.25 # B4-B8 (status)
|
||
|
||
# Timeouts
|
||
response_timeout: float = 0.1 # 100ms
|
||
```
|
||
|
||
## Usage Examples
|
||
|
||
### Example 1: Send Radar Mode Command
|
||
|
||
```python
|
||
from pybusmonitor1553.core.bus_controller import send_radar_command
|
||
from pybusmonitor1553.lib1553.messages import MsgA2
|
||
from pybusmonitor1553.lib1553.constants import DesignationControl
|
||
|
||
# Send A2 command to change designation mode
|
||
msg = MsgA2()
|
||
msg.designation_control = DesignationControl.STT
|
||
msg.priority_target_number = 5
|
||
|
||
send_radar_command("192.168.1.100", subaddress=2, message=msg)
|
||
```
|
||
|
||
### Example 2: Continuous TWS Target Monitoring
|
||
|
||
```python
|
||
bc = BusController()
|
||
bc.connect()
|
||
|
||
received_targets = []
|
||
|
||
def on_tws_target(msg):
|
||
if msg.valid_data_flag == 0: # 0=VALID per ICD
|
||
received_targets.append({
|
||
'number': msg.target_number,
|
||
'x': msg.track_position_x,
|
||
'y': msg.track_position_y,
|
||
'z': msg.track_position_z
|
||
})
|
||
|
||
bc.register_callback("B1", on_tws_target)
|
||
bc.register_callback("B2", on_tws_target)
|
||
bc.register_callback("B3", on_tws_target)
|
||
|
||
bc.start_polling()
|
||
time.sleep(5) # Collect 5 seconds of data
|
||
bc.stop_polling()
|
||
|
||
print(f"Received {len(received_targets)} valid targets")
|
||
```
|
||
|
||
### Example 3: Multi-Message Command Sequence
|
||
|
||
```python
|
||
with BusController() as bc:
|
||
# Step 1: Configure radar settings (A1)
|
||
a1 = MsgA1()
|
||
a1.target_history = 3
|
||
a1.symbol_intensity = 75
|
||
bc.send_command(1, a1)
|
||
|
||
# Step 2: Set TWS mode (A2)
|
||
a2 = MsgA2()
|
||
a2.designation_control = DesignationControl.TWS
|
||
bc.send_command(2, a2)
|
||
|
||
# Step 3: Verify mode change (B7)
|
||
b7 = bc.request_data(7, MsgB7, timeout=0.5)
|
||
if b7 and b7.master_mode == MasterMode.TWS:
|
||
print("✓ Radar successfully switched to TWS mode")
|
||
```
|
||
|
||
## Testing
|
||
|
||
Comprehensive test suite in `tests/test_udp1553.py`:
|
||
|
||
- **Header Tests**: Structure size (64 bytes), validation, packing
|
||
- **Command/Status Word Tests**: Bitfield layout, wire value conversion, byte swapping
|
||
- **Message Block Tests**: Pack/unpack, marker validation, CW inversion check
|
||
- **Packet Tests**: Multi-message packets, end markers, frame counters
|
||
- **Roundtrip Tests**: Complete encode/decode with real message classes
|
||
|
||
**Coverage:** 96% (166/173 lines in udp1553.py)
|
||
|
||
Run tests:
|
||
```bash
|
||
pytest tests/test_udp1553.py -v
|
||
```
|
||
|
||
## Protocol Constants
|
||
|
||
```python
|
||
# UDP Header
|
||
MARKER_1553 = 0x1553 # Header start marker
|
||
MARKER_END = 0x5315 # Packet end marker
|
||
OTYPE_BC = 0x4342 # "BC" origin type
|
||
OTYPE_RT = 0x5452 # "RT" origin type
|
||
|
||
# Message Block
|
||
MARKER_MSG_BEGIN = 0x3C3C # Message start marker
|
||
MARKER_MSG_END = 0x3E3E # Message end marker
|
||
|
||
# Network Ports
|
||
RT_PORT = 51553 # RT receives commands
|
||
BC_PORT = 61553 # BC receives responses
|
||
```
|
||
|
||
## Compatibility with GrifoScope
|
||
|
||
This implementation is binary-compatible with GrifoScope C++ SDK:
|
||
|
||
**Source Reference:**
|
||
- `cpp/GrifoScope/GrifoSdkEif/pub/TH/udp1553_types.h`
|
||
|
||
**Verified Compatibility:**
|
||
- Header structure: 64 bytes, identical field layout
|
||
- Command/Status words: Correct bitfield interpretation
|
||
- Message blocks: Marker placement, payload padding, CW inversion
|
||
- Packet structure: Multi-message support, end marker
|
||
|
||
**Tested Scenarios:**
|
||
- PyBusMonitor ↔ GrifoScope packet exchange
|
||
- Binary dumps match byte-for-byte (see `tools/compare_a2_messages.py`)
|
||
|
||
## Future Extensions
|
||
|
||
### Remote Terminal (RT) Simulator
|
||
For testing without hardware:
|
||
```python
|
||
# pybusmonitor1553/core/remote_terminal.py (future)
|
||
class RemoteTerminal:
|
||
"""Passive RT simulator - responds to BC requests."""
|
||
|
||
def __init__(self, rt_address=20, listen_port=51553):
|
||
self.rt_addr = rt_address
|
||
self.port = listen_port
|
||
self.a_messages = {} # Store received A commands
|
||
self.b_messages = {} # Pre-configured B responses
|
||
|
||
def handle_a_command(self, subaddress, payload):
|
||
"""Process BC→RT command (store state)."""
|
||
msg_class = get_message_class('A', subaddress)
|
||
msg = msg_class()
|
||
msg.unpack(payload)
|
||
self.a_messages[subaddress] = msg
|
||
|
||
def handle_b_request(self, subaddress) -> bytes:
|
||
"""Process RT→BC request (return current state)."""
|
||
msg = self.b_messages.get(subaddress)
|
||
return msg.pack() if msg else b'\x00' * 64
|
||
```
|
||
|
||
### Logging Integration
|
||
Add structured logging for protocol debugging:
|
||
```python
|
||
import logging
|
||
logger = logging.getLogger('udp1553')
|
||
|
||
def pack(self) -> bytes:
|
||
data = ...
|
||
logger.debug(f"TX: SA={self.command_word.sa}, "
|
||
f"RT={self.command_word.rt}, "
|
||
f"CW=0x{self.command_word.get_wire_value():04X}")
|
||
return data
|
||
```
|
||
|
||
## References
|
||
|
||
- **MIL-STD-1553B:** Military Standard for Data Bus
|
||
- **ICD Document:** `doc/ICD_DECD_FTH - GRIFO-F_TH, Data Exchange Control Document for, rev -A, Draft 2.md`
|
||
- **C++ Implementation:** `cpp/GrifoScope/GrifoSdkEif/pub/TH/udp1553_types.h`
|
||
- **Field Mappings:** `doc/C++-Python-Message-Mapping.md`
|
||
- **Architecture:** `doc/Technical-Architecture.md`
|
||
|
||
## Troubleshooting
|
||
|
||
### Issue: Command word bit positions incorrect
|
||
**Symptom:** RT receives commands but interprets wrong subaddress
|
||
**Cause:** Endianness mismatch in bitfield interpretation
|
||
**Solution:** Verify `BigEndianStructure` used for CW/SW bitfields, `get_wire_value()` performs byte swap
|
||
|
||
### Issue: Inverted CW validation fails
|
||
**Symptom:** `ValueError: Command word mismatch: CW=0xXXXX, ~CW=0xYYYY`
|
||
**Cause:** Inversion applied to wrong value (raw vs wire)
|
||
**Solution:** Invert `get_wire_value()` result: `~cw_wire & 0xFFFF`
|
||
|
||
### Issue: Message payload corrupted
|
||
**Symptom:** Decoded field values incorrect (e.g., position x = 0 when should be 15000)
|
||
**Cause:** Endianness error in payload data
|
||
**Solution:** Payload MUST be big-endian from `MessageBase.pack()` - verify `struct.pack('>H', value)`
|
||
|
||
### Issue: RT not responding to requests
|
||
**Symptom:** `request_data()` times out, no B message received
|
||
**Cause:** TR bit incorrect (should be 1 for RT→BC)
|
||
**Solution:** Set `is_transmit=True` for B message requests:
|
||
```python
|
||
create_rt_to_bc_request(rt_addr=20, subaddress=7) # TR=1 automatically
|
||
```
|