Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b9f5b65
Scaffold Rust UMDF proof of concept
rtreit Aug 14, 2026
6b48914
Build UMDF proof of concept with NuGet WDK
rtreit Aug 15, 2026
111a1ff
Add bounded UMDF proof-of-concept channel
rtreit Aug 21, 2026
939d7ef
Move UMDF state into typed device context
rtreit Aug 21, 2026
180a8b5
Expose UMDF device as a serial port
rtreit Aug 22, 2026
f5ecd53
Implement the UMDF private transport protocol
rtreit Aug 22, 2026
1cd2e97
Connect CatHub to managed UMDF serial endpoints
rtreit Aug 22, 2026
63fe376
Add isolated UMDF deployment and end-to-end test
rtreit Aug 22, 2026
0b05353
Verify driver and daemon protocol interoperability
rtreit Aug 22, 2026
05b3cdb
Enforce serial read timeouts in the UMDF driver
rtreit Aug 22, 2026
3a710c8
Provision stable UMDF endpoint identities
rtreit Aug 22, 2026
1b4973d
Provision CatHub-owned virtual COM endpoints
rtreit Aug 22, 2026
47bf189
Restrict the UMDF daemon channel to its owner
rtreit Aug 22, 2026
457825d
Exercise UMDF daemon failure and recovery
rtreit Aug 22, 2026
8a3b6e0
Record UMDF package provenance and checksums
rtreit Aug 22, 2026
18f8119
Track serial conformance executable
rtreit Aug 22, 2026
dc088b4
Build pure Rust UMDF driver in CI
rtreit Aug 22, 2026
8304310
Package UMDF driver from locked release build
rtreit Aug 22, 2026
0798f12
Keep managed serial stubs portable
rtreit Aug 22, 2026
7cf7acb
Exercise UMDF device restart recovery
rtreit Aug 22, 2026
c24eb20
Document CatHub-owned Windows serial setup
rtreit Aug 22, 2026
d8c526b
Document tonic status lint exception
rtreit Aug 22, 2026
0d841a5
Record virtual serial signing decision gate
rtreit Aug 22, 2026
7777d01
Audit Rust dependency supply chain
rtreit Aug 22, 2026
530e77c
Satisfy current stable provisioning lint
rtreit Aug 22, 2026
5d18142
Capture clean-target UMDF acceptance evidence
rtreit Aug 22, 2026
9bfe9c5
Exercise managed UMDF serial APIs end to end
rtreit Aug 22, 2026
3454a56
Verify managed serial buffer saturation
rtreit Aug 22, 2026
10e0942
Harden CHVS decoders against malformed input
rtreit Aug 22, 2026
47d759b
Verify managed COM exclusive-open behavior
rtreit Aug 22, 2026
d77aadb
Harden first-time UMDF provisioning diagnostics
rtreit Aug 22, 2026
372334b
Sign UMDF image before catalog generation
rtreit Aug 22, 2026
236dcc8
Isolate each CatHub UMDF device host
rtreit Aug 22, 2026
0e4aabf
Register managed ports with Windows serial discovery
rtreit Aug 22, 2026
a244d92
Verify serial flow control round trips
rtreit Aug 22, 2026
be850dc
Broaden native serial control conformance
rtreit Aug 22, 2026
32641a2
Complete UMDF serial transport end to end
rtreit Aug 22, 2026
f641c2b
Harden UMDF end-to-end acceptance harness
rtreit Aug 22, 2026
ab669c3
Document UMDF installed-driver acceptance
rtreit Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,44 @@ jobs:
with:
dotnet-version: 10.0.x
- run: dotnet build CatHub.slnx -c Release

