forked from n6cta/baecun
Initial commit of source for v0.1.0
This commit is contained in:
177
src/main.rs
Normal file
177
src/main.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user