diff --git a/crates/samedec/README.md b/crates/samedec/README.md index 80bfec5..5cb1a5c 100644 --- a/crates/samedec/README.md +++ b/crates/samedec/README.md @@ -96,18 +96,19 @@ sometimes referred to in your audio drivers as `s16ne`. The sampling `--rate` you set in `samedec` must match the sampling rate of the signal you are piping in. `samedec`'s demodulator will be designed for whatever `--rate` you request, and it can work with a variety of sampling rates. We -recommend using at least `8000` Hz. Higher sampling rates will cause `samedec` -to use more CPU and I/O throughput, but the difference may not be particularly -important on most systems. +recommend using your sound card's native sampling rate, which is often either +`44100` Hz or `48000` Hz. On linux, you can obtain piped audio with either +[`pw-record`](https://manpages.ubuntu.com/manpages/lunar/man1/pw-play.1.html) +(PipeWire), [`parec`](https://manpages.debian.org/testing/pulseaudio-utils/parec.1.en.html) -(PulseAudio) or +(PulseAudio), or [`arecord`](https://manpages.debian.org/testing/alsa-utils/arecord.1.en.html) -(ALSA). Both are preinstalled on most desktop distributions. +(ALSA). Most desktop distributions have at least one of these preinstalled. ```bash -parec --channels 1 --format s16ne --rate 22050 --latency-msec 500 \ +pw-record --channels 1 --format s16 --rate 22050 -- - \ | samedec -r 22050 ``` @@ -168,6 +169,9 @@ broken. > from Wikimedia Commons. Running: > > ```bash +> curl -C - -o Same.wav \ +> https://upload.wikimedia.org/wikipedia/commons/2/25/Same.wav +> > sox 'Same.wav' -t raw -r 22.05k -e signed -b 16 -c 1 - | \ > samedec -r 22050 > ``` @@ -282,11 +286,26 @@ The child process receives the following additional environment variables: be empty if the significance level could not be determined (i.e., because the event code is unknown). - * `T`: Test - * `S`: Statement - * `E`: Emergency - * `A`: Watch - * `W`: Warning + |  |  | + |-------|-----------------| + | "`T`" | Test | + | "`S`" | Statement | + | "`E`" | Emergency | + | "`A`" | Watch | + | "`W`" | Warning | + | "" | Unknown | + +* `SAMEDEC_SIG_NUM`: significance level, expressed as a whole number + in increasing order of severity. + + |  |  | + |-------|-----------------| + | "`0`" | Test | + | "`1`" | Statement | + | "`2`" | Emergency | + | "`3`" | Watch | + | "`4`" | Warning | + | "`5`" | Unknown | * `SAMEDEC_LOCATIONS`: *space-delimited* list of FIPS location codes, which are six characters long. Example: "`012057 012081`" @@ -306,6 +325,12 @@ The child process receives the following additional environment variables: Remember: the purge time is the expiration time of the *message* and *not* the expected duration of the hazard. +* `SAMEDEC_IS_NATIONAL`: Set to "`Y`" if the message contains a recognized + national-level event and location code. The message may either be a test or + an actual emergency. Clients are **strongly encouraged** to always play + national-level messages and to never provide the option to suppress them. + For non-national messages, this variable is set to the empty string. + ### Design Requirements for Child Processes `samedec` provides child processes with input samples synchronously, via @@ -347,17 +372,21 @@ must have the execute bit set (`chmod +x …`). ```bash #!/bin/bash -[ "${SAMEDEC_SIGNIFICANCE}" = "W" ] || exit 0 +[[ -n "${SAMEDEC_IS_NATIONAL}" || "${SAMEDEC_SIG_NUM}" -ge 4 ]] || exit 0 -exec pacat --channels 1 --format s16ne \ - --rate "${SAMEDEC_RATE}" --latency-msec 500 "$@" +exec play -q -t raw --rate "${SAMEDEC_RATE}" -e signed -b 16 -c 1 - "$@" ``` -The above script will use pulseaudio (on linux) to play back any message which -has a significance level of Warning (`W`). We use `exec` to replace the running -shell with `pacat`. `--rate "${SAMEDEC_RATE}"` tells `pacat` what the sampling -rate is. The "`$@`" is a bashism which passes the remaining input arguments to -the script to `pacat` as arguments. +The above script will use sox to play back any message which: + +1. is a national-level activation; **OR** +2. has a significance level of at least Warning + +We use `exec` to replace the running shell with `play`. +`--rate "${SAMEDEC_RATE}"` tells sox what the sampling rate is. +The "`$@`" is a bashism which passes the remaining input arguments to +the script to sox as arguments. + If you name this script `./play_on_warn.sh`, then an example invocation of `samedec` is: diff --git a/crates/samedec/src/spawner.rs b/crates/samedec/src/spawner.rs index 47f4c98..8cd3028 100644 --- a/crates/samedec/src/spawner.rs +++ b/crates/samedec/src/spawner.rs @@ -40,6 +40,7 @@ where }; let locations: Vec<&str> = header.location_str_iter().collect(); + let evt = header.event(); Command::new(cmd) .stdin(Stdio::piped()) @@ -54,14 +55,22 @@ where header.originator().as_display_str(), ) .env(childenv::SAMEDEC_EVT, header.event_str()) - .env(childenv::SAMEDEC_EVENT, header.event().to_string()) + .env(childenv::SAMEDEC_EVENT, evt.to_string()) .env( childenv::SAMEDEC_SIGNIFICANCE, - header.event().significance().as_code_str(), + evt.significance().as_code_str(), + ) + .env( + childenv::SAMEDEC_SIG_NUM, + (evt.significance() as u8).to_string(), ) .env(childenv::SAMEDEC_LOCATIONS, locations.join(" ")) .env(childenv::SAMEDEC_ISSUETIME, issue_ts) .env(childenv::SAMEDEC_PURGETIME, purge_ts) + .env( + childenv::SAMEDEC_IS_NATIONAL, + bool_to_env(header.is_national()), + ) .spawn() } @@ -113,13 +122,35 @@ mod childenv { /// Significance levels are assigned by the `sameold` /// developers. /// - /// * `T`: Test - /// * `S`: Statement - /// * `E`: Emergency - /// * `A`: Watch - /// * `W`: Warning + /// |  |  | + /// |-------|-----------------| + /// | "`T`" | Test | + /// | "`S`" | Statement | + /// | "`E`" | Emergency | + /// | "`A`" | Watch | + /// | "`W`" | Warning | + /// | "``" | Unknown | pub const SAMEDEC_SIGNIFICANCE: &str = "SAMEDEC_SIGNIFICANCE"; + /// SAME event significance level, numeric + /// + /// The significance level assigned to the SAME event code, + /// expressed as a whole number in increasing order of + /// severity. + /// + /// Significance levels are assigned by the `sameold` + /// developers. + /// + /// |  |  | + /// |-------|-----------------| + /// | "`0`" | Test | + /// | "`1`" | Statement | + /// | "`2`" | Emergency | + /// | "`3`" | Watch | + /// | "`4`" | Warning | + /// | "`5`" | Unknown | + pub const SAMEDEC_SIG_NUM: &str = "SAMEDEC_SIG_NUM"; + /// FIPS code locations /// /// Area(s) affected by the message, as a space-delimited list @@ -143,6 +174,22 @@ mod childenv { /// clock. It will be empty if a complete timestamp cannot be /// calculated. pub const SAMEDEC_PURGETIME: &str = "SAMEDEC_PURGETIME"; + + /// True if the message is a national activation + /// + /// This variable is set to `Y` if: + /// + /// - the location code in the SAME message indicates + /// national applicability; and + /// + /// - the event code is reserved for national use + /// + /// Otherwise, this variable is set to the empty string. + /// + /// The message may either be a national test or a national emergency. + /// Clients are **strongly encouraged** to always play national-level + /// messages and to never provide the option to suppress them. + pub const SAMEDEC_IS_NATIONAL: &str = "SAMEDEC_IS_NATIONAL"; } // convert DateTime to UTC unix timestamp in seconds, as string @@ -150,6 +197,18 @@ fn time_to_unix_str(tm: DateTime) -> String { format!("{}", tm.format("%s")) } +// convert true → "Y", false → "" +// +// this is useful for environment variables since empty values +// are usually treated as false +fn bool_to_env(val: bool) -> &'static str { + if val { + "Y" + } else { + "" + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/sameold/src/message.rs b/crates/sameold/src/message.rs index 6f2c37c..b948af0 100644 --- a/crates/sameold/src/message.rs +++ b/crates/sameold/src/message.rs @@ -367,8 +367,7 @@ impl MessageHeader { /// Per the SAME standard, a message can have up to 31 /// location codes. pub fn location_str_iter<'m>(&'m self) -> std::str::Split<'m, char> { - let locations = &self.message[Self::OFFSET_AREA_START..self.offset_time]; - locations.split('-') + self.location_str().split('-') } /// Message validity duration (Duration) @@ -529,6 +528,25 @@ impl MessageHeader { self.voting_byte_count } + /// True if the message is a national activation + /// + /// Returns true if: + /// + /// - the location code in the SAME message indicates + /// national applicability; and + /// + /// - the event code is reserved for national use + /// + /// The message may either be a test or an actual emergency. + /// Consult the [`event()`](MessageHeader::event) for details. + /// + /// Clients are **strongly encouraged** to always play + /// national-level messages and to never provide the option to + /// suppress them. + pub fn is_national(&self) -> bool { + self.location_str() == Self::LOCATION_NATIONAL && self.event().phenomenon().is_national() + } + /// Obtain the owned message String /// /// Destroys this object and releases the message @@ -537,6 +555,11 @@ impl MessageHeader { self.message } + /// The location portion of the message string + fn location_str(&self) -> &str { + &self.message[Self::OFFSET_AREA_START..self.offset_time] + } + const OFFSET_ORG: usize = 5; const OFFSET_EVT: usize = 9; const OFFSET_AREA_START: usize = 13; @@ -545,6 +568,7 @@ impl MessageHeader { const OFFSET_FROMPLUS_CALLSIGN: usize = 14; const OFFSET_FROMEND_CALLSIGN_END: usize = 1; const PANIC_MSG: &'static str = "MessageHeader validity check admitted a malformed message"; + const LOCATION_NATIONAL: &'static str = "000000"; } impl fmt::Display for Message { @@ -830,6 +854,7 @@ mod tests { assert_eq!(msg.callsign(), "NOCALL00"); assert_eq!(msg.parity_error_count(), 6); assert_eq!(msg.voting_byte_count(), msg.as_str().len()); + assert!(!msg.is_national()); let loc: Vec<&str> = msg.location_str_iter().collect(); assert_eq!(loc.as_slice(), &["012345", "567890", "888990"]); @@ -870,4 +895,21 @@ mod tests { let msg = Message::try_from("NN".to_owned()).expect("bad msg"); assert_eq!(Message::EndOfMessage, msg); } + + #[test] + fn test_is_national() { + let national = MessageHeader::new("ZCZC-PEP-NPT-000000+0030-2771820-TEST -").unwrap(); + assert!(national.is_national()); + + let national = MessageHeader::new("ZCZC-PEP-EAN-000000+0030-2771820-TEST -").unwrap(); + assert!(national.is_national()); + + let not_national = + MessageHeader::new("ZCZC-PEP-NPT-000001+0030-2771820-TEST -").unwrap(); + assert!(!not_national.is_national()); + + let not_national = + MessageHeader::new("ZCZC-PEP-NPT-000000-000001+0030-2771820-TEST -").unwrap(); + assert!(!not_national.is_national()); + } } diff --git a/sample/long_message.22050.s16le.sh b/sample/long_message.22050.s16le.sh index 6abde37..3189577 100644 --- a/sample/long_message.22050.s16le.sh +++ b/sample/long_message.22050.s16le.sh @@ -9,7 +9,9 @@ exec 0>/dev/null [ "$SAMEDEC_EVENT" = "Practice/Demo Warning" ] [ "$SAMEDEC_ORG" = "EAS" ] [ "$SAMEDEC_SIGNIFICANCE" = "W" ] +[ "$SAMEDEC_SIG_NUM" -eq 4 ] [ "$SAMEDEC_LOCATIONS" = "372088 091724 919623 645687 745748 175234 039940 955869 091611 304171 931612 334828 179485 569615 809223 830187 611340 014693 472885 084645 977764 466883 406863 390018 701741 058097 752790 311648 820127 255900 581947" ] [ "$SAMEDEC_ISSUETIME" = "$SAMEDEC_PURGETIME" ] +[ "$SAMEDEC_IS_NATIONAL" = "" ] echo "+OK" diff --git a/sample/npt.22050.s16le.sh b/sample/npt.22050.s16le.sh index 76aad32..8bf6409 100644 --- a/sample/npt.22050.s16le.sh +++ b/sample/npt.22050.s16le.sh @@ -8,7 +8,9 @@ exec 0>/dev/null [ "$SAMEDEC_EVENT" = "National Periodic Test" ] [ "$SAMEDEC_ORG" = "PEP" ] [ "$SAMEDEC_SIGNIFICANCE" = "T" ] +[ "$SAMEDEC_SIG_NUM" -eq 0 ] [ "$SAMEDEC_LOCATIONS" = "000000" ] +[ "$SAMEDEC_IS_NATIONAL" = "Y" ] lifetime=$(( SAMEDEC_PURGETIME - SAMEDEC_ISSUETIME)) [ "$lifetime" -eq $(( 30*60 )) ] diff --git a/sample/two_and_two.22050.s16le.sh b/sample/two_and_two.22050.s16le.sh index a67636a..a180011 100644 --- a/sample/two_and_two.22050.s16le.sh +++ b/sample/two_and_two.22050.s16le.sh @@ -8,6 +8,8 @@ exec 0>/dev/null [ "$SAMEDEC_EVENT" = "Severe Thunderstorm Warning" ] [ "$SAMEDEC_ORIGINATOR" = "National Weather Service" ] [ "$SAMEDEC_SIGNIFICANCE" = "W" ] +[ "$SAMEDEC_SIG_NUM" -eq 4 ] +[ "$SAMEDEC_IS_NATIONAL" = "" ] lifetime=$(( SAMEDEC_PURGETIME - SAMEDEC_ISSUETIME)) [ "$lifetime" -eq $(( 1*60*60 + 30*60 )) ]