summaryrefslogtreecommitdiff
path: root/src/sigma-sender.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/sigma-sender.rs')
-rw-r--r--src/sigma-sender.rs317
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