umdf-driver:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: dtolnay/rust-toolchain@1.91.0
with:
components: rustfmt, clippy
targets: x86_64-pc-windows-msvc
- uses: NuGet/setup-nuget@v4.0
with:
nuget-version: 6.14.3
- name: Restore pinned WDK
shell: pwsh
run: |
$packageRoot = Join-Path $env:LOCALAPPDATA 'CatHub\wdk\packages'
nuget install Microsoft.Windows.WDK.x64 `
-Version 10.0.28000.2526 `
-OutputDirectory $packageRoot `
-NonInteractive `
-DirectDownload `
-Source https://api.nuget.org/v3/index.json
- name: Build, test, and lint pure-Rust UMDF driver
shell: pwsh
run: .\scripts\Test-UmdfPoc.ps1

supply-chain:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- name: Audit application dependencies
uses: EmbarkStudios/cargo-deny-action@v2.1.1
with:
command: check
command-arguments: advisories bans sources
- name: Audit UMDF driver dependencies
uses: EmbarkStudios/cargo-deny-action@v2.1.1
with:
command: check
command-arguments: advisories bans sources
manifest-path: drivers/cathub-virtual-serial-umdf/Cargo.toml
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ target
artifacts
.vs
**/bin
!crates/cathub-virtual-serial/src/bin/
!crates/cathub-virtual-serial/src/bin/serial-conformance.rs
**/obj

# These are backup files generated by rustfmt
Expand Down
16 changes: 9 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/cathub-protocol",
"crates/cathub-virtual-serial",
]
exclude = ["drivers/cathub-virtual-serial-umdf"]

[workspace.package]
version = "0.2.1"
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ Clients must validate that process ID before they use the endpoint.
## Client interfaces

- Hamlib-aware clients connect to a configured `[[hamlib_net]]` TCP listener.
- Serial CAT clients connect to the application side of a dedicated virtual serial pair.
- Legacy WinKeyer clients connect to their own virtual serial pair.
- On Windows, serial CAT clients can connect to a single CatHub-owned UMDF COM endpoint; existing
physical and externally provisioned serial transports remain supported.
- Legacy WinKeyer clients can use their own CatHub-owned UMDF COM endpoint.
- Typed WinKeyer clients connect to the loopback gRPC address in `[winkeyer].api_bind`.

A launcher-managed client uses the endpoint in CatHub's runtime file.
Expand Down Expand Up @@ -165,6 +166,8 @@ Registry publication is a separate authorized operation. See
- [Virtual serial Phase 1](docs/design/virtual-serial-transport-phase-1.md)
- [Serial client inventory](docs/testing/serial-client-inventory.md)
- [Operator setup](docs/integration/setup.md)
- [Windows CatHub-owned virtual serial](docs/integration/windows-virtual-serial.md)
- [Virtual serial signing decision](docs/design/virtual-serial-signing-decision.md)
- [Release and compatibility](docs/architecture/release-and-compatibility.md)

## License
Expand Down
30 changes: 12 additions & 18 deletions config/cathub.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# Validate without touching hardware:
# cargo run -p cathub -- --config config/cathub.toml --dry-run
#
# Virtual serial pairs (com0com): the daemon binds the first port of each pair, the
# application binds the second. Create one pair per serial client.
# CatHub-managed virtual serial endpoints expose one application-facing COM port each.
# The daemon connects through the private UMDF interface selected by `virtual_endpoint`.
# Provision them with `cathub --config config/cathub.toml virtual-serial apply --inf <INF>`.

[radio]
backend = "ts590" # first-class native Kenwood TS-590 driver (no Hamlib linked)
Expand All @@ -30,8 +31,7 @@ native_push = true # daemon enables and owns the TS-590 AI2; stream
# --- WinKeyer broker ------------------------------------------------------------------
#
# CatHub owns the physical keyer. Typed clients use the loopback API; legacy clients use
# the application side of a dedicated virtual serial pair. Change COM40/COM41 if that pair
# is not installed on this station.
# a dedicated CatHub-managed COM endpoint.

[winkeyer]
port = "COM3"
Expand All @@ -41,15 +41,15 @@ api_bind = "127.0.0.1:50071"

[[winkeyer_endpoint]]
name = "n1mm-cw"
transport = "COM40" # N1MM WinKeyer port is the paired COM41
virtual_endpoint = "n1mm-winkeyer"
application_transport = "COM41"
baud = 1200
primary = true
perms = ["status", "send", "control", "ptt"]

[[winkeyer_endpoint]]
name = "wktools-maintenance"
transport = "COM42" # WKTools maintenance port is the paired COM43
virtual_endpoint = "wktools"
application_transport = "COM43"
baud = 1200
primary = false
Expand Down Expand Up @@ -92,16 +92,10 @@ bind = "127.0.0.1:4534"
single_vfo = true
perms = ["read", "write"]

# --- Serial endpoints (com0com virtual pairs) ----------------------------------------------
# --- CatHub-managed serial endpoints ---------------------------------------------------
#
# IMPORTANT: each com0com pair has TWO port numbers. The daemon binds the port named below
# (`transport`); your application must connect to the OTHER port in the same pair. Never
# point an application at the daemon's port -- they cannot both open the same port.
#
# pair daemon binds (transport) application connects to
# COM10 <-> COM11 COM10 COM11 (HDSDR / OmniRig)
# COM20 <-> COM21 COM20 COM21 (N1MM)
# COM30 <-> COM31 COM30 COM31 (ARCP-590)
# Each application opens `application_transport`; CatHub attaches to the same device through
# its private interface. There is no daemon-side COM port and no null-modem pair.

# HDSDR via OmniRig as a TS-2000-style controller. The panadapter follows the radio, and
# click-to-tune on the waterfall may set frequency. Mode writes are denied so OmniRig cannot
Expand All @@ -110,7 +104,7 @@ perms = ["read", "write"]
# can never oscillate the TS-590's A/B VFO selection.
[[serial_endpoint]]
name = "hdsdr-omnirig"
transport = "COM10" # OmniRig binds COM11
virtual_endpoint = "hdsdr-cat"
application_transport = "COM11"
baud = 115200
dialect = "ts2000"
Expand All @@ -122,7 +116,7 @@ perms = ["read", "frequency_write"]
# whichever VFO the radio is actually on, so A/B switching tracks seamlessly.
[[serial_endpoint]]
name = "n1mm"
transport = "COM20" # N1MM binds COM21
virtual_endpoint = "n1mm-cat"
application_transport = "COM21"
baud = 115200
dialect = "ts590"
Expand All @@ -134,7 +128,7 @@ perms = ["read", "write", "ptt"]
# single_vfo off (the default).
[[serial_endpoint]]
name = "arcp590"
transport = "COM30" # ARCP-590 binds COM31
virtual_endpoint = "arcp590-cat"
application_transport = "COM31"
baud = 115200
dialect = "ts590"
Expand Down
17 changes: 13 additions & 4 deletions crates/cathub-virtual-serial/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ repository.workspace = true
readme = "README.md"
publish = false

[features]
default = ["conformance"]
conformance = ["dep:clap", "dep:serde", "dep:serde_json", "dep:windows-sys"]

[[bin]]
name = "serial-conformance"
path = "src/bin/serial-conformance.rs"
required-features = ["conformance"]

[dependencies]
clap = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
clap = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
windows-sys = { version = "0.59", optional = true, features = [
"Win32_Devices_Communication",
"Win32_Foundation",
"Win32_Security",
Expand Down
15 changes: 15 additions & 0 deletions crates/cathub-virtual-serial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@ cargo run -p cathub-virtual-serial --bin serial-conformance -- run `
```

Do not use a physical radio or a physical WinKeyer for this test.

For an installed CatHub-owned UMDF endpoint, start CatHub's hidden loopback-only test peer and use
the private-channel conformance mode instead of creating a second COM port:

```powershell
cathub virtual-serial test-peer --endpoint cathub-default `
--kind cat --listen 127.0.0.1:39116

serial-conformance run --application-port COM91 `
--peer-tcp 127.0.0.1:39116 --profile n1mm-radio `
--output artifacts\serial-conformance\managed-n1mm-radio.json
```

