Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
770 changes: 759 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

26 changes: 24 additions & 2 deletions apps/sergw/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ publish.workspace = true
license.workspace = true

[dependencies]
clap = { version = "4.5.16", features = ["derive"] }
ctrlc = "3.4"
anyhow = "1"
bytes = "1"
clap = { version = "4", features = ["derive"] }
crossbeam-channel = "0.5"
ctrlc = "3"
serialport = "4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
dashmap = "5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
crossterm = "0.26"
ratatui = "0.26"
libmdns = { version = "0.7", optional = true }

[target.'cfg(target_os = "linux")'.dev-dependencies]
nix = { version = "0.28", features = ["term"] }

[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.28", features = ["term"] }

[features]
default = ["mdns"]
mdns = ["libmdns"]
153 changes: 68 additions & 85 deletions apps/sergw/README.md
Original file line number Diff line number Diff line change
@@ -1,118 +1,101 @@
# SERGW - Serial Gateway
### sergw

## NAME
Simple serial ↔ TCP gateway with a built‑in TUI and optional zero‑config mDNS advertisement.

sergw - Serial-to-TCP gateway for communication between serial devices and TCP clients
sergw opens a local serial device and serves it over TCP, broadcasting serial output to all connected clients and forwarding client input to the serial device. It emphasizes pragmatic reliability (auto‑reconnect) and visibility (TUI overview and hex/ascii/dec inspector).

## SYNOPSIS
### Highlights

**sergw** [*OPTIONS*] _COMMAND_
- Serial ↔ TCP bridge over raw TCP (no telnet/RFC2217, no TLS)
- Multi‑client fan‑out: serial output is broadcast to all connected TCP clients
- Backpressure handling: slow or disconnected clients are dropped
- TUI: overview (connections, throughput, events) and inspector (hex/ascii/dec)
- Auto‑reconnect for serial (reader/writer) with buffered retry for writes
- Zero‑config mDNS (feature ‘mdns’, enabled by default): `_sergw._tcp`
- Linux mock tools: PTY‑backed mock serial and TCP chat helper (Linux only)

**sergw ports**
### Install

**sergw listen** [*OPTIONS*] --serial _PORT_
- From crates.io (default features include mDNS):

## DESCRIPTION

sergw bridges communication between a serial device and multiple TCP clients. Data from the serial port is broadcast to all connected TCP clients. Data from any TCP client is forwarded to the serial port.

The gateway automatically reconnects to the serial port if the connection is lost. Graceful shutdown is supported via Ctrl-C.

## COMMANDS

### ports

Lists available USB serial ports. Outputs port names separated by spaces.

```bash
sergw ports
# Output: /dev/ttyUSB0 /dev/ttyUSB1 /dev/ttyACM0
```
cargo install sergw
```

### listen

Starts the TCP server and establishes serial communication.

**Required Options:**

- `--serial *PORT*` - Serial port device path (e.g., `/dev/ttyUSB0`, `COM3`)

**Optional Options:**

- `--baud *RATE*` - Serial baud rate (default: 57600)
- `--host *ADDRESS*` - TCP bind address and port (default: 127.0.0.1:5656)
- `-v, --verbose` - Enable verbose output
- Without mDNS:

## OPTIONS
```
cargo install sergw --no-default-features
```

| Option | Description | Default |
| --------------- | ------------------------- | -------------- |
| `--serial` | Serial port device path | Required |
| `--baud` | Serial baud rate | 57600 |
| `--host` | TCP bind address and port | 127.0.0.1:5656 |
| `-v, --verbose` | Enable verbose logging | false |
### Quick start

## EXAMPLES
Bridge a serial device on port 5656 (defaults shown):

```bash
sergw listen --serial /dev/ttyUSB0
sergw listen --serial /dev/ttyUSB0 --baud 115200 --host 0.0.0.0:8080
sergw listen --serial /dev/ttyUSB0 --verbose
sergw ports
```
sergw listen --serial /dev/ttyUSB0 --baud 115200 --host 127.0.0.1:5656
```

## ARCHITECTURE

The gateway uses multiple threads:
Connect a TCP client (e.g. `nc 127.0.0.1 5656`) to interact.

1. **Main Reconnect Loop** - Attempts to connect to the serial port and spawns a reader thread. If the connection is lost, it cleans up and retries after a delay.
### CLI

2. **Serial Reader Thread** - Reads data from the serial port and broadcasts to all TCP clients.
```
sergw
ports [--all] [--verbose] [--format text|json]
listen [--serial <PATH>] [--baud <u32>] [--host <addr:port>]
[--data-bits five|six|seven|eight]
[--parity none|odd|even]
[--stop-bits one|two]
[--buffer <usize>]
mock serial [--alias <PATH>] # Linux only
mock listener [--host <addr:port>] # Linux only
```

3. **Serial Writer Thread** - Receives data from TCP clients and writes to the serial port.
- `ports`: list serial ports (USB‑only by default). Use `--all` to include non‑USB. `--format json` for machine output.
- `listen`: start the bridge. If `--serial` is omitted and exactly one USB serial is present, it is auto‑selected; otherwise a helpful error is returned.
- `mock serial` (Linux): create a PTY that behaves like a serial device and open a TUI to interact.
- `mock listener` (Linux): connect to a TCP server with a TUI (handy for testing the bridge from the client side).

4. **TCP Listener Thread** - Accepts new TCP connections and spawns handler threads.
### mDNS / Bonjour (optional)

5. **TCP Client Handler Threads** - One per client, handles bidirectional data forwarding.
When built with the `mdns` feature (default), sergw advertises the service using type `_sergw._tcp`.

Thread coordination uses shared state (`Arc<Mutex<...>>`), message channels (`std::sync::mpsc`), and an atomic shutdown flag.
- Instance name: `sergw:<ttyname>` (e.g. `sergw:ttyUSB0`)
- TXT records: `provider=sergw`

## REQUIREMENTS
Disable mDNS by building without default features:

- Rust toolchain (install via [rustup](https://rustup.rs/))
- Serial port access permissions
- Linux: Add user to `dialout` group
- Windows: Administrator privileges may be required
```
cargo build --no-default-features
```

## TROUBLESHOOTING
### TUI overview

### Permission Denied on Serial Port
- Tabs: Overview (connections, throughput, events), Inspector (live dump)
- Inspector: formats (hex/ascii/dec), per‑device filter, pause/scroll
- Key hints in footer

**Linux:**
### Reliability & behavior

```bash
sudo usermod -a -G dialout $USER
# Log out and back in
```
- Serial auto‑reconnect on read/write failures; writer retries buffered write after reconnect
- TCP reader/writer per connection; on backpressure the connection is dropped rather than slowing others
- Raw byte forwarding (no framing, no higher protocols)

**Windows:**
### Exit codes

- Run as Administrator
- 2: no serial ports found for auto‑selection
- 3: multiple serial ports detected, explicit `--serial` required
- 4: bind‑like networking error (e.g. address in use)
- 5: serial open/error
- 1: other errors

### Serial Port Not Found
### Development

1. Verify device is connected and powered
2. Check if port is in use:
```bash
lsof /dev/ttyUSB0 # Linux
```
3. Try different baud rates
- Build: `cargo build --all-features`
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
- Tests: unit tests + Linux PTY integration test

### TCP Connection Issues
### License

1. **Port in use:**
```bash
netstat -tulpn | grep :5656 # Linux
```
2. **Firewall blocking:** Allow incoming connections on specified port
3. **Network access:** Use `0.0.0.0` instead of `127.0.0.1`
GPL‑3.0‑or‑later
1 change: 1 addition & 0 deletions apps/sergw/src/app/listen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use crate::net::server::run_listen;
1 change: 1 addition & 0 deletions apps/sergw/src/app/listener.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use crate::net::listener::run_chat;
4 changes: 4 additions & 0 deletions apps/sergw/src/app/mock/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod pty;
pub mod serial;
pub mod ui; // orchestrator
pub use serial::run_mock_serial;
18 changes: 18 additions & 0 deletions apps/sergw/src/app/mock/pty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#[cfg(target_os = "linux")]
use std::os::fd::AsRawFd;
#[cfg(target_os = "linux")]
use std::os::unix::io::OwnedFd;

#[cfg(target_os = "linux")]
use anyhow::Result;
#[cfg(target_os = "linux")]
use nix::pty::{openpty, OpenptyResult};

#[cfg(target_os = "linux")]
pub fn create_pty_pair() -> Result<(OwnedFd, OwnedFd, String)> {
let OpenptyResult { master, slave, .. } = openpty(None, None)?;
// Resolve stable path to the slave PTY for use as a serial device path
let slave_symlink = format!("/proc/self/fd/{}", slave.as_raw_fd());
let slave_path = std::fs::read_link(&slave_symlink)?;
Ok((master, slave, slave_path.to_string_lossy().into_owned()))
}
29 changes: 29 additions & 0 deletions apps/sergw/src/app/mock/serial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Orchestrates PTY creation and UI

#[cfg(target_os = "linux")]
use anyhow::Result;

#[cfg(target_os = "linux")]
pub fn run_mock_serial() -> Result<()> {
use super::pty::create_pty_pair;
use super::ui::run_mock_chat_with_title;

let (master, _slave_fd, slave_path) = create_pty_pair()?;

// Create a default temporary alias symlink for the slave path for the program duration
let alias_path = "/tmp/sergw-serial";
// ensure old alias is removed, then create new symlink; cleaned up by guard on exit
let _ = std::fs::remove_file(alias_path);
let _ = std::os::unix::fs::symlink(&slave_path, alias_path);

struct SymlinkGuard(&'static str);
impl Drop for SymlinkGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(self.0);
}
}
let _guard = SymlinkGuard(alias_path);

run_mock_chat_with_title(master, format!("mock serial | {alias_path}"))?;
Ok(())
}
Loading
Loading