Python library for controlling Icom transceivers over LAN (UDP) — no wfview/hamlib required
Project description
icom-lan
Python library for controlling Icom transceivers over LAN (UDP).
Direct connection to your radio — no wfview, hamlib, or RS-BA1 required.
Features
- 📡 Direct UDP connection — no intermediate software needed
- 🎛️ Full CI-V command set — frequency, mode, filter, power, meters, PTT, CW keying, VFO, split, ATT, PREAMP
- 🔍 Network discovery — find radios on your LAN automatically
- 💻 CLI tool —
icom-lan status,icom-lan freq 14.074m - ⚡ Async API — built on asyncio for seamless integration
- 🚀 Fast non-audio connect path — CLI/status calls don't block on audio-port negotiation
- 🧠 Commander queue — wfview-style serialized command execution with pacing, retries, and dedupe
- 📊 Scope/waterfall — real-time spectrum data with callback API
- 🔒 Zero dependencies — pure Python, stdlib only
- 📝 Type-annotated — full
py.typedsupport
Supported Radios
| Radio | Status | CI-V Address |
|---|---|---|
| IC-7610 | ✅ Tested | 0x98 |
| IC-705 | Should work | 0xA4 |
| IC-7300 | Should work | 0x94 |
| IC-9700 | Should work | 0xA2 |
| IC-7851 | Should work | 0x8E |
| IC-R8600 | Should work | 0x96 |
Any Icom radio with LAN/WiFi control should work — the CI-V address is configurable.
Installation
pip install icom-lan
From source:
git clone https://github.com/morozsm/icom-lan.git
cd icom-lan
pip install -e .
Quick Start
Python API
import asyncio
from icom_lan import IcomRadio
async def main():
async with IcomRadio("192.168.1.100", username="user", password="pass") as radio:
# Read current state
freq = await radio.get_frequency()
mode = await radio.get_mode()
s = await radio.get_s_meter()
print(f"{freq/1e6:.3f} MHz {mode.name} S={s}")
# Tune to 20m FT8
await radio.set_frequency(14_074_000)
await radio.set_mode("USB")
# VFO & Split
await radio.select_vfo("MAIN")
await radio.set_split_mode(True)
# CW
await radio.send_cw_text("CQ CQ DE KN4KYD K")
# Scope / Waterfall
def on_frame(frame):
print(f"{frame.start_freq_hz/1e6:.3f}–{frame.end_freq_hz/1e6:.3f} MHz, {len(frame.pixels)} px")
radio.on_scope_data(on_frame)
await radio.enable_scope()
asyncio.run(main())
CLI
# Set credentials via environment
export ICOM_HOST=192.168.1.100
export ICOM_USER=myuser
export ICOM_PASS=mypass
# Radio status
icom-lan status
# Frequency (multiple input formats)
icom-lan freq # Get
icom-lan freq 14.074m # Set (MHz)
icom-lan freq 7074k # Set (kHz)
icom-lan freq 14074000 # Set (Hz)
# Mode
icom-lan mode USB
# Meters (JSON output)
icom-lan meter --json
# CW keying
icom-lan cw "CQ CQ DE KN4KYD K"
# PTT
icom-lan ptt on
icom-lan ptt off
# Attenuator & Preamp (Command29-aware for IC-7610)
icom-lan att # Get attenuation level
icom-lan att 18 # Set 18 dB
icom-lan preamp # Get preamp level
icom-lan preamp 1 # Set PREAMP 1
# Scope / Waterfall snapshot (requires: pip install icom-lan[scope])
icom-lan scope # Combined spectrum + waterfall → scope.png
icom-lan scope --spectrum-only # Spectrum only (1 frame)
icom-lan scope --theme grayscale # Grayscale theme
icom-lan scope --json # Raw data as JSON (no Pillow needed)
# Example output

# Discover radios on network
icom-lan discover
API Reference
IcomRadio Methods
| Method | Description |
|---|---|
get_frequency() → int |
Current frequency in Hz |
set_frequency(hz) |
Set frequency |
get_mode() → Mode |
Current mode |
get_mode_info() → (Mode, filter) |
Current mode + filter number (if reported) |
set_mode(mode, filter_width=None) |
Set mode (optionally with filter 1-3) |
get_filter() / set_filter(n) |
Read/set filter number |
get_power() → int |
RF power level (0–255) |
set_power(level) |
Set RF power |
get_s_meter() → int |
S-meter (0–255) |
get_swr() → int |
SWR meter (0–255, TX only) |
get_alc() → int |
ALC meter (0–255, TX only) |
set_ptt(on) |
Push-to-talk on/off |
select_vfo(vfo) |
Select VFO (A/B/MAIN/SUB) |
set_split_mode(on) |
Split on/off |
get_attenuator_level(receiver) → int |
Read attenuator in dB (Command29) |
set_attenuator_level(db, receiver) |
Set attenuator dB (0–45, 3 dB steps) |
get_preamp(receiver) → int |
Read preamp level (Command29) |
set_preamp(level, receiver) |
Set preamp (0=off, 1=PRE1, 2=PRE2) |
on_scope_data(callback) |
Register callback for scope/waterfall frames |
enable_scope(output=True) |
Enable scope display + data output |
disable_scope() |
Disable scope data output |
send_cw_text(text) / stop_cw_text() |
Send/stop CW via built-in keyer |
power_control(on) |
Remote power on/off |
snapshot_state() / restore_state(state) |
Best-effort state save/restore |
send_civ(cmd, sub, data) |
Send raw CI-V command |
Configuration
| Parameter | Default | Env Var | Description |
|---|---|---|---|
host |
— | ICOM_HOST |
Radio IP address |
port |
50001 |
ICOM_PORT |
Control port |
username |
"" |
ICOM_USER |
Auth username |
password |
"" |
ICOM_PASS |
Auth password |
radio_addr |
0x98 |
— | CI-V address |
timeout |
5.0 |
— | Timeout (seconds) |
How It Works
The library implements the Icom proprietary LAN protocol:
- Control port (50001) — UDP handshake, authentication, session management
- CI-V port (50002) — CI-V command exchange
- Audio port (50003) — RX/TX audio streaming (including full-duplex orchestration)
Discovery → Login → Token → Conninfo → CI-V Open → Commands
See the protocol documentation for a deep dive.
Testing
# Unit tests (no radio required) — 469 tests
pytest tests/test_*.py
# Mock integration tests (full UDP protocol, no radio required)
pytest tests/test_mock_integration.py
# Integration tests (real radio required)
export ICOM_HOST=192.168.55.40
export ICOM_USER=your_username
export ICOM_PASS=your_password
pytest -m integration tests/integration
# Guarded power-cycle test (will actually power off/on radio)
export ICOM_ALLOW_POWER_CONTROL=1
pytest -m integration tests/integration/test_radio_integration.py::TestPowerHardware::test_power_cycle_roundtrip -q -s
# Soak test (seconds)
export ICOM_SOAK_SECONDS=120
pytest -m integration tests/integration/test_radio_integration.py::TestSoak::test_soak_retries_and_logging -q -s
Documentation
📖 Full documentation: morozsm.github.io/icom-lan
Security
- Zero external dependencies — minimal attack surface
- Credentials passed via env vars or parameters, never stored
- The Icom protocol uses UDP without encryption — see SECURITY.md
License
MIT — see LICENSE.
Protocol knowledge based on wfview (GPLv3) reverse engineering. This is an independent clean-room implementation, not a derivative work.
Acknowledgments
- The wfview project for their extensive reverse engineering of the Icom LAN protocol
- The amateur radio community for testing and feedback
Trademark Notice
Icom™ and the Icom logo are registered trademarks of Icom Incorporated. This project is not affiliated with, endorsed by, or sponsored by Icom. Product names are used solely for identification and compatibility purposes (nominative fair use).
73 de KN4KYD 🏗️
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file icom_lan-0.6.0.tar.gz.
File metadata
- Download URL: icom_lan-0.6.0.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97a98881d057bf92f3c7b06296927e411b0eedf11882939375207336a079ab88
|
|
| MD5 |
091be68c1f4489e8435e71de53c5f053
|
|
| BLAKE2b-256 |
ecf29d525fbf84d0d6c93aaef7ac399ad435728389b4cf1525816a73123925dd
|
File details
Details for the file icom_lan-0.6.0-py3-none-any.whl.
File metadata
- Download URL: icom_lan-0.6.0-py3-none-any.whl
- Upload date:
- Size: 57.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32c617e583e3d0b407c25f759be10e902604132305bacbb98cc871b0046889c7
|
|
| MD5 |
1e406264b6b35c05d9a7cca7e4fded32
|
|
| BLAKE2b-256 |
cbbbc8e641db2668b5a6f0840e73de17a5153d3e631c74c9f612bc8bc41bc4eb
|