The test peer binds only the explicitly supplied address; the isolated-target harness always uses
IPv4 loopback and terminates it before starting the production daemon path.
85 changes: 85 additions & 0 deletions crates/cathub-virtual-serial/src/bin/serial-conformance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Windows serial API conformance command.

use std::net::SocketAddr;
use std::path::PathBuf;

use cathub_virtual_serial::conformance::{profiles, run, run_with_tcp_peer, ConformanceError};
use clap::{Parser, Subcommand};

#[derive(Debug, Parser)]
#[command(
name = "serial-conformance",
version,
about = "Test a virtual serial pair with the Windows serial API"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
/// List the supported application profiles as JSON.
Profiles,
/// Run one profile against an isolated virtual serial pair.
Run {
/// Application-facing COM port.
#[arg(long)]
application_port: String,
/// Paired COM port that the harness uses as the transport peer.
#[arg(
long,
required_unless_present = "peer_tcp",
conflicts_with = "peer_tcp"
)]
peer_port: Option<String>,
/// TCP bridge to a CatHub-managed endpoint's private transport peer.
#[arg(
long,
required_unless_present = "peer_port",
conflicts_with = "peer_port"
)]
peer_tcp: Option<SocketAddr>,
/// Stable profile name from the profiles command.
#[arg(long)]
profile: String,
/// Optional JSON report path.
#[arg(long)]
output: Option<PathBuf>,
},
}

fn main() -> Result<(), ConformanceError> {
match Cli::parse().command {
Command::Profiles => {
println!("{}", serde_json::to_string_pretty(profiles())?);
Ok(())
}
Command::Run {
application_port,
peer_port,
peer_tcp,
profile,
output,
} => {
let report = match (peer_port, peer_tcp) {
(Some(peer_port), None) => run(&profile, &application_port, &peer_port)?,
(None, Some(peer_address)) => {
run_with_tcp_peer(&profile, &application_port, peer_address)?
}
_ => unreachable!("clap requires exactly one peer transport"),
};
println!("{}", serde_json::to_string_pretty(&report)?);
if let Some(path) = output {
report.write_json(&path)?;
}
if report.required_cases_pass() {
Ok(())
} else {
Err(ConformanceError::InvalidResult(
"one or more required conformance cases failed".to_string(),
))
}
}
}
}
Loading