diff options
author | crt mlol <crt@teleco.ch> | 2025-06-18 16:48:11 +0200 |
---|---|---|
committer | crt mlol <crt@teleco.ch> | 2025-06-18 16:48:11 +0200 |
commit | 4c8b342a4353efedb2e4ade26d74231b6aa671d5 (patch) | |
tree | 87081396cc603bf99e8dbdc8836ed310a5f9d866 /src/sigma-sender.rs |
i want to skibidi
Diffstat (limited to 'src/sigma-sender.rs')
-rw-r--r-- | src/sigma-sender.rs | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/src/sigma-sender.rs b/src/sigma-sender.rs new file mode 100644 index 0000000..c31e6d6 --- /dev/null +++ b/src/sigma-sender.rs @@ -0,0 +1,317 @@ +use std::env; +use std::net::UdpSocket; + +#[derive(Clone, Copy)] +enum CommandType { + Melody, + Alarm, + Stop, +} + +impl CommandType { + fn code(&self) -> u16 { + match self { + CommandType::Melody => 0x3001, + CommandType::Alarm => 0x5001, + CommandType::Stop => 0x5002, + } + } + + fn name(&self) -> &'static str { + match self { + CommandType::Melody => "melody", + CommandType::Alarm => "alarm", + CommandType::Stop => "stop", + } + } +} + +fn compute_psa_checksum(data: &[u8]) -> [u8; 2] { + let mut var_e: u16 = 0x0000; + for (i, &b) in data.iter().enumerate() { + var_e ^= (b as u16).wrapping_add(i as u16) & 0xFFFF; + } + var_e.to_be_bytes() +} + +fn build_zone_bytes(zones: &[u8]) -> [u8; 12] { + let mut zone_bytes = [0u8; 12]; + + // Special case: zone 0 means all zones (set all bits) + if zones.contains(&0) { + return [0xff; 12]; + } + + for &zone in zones { + if zone > 100 { + continue; + } + let idx = (zone - 1) / 8; + let bit = (zone - 1) % 8; + zone_bytes[idx as usize] |= 1 << bit; + } + zone_bytes +} + +fn main() { + let args: Vec<String> = env::args().collect(); + + // Show help if no arguments provided + if args.len() == 1 { + eprintln!("sigma-sender - Send command packets to Bodet PSA System + +Usage: sigma-sender [OPTIONS] --command <COMMAND> --zones <ZONES>... [--melody <MELODY>] [--volume <VOLUME>] [--repeats <REPEATS>] + +Options: + -i, --ip <IP> IP address to send to [default: 239.192.55.1] + -p, --port <PORT> UDP port [default: 1681] + -c, --command <COMMAND> Command type: melody, alarm, stop + -m, --melody <MELODY> Melody number (1-30, required for melody/alarm) + -v, --volume <VOLUME> Volume level (1-8, required for melody/alarm) + -r, --repeats <REPEATS> Repeat count (0=infinite, 1-8, required for melody/alarm) + -z, --zones <ZONES>... Zone numbers (0=all zones, 1-100, space-separated) + --stop-all Quick stop all zones (equivalent to: --command stop --zones 0) + --debug Show debug information + -h, --help Print help + +Examples: + sigma-sender --command melody -m 5 -v 4 -r 2 -z 8 + # Send melody 5, volume 4, repeat 2 times to zone 8 + sigma-sender --command alarm -m 9 -v 8 -r 0 -z 1 2 3 + # Send alarm 9, volume 8, infinite repeats to zones 1, 2, and 3 + sigma-sender --command stop -z 0 + # Stop all zones + sigma-sender --stop-all + # Quick stop all zones + sigma-sender --debug --command melody -m 1 -v 1 -r 1 -z 8 + # Send melody 1 to zone 8 and show packet hex + +Notes: + - Commands: melody (3001), alarm (5001), stop (5002) + - Melodies: 1-30 (hex in protocol, decimal here) + - Volume: 1-8 (max 8) + - Repeats: 0=infinite, 1-8 (finite) + - Zones: 0=all zones, 1-100 (multiple allowed) + - Stop command ignores melody/volume/repeats parameters + - Defaults: IP=239.192.55.1, Port=1681"); + std::process::exit(1); + } + + // Parse command line arguments + let mut ip = "239.192.55.1".to_string(); + let mut port = 1681u16; + let mut debug = false; + let mut command: Option<CommandType> = None; + let mut melody: Option<u8> = None; + let mut volume: Option<u8> = None; + let mut repeats: Option<u8> = None; + let mut zones: Vec<u8> = Vec::new(); + let mut stop_all = false; + + let mut idx = 1; + while idx < args.len() { + match args[idx].as_str() { + "-i" | "--ip" => { + if idx + 1 < args.len() { + ip = args[idx + 1].clone(); + idx += 2; + } else { + eprintln!("Error: --ip requires a value"); + std::process::exit(1); + } + } + "-p" | "--port" => { + if idx + 1 < args.len() { + port = args[idx + 1].parse().unwrap_or_else(|_| { + eprintln!("Error: Invalid port number"); + std::process::exit(1); + }); + idx += 2; + } else { + eprintln!("Error: --port requires a value"); + std::process::exit(1); + } + } + "-c" | "--command" => { + if idx + 1 < args.len() { + command = Some(match args[idx + 1].as_str() { + "melody" => CommandType::Melody, + "alarm" => CommandType::Alarm, + "stop" => CommandType::Stop, + _ => { + eprintln!("Error: Invalid command type. Use: melody, alarm, or stop"); + std::process::exit(1); + } + }); + idx += 2; + } else { + eprintln!("Error: --command requires a value"); + std::process::exit(1); + } + } + "-m" | "--melody" => { + if idx + 1 < args.len() { + melody = Some(args[idx + 1].parse().unwrap_or_else(|_| { + eprintln!("Error: Invalid melody number"); + std::process::exit(1); + })); + idx += 2; + } else { + eprintln!("Error: --melody requires a value"); + std::process::exit(1); + } + } + "-v" | "--volume" => { + if idx + 1 < args.len() { + volume = Some(args[idx + 1].parse().unwrap_or_else(|_| { + eprintln!("Error: Invalid volume level"); + std::process::exit(1); + })); + idx += 2; + } else { + eprintln!("Error: --volume requires a value"); + std::process::exit(1); + } + } + "-r" | "--repeats" => { + if idx + 1 < args.len() { + repeats = Some(args[idx + 1].parse().unwrap_or_else(|_| { + eprintln!("Error: Invalid repeat count"); + std::process::exit(1); + })); + idx += 2; + } else { + eprintln!("Error: --repeats requires a value"); + std::process::exit(1); + } + } + "-z" | "--zones" => { + idx += 1; + while idx < args.len() && !args[idx].starts_with('-') { + let zone = args[idx].parse().unwrap_or_else(|_| { + eprintln!("Error: Invalid zone number: {}", args[idx]); + std::process::exit(1); + }); + zones.push(zone); + idx += 1; + } + } + "--stop-all" => { + stop_all = true; + idx += 1; + } + "--debug" => { + debug = true; + idx += 1; + } + "-h" | "--help" => { + std::process::exit(0); + } + _ => { + eprintln!("Error: Unknown argument: {}", args[idx]); + std::process::exit(1); + } + } + } + + // Handle --stop-all shortcut + if stop_all { + command = Some(CommandType::Stop); + zones = vec![0]; // All zones + } + + // Validate required arguments + let command = command.unwrap_or_else(|| { + eprintln!("Error: --command is required (use --stop-all for quick stop)"); + std::process::exit(1); + }); + + if zones.is_empty() { + eprintln!("Error: --zones is required"); + std::process::exit(1); + } + + // Validate command-specific requirements + match command { + CommandType::Melody | CommandType::Alarm => { + let melody = melody.unwrap_or_else(|| { + eprintln!("Error: --melody is required for {} command", command.name()); + std::process::exit(1); + }); + let volume = volume.unwrap_or_else(|| { + eprintln!("Error: --volume is required for {} command", command.name()); + std::process::exit(1); + }); + let repeats = repeats.unwrap_or_else(|| { + eprintln!("Error: --repeats is required for {} command", command.name()); + std::process::exit(1); + }); + + // Validate ranges + if !(1..=30).contains(&melody) || !(1..=8).contains(&volume) || repeats > 8 { + eprintln!("Error: Melody (1-30), Volume (1-8), Repeats (0-8, 0=infinite)"); + std::process::exit(1); + } + + // Build melody/alarm packet + let mut packet: Vec<u8> = Vec::new(); + packet.extend_from_slice(b"MEL"); + packet.extend_from_slice(&0x0021u16.to_be_bytes()); + packet.extend_from_slice(&[0x01, 0x00]); + packet.extend_from_slice(&[0x28, 0xff]); + packet.extend_from_slice(&command.code().to_be_bytes()); + packet.extend_from_slice(&build_zone_bytes(&zones)); + packet.extend_from_slice(&[0x00, 0x01]); + packet.push(volume); + packet.push(repeats); + packet.push(0x01); + packet.push(melody); + packet.extend_from_slice(&[0x01, 0x00]); + + let checksum = compute_psa_checksum(&packet); + packet.extend_from_slice(&checksum); + + if debug { + let hexstr = packet.iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join(""); + println!("DEBUG: Packet hex: {}", hexstr); + } + + let addr = format!("{}:{}", ip, port); + let sock = UdpSocket::bind("0.0.0.0:0").expect("bind failed"); + sock.set_multicast_ttl_v4(2).ok(); + sock.send_to(&packet, &addr).expect("send failed"); + + let repeat_str = if repeats == 0 { "infinite".to_string() } else { repeats.to_string() }; + println!("Sent {} {} (vol {}, rep {}, zones {:?}) to {}", + command.name(), melody, volume, repeat_str, zones, addr); + } + CommandType::Stop => { + // Build stop packet (different format, shorter) + let mut packet: Vec<u8> = Vec::new(); + packet.extend_from_slice(b"MEL"); + packet.extend_from_slice(&0x001bu16.to_be_bytes()); // 27 bytes for stop (to accommodate 2-byte checksum) + packet.extend_from_slice(&[0x01, 0x00]); + packet.extend_from_slice(&[0x2d, 0xff]); // Different sequence for stop + packet.extend_from_slice(&command.code().to_be_bytes()); + packet.extend_from_slice(&build_zone_bytes(&zones)); + packet.extend_from_slice(&[0x0f, 0x01]); // Different ending for stop + + let checksum = compute_psa_checksum(&packet); + packet.extend_from_slice(&checksum); // This adds 2 bytes + + if debug { + let hexstr = packet.iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join(""); + println!("DEBUG: Packet hex: {}", hexstr); + println!("DEBUG: Packet length: {} bytes", packet.len()); + } + + let addr = format!("{}:{}", ip, port); + let sock = UdpSocket::bind("0.0.0.0:0").expect("bind failed"); + sock.set_multicast_ttl_v4(2).ok(); + sock.send_to(&packet, &addr).expect("send failed"); + + let zone_str = if zones.contains(&0) { "all zones".to_string() } else { format!("zones {:?}", zones) }; + println!("Sent stop command to {} at {}", zone_str, addr); + } + } +}
\ No newline at end of file |