# Architecture

## Overview

```
┌─────────────────────────────────────────────────────┐
│                     icom-lan                         │
│                                                      │
│  ┌─────────┐   ┌──────────┐   ┌──────────────────┐  │
│  │  CLI    │   │  Public  │   │  Raw CI-V        │  │
│  │ (cli.py)│   │   API    │   │  (commands.py)   │  │
│  └────┬────┘   │(radio.py)│   └────────┬─────────┘  │
│       │        └────┬─────┘            │             │
│       └─────────────┤                  │             │
│                     │                  │             │
│            ┌────────┴──────────────────┴──────┐      │
│            │           IcomRadio               │      │
│            │   ┌──────────┐  ┌──────────┐     │      │
│            │   │  Control │  │   CI-V   │     │      │
│            │   │Transport │  │Transport │     │      │
│            │   │ (:50001) │  │ (:50002) │     │      │
│            │   └────┬─────┘  └────┬─────┘     │      │
│            └────────┼─────────────┼───────────┘      │
│                     │             │                   │
└─────────────────────┼─────────────┼───────────────────┘
                      │   UDP       │   UDP
                      ▼             ▼
              ┌───────────────────────────┐
              │       Icom Radio          │
              │   Control  CI-V   Audio   │
              │   :50001  :50002  :50003  │
              └───────────────────────────┘
```

## Module Responsibilities

### `radio.py` — High-Level API

The central orchestrator. `IcomRadio` manages:

- **Two transport instances**: one for control (port 50001), one for CI-V (port 50002)
- **Full handshake sequence**: discovery → login → token → conninfo → CI-V open
- **CI-V command wrapping**: takes raw CI-V frames, wraps them in UDP data packets
- **Response filtering**: skips echoes, waterfall data, and control packets to find CI-V responses
- **Public API methods**: `get_frequency()`, `set_mode()`, etc.

### `transport.py` — UDP Transport

Low-level asyncio UDP handler. Each `IcomTransport` instance manages:

- **UDP socket** via `asyncio.DatagramProtocol`
- **Discovery handshake** (Are You There → I Am Here → Are You Ready)
- **Keep-alive pings** (500ms interval)
- **Sequence tracking** with gap detection
- **Retransmit requests** for missing packets
- **Packet queue** for consumers

### `commands.py` — CI-V Encoding/Decoding

Pure functions for building and parsing CI-V frames:

- Frame construction with BCD frequency encoding
- Response parsing (frequency, mode, meters, ACK/NAK)
- No state, no I/O — purely data transformation

### `auth.py` — Authentication

Handles Icom's proprietary credential encoding and packet construction:

- `encode_credentials()` — substitution-table obfuscation
- `build_login_packet()` — 0x80-byte login packet
- `build_conninfo_packet()` — 0x90-byte connection info
- Response parsers for auth and status packets

### `protocol.py` — Packet Parsing

Header serialization/deserialization and packet type identification.

### `types.py` — Data Types

Enums (`PacketType`, `Mode`), dataclasses (`PacketHeader`, `CivFrame`), and BCD helpers.

### `exceptions.py` — Error Hierarchy

Custom exception classes for structured error handling.

### `cli.py` — Command Line Interface

Argparse-based CLI that wraps the async API with `asyncio.run()`.

## Data Flow

### Sending a Command

```
radio.get_frequency()
    → get_frequency() builds CI-V frame: FE FE 98 E0 03 FD
    → _wrap_civ() adds UDP header (0x15-byte prefix)
    → _civ_transport.send_tracked() assigns sequence number
    → UDP packet sent to radio:50002
```

### Receiving a Response

```
UDP packet arrives on :50002
    → _UdpProtocol.datagram_received()
    → IcomTransport._handle_packet()
        → Check: retransmit request? ping? → handle internally
        → Otherwise: queue for consumer
    → IcomRadio._send_civ_raw() picks up from queue
        → Skip packets that are too small (control)
        → Scan payload for CI-V frames (FE FE ... FD)
        → Filter: skip echoes (from_addr == CONTROLLER), waterfall (cmd 0x27)
        → Match: response from radio with correct command byte
    → parse_frequency_response() extracts Hz from BCD data
```

## Key Design Decisions

### Dual-Port Architecture

CI-V commands **must** go through port 50002, not 50001. The control port (50001) is only for authentication and session management. This was discovered by tracing that the radio never responds to CI-V on the control port.

### GUID Echo

The radio won't report CI-V/audio ports in its status packet unless the client echoes the radio's GUID (bytes 0x20–0x2F from its conninfo) in the client's own conninfo. Without this, `civ_port` comes back as 0.

### Response Filtering

The CI-V port receives various traffic: our command echoes, waterfall data (cmd 0x27), and actual responses. `_send_civ_raw()` filters through all of this to find the matching response from the radio.

### Sequence Number Management

- `send_seq` — tracked data packets on each transport
- `ping_seq` — keep-alive pings (separate counter)
- `_auth_seq` — authentication sequence in radio.py
- `_civ_send_seq` — CI-V and OpenClose packets (separate from transport seq)

## Dependencies

```
icom-lan (runtime)
└── Python 3.11+ stdlib only
    ├── asyncio
    ├── struct
    ├── socket
    ├── logging
    └── dataclasses

icom-lan[dev] (testing)
├── pytest
└── pytest-asyncio

icom-lan[audio] (future)
└── opuslib
```
