1
0
forked from n6cta/baecun

Initial commit of source for v0.1.0

This commit is contained in:
2025-03-01 00:31:28 -08:00
parent 67d961aef3
commit 248e1f0a1c
4 changed files with 318 additions and 1 deletions

177
src/main.rs Normal file
View File

@@ -0,0 +1,177 @@
use clap::{Parser, ArgGroup};
use std::io::{self, Read, Write};
use std::net::{IpAddr, TcpStream};
/// Validate IP address.
fn validate_ip(ip: &str) -> Result<IpAddr, String> {
ip.parse::<IpAddr>()
.map_err(|_| format!("Invalid IP address format: {}", ip))
}
/// Validate the port number (reject 0).
fn validate_port(port: &str) -> Result<u16, String> {
match port.parse::<u16>() {
Ok(p) if p != 0 => Ok(p),
_ => Err(format!("Invalid port number: {}. Port must be greater than 0.", port)),
}
}
/// Validate callsigns (no more than 9 characters, alphanumeric, and dashes).
fn validate_callsign(call: &str) -> Result<String, String> {
if call.len() <= 9 && call.chars().all(|c| c.is_alphanumeric() || c == '-') {
Ok(call.to_string())
} else {
Err("Callsign must be 9 characters or less and can only contain alphanumeric characters and dashes".to_string())
}
}
/// Validate AGWPE channel (must be between 0 and 255).
fn validate_channel(channel: &str) -> Result<u8, String> {
match channel.parse::<u8>() {
Ok(c) => Ok(c),
Err(_) => Err(format!("Invalid channel: {}. Channel must be between 0 and 255.", channel)),
}
}
/// Constants for frame construction.
const RESERVED_3: [u8; 3] = [0x00, 0x00, 0x00];
const DATA_KIND_M: u8 = 0x4D;
const DATA_KIND_V: u8 = 0x56;
const RESERVED_1: u8 = 0x00;
const PID_AX25: u8 = 0xF0;
const USER_RESERVED: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
/// Helper to encode a callsign into a fixed 10-byte field.
fn encode_callsign(call: &str) -> [u8; 10] {
let mut buf = [0u8; 10];
let bytes = call.as_bytes();
let len = bytes.len().min(9);
buf[..len].copy_from_slice(&bytes[..len]);
buf
}
/// Command-line arguments parser.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[clap(group(
ArgGroup::new("input")
.required(false)
.args(&["message"])
))]
struct Cli {
#[arg(short = 'i', long, value_parser = validate_ip)]
ip: IpAddr,
#[arg(short = 'p', long, value_parser = validate_port)]
port: u16,
#[arg(short = 'f', long, value_parser = validate_callsign)]
from_call: String,
#[arg(short = 't', long, value_parser = validate_callsign)]
to_call: String,
#[arg(short = 'm', long)]
message: Option<String>,
#[arg(short = 'c', long, value_parser = validate_channel, default_value_t = 0)]
channel: u8,
#[arg(short = 'd', long, value_parser = validate_callsign, num_args(1..=7))]
digis: Vec<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse the command-line arguments.
let cli = Cli::parse();
// Get the message either from the '-m' flag or from stdin.
let message = if let Some(msg) = cli.message {
let trimmed = msg.trim_end();
if trimmed.as_bytes().len() > 255 {
return Err("Message must be 255 bytes or less".into());
}
trimmed.as_bytes().to_vec()
} else {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
let trimmed = buffer.trim_end();
trimmed.as_bytes().to_vec()
};
// Build the frame based on whether digipeaters are provided.
let frame = if cli.digis.is_empty() {
build_m_frame(cli.channel, &cli.from_call, &cli.to_call, &message)
} else {
let digipeaters: Vec<&str> = cli.digis.iter().map(String::as_str).collect();
build_v_frame(cli.channel, &cli.from_call, &cli.to_call, &digipeaters, &message)
};
// Send the frame.
send_frame(&frame, &cli.ip.to_string(), cli.port)?;
Ok(())
}
/// Build 'M' frame (unproto without digipeaters).
fn build_m_frame(port: u8, source_call: &str, destination_call: &str, message: &[u8]) -> Vec<u8> {
let mut frame = Vec::new();
frame.push(port); // AGWPE Port.
frame.extend_from_slice(&RESERVED_3);
frame.push(DATA_KIND_M); // DataKind 'M'.
frame.push(RESERVED_1);
frame.push(PID_AX25); // PID for AX.25.
frame.push(RESERVED_1);
frame.extend_from_slice(&encode_callsign(source_call));
frame.extend_from_slice(&encode_callsign(destination_call));
frame.extend_from_slice(&(message.len() as u32).to_le_bytes());
frame.extend_from_slice(&USER_RESERVED);
frame.extend_from_slice(message);
frame
}
/// Build 'V' frame (unproto with digipeaters).
fn build_v_frame(
port: u8,
source_call: &str,
destination_call: &str,
digipeaters: &[&str],
message: &[u8],
) -> Vec<u8> {
let mut frame = Vec::new();
frame.push(port);
frame.extend_from_slice(&RESERVED_3);
frame.push(DATA_KIND_V); // DataKind 'V'.
frame.push(RESERVED_1);
frame.push(PID_AX25);
frame.push(RESERVED_1);
frame.extend_from_slice(&encode_callsign(source_call));
frame.extend_from_slice(&encode_callsign(destination_call));
let data_len = 1 + digipeaters.len() * 10 + message.len();
frame.extend_from_slice(&(data_len as u32).to_le_bytes());
frame.extend_from_slice(&USER_RESERVED);
frame.push(digipeaters.len() as u8);
for digipeater in digipeaters {
frame.extend_from_slice(&encode_callsign(digipeater));
}
frame.extend_from_slice(message);
frame
}
/// Send the frame over TCP to the AGWPE server.
fn send_frame(frame: &[u8], host: &str, port: u16) -> io::Result<()> {
let addr = format!("{}:{}", host, port);
let mut stream = TcpStream::connect(addr)?;
stream.write_all(frame)?;
Ok(())
}