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 = 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 --zones ... [--melody ] [--volume ] [--repeats ] Options: -i, --ip IP address to send to [default: 239.192.55.1] -p, --port UDP port [default: 1681] -c, --command Command type: melody, alarm, stop -m, --melody Melody number (1-30, required for melody/alarm) -v, --volume Volume level (1-8, required for melody/alarm) -r, --repeats Repeat count (0=infinite, 1-8, required for melody/alarm) -z, --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 = None; let mut melody: Option = None; let mut volume: Option = None; let mut repeats: Option = None; let mut zones: Vec = 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 = 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::>().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 = 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::>().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); } } }