Skip to content

feat: direct TCP Winlink CMS gateway (Phase 5.5)#104

Merged
dc0sk merged 3 commits intomainfrom
openpulse-gateway
May 5, 2026
Merged

feat: direct TCP Winlink CMS gateway (Phase 5.5)#104
dc0sk merged 3 commits intomainfrom
openpulse-gateway

Conversation

@dc0sk
Copy link
Copy Markdown
Owner

@dc0sk dc0sk commented May 5, 2026

Summary

  • New openpulse-gateway binary crate connecting directly to cms.winlink.org:8772 over TCP
  • Phase 1 (ISS): reads CMS banner, sends B2F FC+FF proposals, receives FS, sends compressed message blobs
  • Phase 2 (IRS): reuses the same TCP connection with a fresh B2fSession(Irs) to receive pending messages from the CMS
  • CLI: openpulse-gateway [--host] [--port] [--callsign] send --to <CALL> [--subject] [--message | stdin]
  • Callsign defaults to station.callsign from ~/.config/openpulse/config.toml; --callsign overrides

Test plan

  • gateway_round_trip unit test: loopback TCP pair, mock CMS server in a thread validates full ISS+IRS exchange (no network access)
  • cargo test -p openpulse-gateway --no-default-features
  • cargo test --workspace --no-default-features
  • cargo clippy --workspace --no-default-features -- -D warnings
  • cargo fmt --all -- --check

Closes roadmap task: Phase 5.5 — Direct TCP Winlink CMS gateway

dc0sk added 2 commits May 5, 2026 14:14
New openpulse-gateway binary connects directly to cms.winlink.org:8772,
sends outbound messages via B2F ISS protocol, then receives pending
messages via B2F IRS protocol on the same TCP connection.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new openpulse-gateway binary that uses the existing B2F/DataPort pieces to talk directly to a Winlink CMS over TCP, wires it into the workspace, and documents Phase 5.5 as complete. Review found blocking correctness issues in the new gateway flow.

Changes:

  • add crates/openpulse-gateway with CLI, TCP exchange logic, and a mock-CMS round-trip test
  • register the new gateway crate in the workspace
  • update CLAUDE.md to mark the direct TCP CMS gateway phase as done

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
crates/openpulse-gateway/src/main.rs New gateway CLI, session handling, TCP exchange logic, and unit test.
crates/openpulse-gateway/Cargo.toml Defines the new binary crate and its dependencies.
CLAUDE.md Marks Phase 5.5 as completed and summarizes the new gateway.
Cargo.toml Adds the gateway crate to the workspace members and workspace dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +149 to +150
for (h, b) in messages {
session.queue_message(h, b)?;
Comment thread crates/openpulse-gateway/src/main.rs Outdated
Comment on lines +165 to +168
tracing::debug!("← {}", line.trim());
session.handle_line(&line)?;

for blob in session.drain_pending_data() {
Comment thread crates/openpulse-gateway/src/main.rs Outdated
pub(crate) fn irs_receive(data: &mut DataPort) -> anyhow::Result<Vec<Vec<u8>>> {
let mut session = B2fSession::new(SessionRole::Irs);

while let Ok(frame) = data.recv_frame() {
Comment thread crates/openpulse-gateway/src/main.rs Outdated
Comment on lines +225 to +233

tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();

let cfg = openpulse_config::load()?;
Comment thread crates/openpulse-gateway/src/main.rs Outdated
Comment on lines +181 to +184
pub(crate) fn irs_receive(data: &mut DataPort) -> anyhow::Result<Vec<Vec<u8>>> {
let mut session = B2fSession::new(SessionRole::Irs);

while let Ok(frame) = data.recv_frame() {
Comment on lines +262 to +265
let header = build_header(&callsign, &to, &subject, body.len() as u32);
tracing::info!("Connecting to {}:{} as {}", cli.host, cli.port, callsign);

let received = connect_and_exchange(&cli.host, cli.port, vec![(header, body)])?;
Comment thread CLAUDE.md
Comment on lines +345 to +352
**Phase 5.5 — Direct TCP Winlink CMS gateway** ✅ Done
- `crates/openpulse-gateway/`: new binary crate (`openpulse-gateway`)
- Phase 1 (ISS): connects to `cms.winlink.org:8772`, reads CMS banner, sends FC+FF proposals, reads FS, sends compressed blobs
- Phase 2 (IRS): same TCP connection, fresh `B2fSession(Irs)`, reads CMS FC+FF proposals, sends FS, reads and decompresses reply blobs
- `DataPort` wraps `TcpStream` directly — Winlink CMS TCP uses identical u16-BE framing as B2F driver
- CLI: `openpulse-gateway [--host] [--port] [--callsign] send --to <CALL> [--subject] [--message | stdin]`
- Callsign read from `~/.config/openpulse/config.toml`; `--callsign` overrides; bails on default `N0CALL`
- `gateway_round_trip` unit test: mock CMS TCP server validates full ISS+IRS exchange without network access
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +262 to +265
let header = build_header(&callsign, &to, &subject, body.len() as u32);
tracing::info!("Connecting to {}:{} as {}", cli.host, cli.port, callsign);

let received = connect_and_exchange(&cli.host, cli.port, vec![(header, body)])?;
Comment on lines +200 to +201
let blob = data.recv_frame().context("reading CMS message blob")?;
out.push(session.receive_data(blob)?);
Comment thread crates/openpulse-gateway/src/main.rs Outdated
Comment on lines +225 to +233

tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();

let cfg = openpulse_config::load()?;
Comment on lines +163 to +171
let frame = data.recv_frame().context("reading CMS FS")?;
let line = String::from_utf8_lossy(&frame).into_owned();
tracing::debug!("← {}", line.trim());
session.handle_line(&line)?;

for blob in session.drain_pending_data() {
tracing::debug!("→ [blob {} B]", blob.len());
data.send_frame(&blob)?;
}
Comment thread crates/openpulse-gateway/src/main.rs Outdated
Comment on lines +184 to +188
while let Ok(frame) = data.recv_frame() {
let line = String::from_utf8_lossy(&frame).into_owned();
tracing::debug!("← {}", line.trim());
let responses = session.handle_line(&line)?;
for resp in &responses {
…O errors

- session.rs: queue_message now prepends encoded header bytes before gzip,
  so To/From/Subject reach the CMS
- b2f-driver lib.rs: DecodedMessage gains a `header` field; run_irs splits
  decompressed blobs on \r\n\r\n using new split_message helper
- gateway main.rs: build_header sets body field correctly; iss_send bails
  if CMS rejects all proposals; irs_receive propagates non-EOF I/O errors
  instead of treating timeout as empty mailbox; tracing init moved after
  config load so logging.level setting is respected
- Updated b2f_integration, driver_integration, and gateway_round_trip tests
  to assert on both header metadata and body bytes
@dc0sk dc0sk merged commit 644d285 into main May 5, 2026
8 checks passed
@dc0sk dc0sk deleted the openpulse-gateway branch May 5, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants