From 8f3a762d7b0438a2e1ada63d85c6c30f91ad73d7 Mon Sep 17 00:00:00 2001 From: Colin S <3526918+cbs228@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:44:40 -0600 Subject: [PATCH 1/3] sameold,samedec: report if message is national Report valid national periodic tests and national emergencies via `MessageHeader::is_national()`. Both the event and location codes are checked. These messages are reported to samedec child processes via the `$SAMEDEC_IS_NATIONAL` environment variable, which is set to "`Y`" for national messages and is empty otherwise. This is an API-expanding and CLI-expanding change. --- crates/samedec/README.md | 6 ++++ crates/samedec/src/spawner.rs | 32 +++++++++++++++++++++ crates/sameold/src/message.rs | 46 ++++++++++++++++++++++++++++-- sample/long_message.22050.s16le.sh | 1 + sample/npt.22050.s16le.sh | 1 + sample/two_and_two.22050.s16le.sh | 1 + 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/crates/samedec/README.md b/crates/samedec/README.md index 80bfec5..7a72af5 100644 --- a/crates/samedec/README.md +++ b/crates/samedec/README.md @@ -306,6 +306,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 diff --git a/crates/samedec/src/spawner.rs b/crates/samedec/src/spawner.rs index 47f4c98..ca8b75f 100644 --- a/crates/samedec/src/spawner.rs +++ b/crates/samedec/src/spawner.rs @@ -62,6 +62,10 @@ where .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() } @@ -143,6 +147,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 +170,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..7429290 100644 --- a/sample/long_message.22050.s16le.sh +++ b/sample/long_message.22050.s16le.sh @@ -11,5 +11,6 @@ exec 0>/dev/null [ "$SAMEDEC_SIGNIFICANCE" = "W" ] [ "$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..392bb01 100644 --- a/sample/npt.22050.s16le.sh +++ b/sample/npt.22050.s16le.sh @@ -9,6 +9,7 @@ exec 0>/dev/null [ "$SAMEDEC_ORG" = "PEP" ] [ "$SAMEDEC_SIGNIFICANCE" = "T" ] [ "$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..f8bf4e6 100644 --- a/sample/two_and_two.22050.s16le.sh +++ b/sample/two_and_two.22050.s16le.sh @@ -8,6 +8,7 @@ exec 0>/dev/null [ "$SAMEDEC_EVENT" = "Severe Thunderstorm Warning" ] [ "$SAMEDEC_ORIGINATOR" = "National Weather Service" ] [ "$SAMEDEC_SIGNIFICANCE" = "W" ] +[ "$SAMEDEC_IS_NATIONAL" = "" ] lifetime=$(( SAMEDEC_PURGETIME - SAMEDEC_ISSUETIME)) [ "$lifetime" -eq $(( 1*60*60 + 30*60 )) ] From cb13b8c5fe5d04110cc96aae08210f014f4ec1e4 Mon Sep 17 00:00:00 2001 From: Colin S <3526918+cbs228@users.noreply.github.com> Date: Sat, 24 Feb 2024 11:41:42 -0600 Subject: [PATCH 2/3] samedec: add SAMEDEC_SIG_NUM environment variable Add the `$SAMEDEC_SIG_NUM` environment variable, which contains a numeric representation of the significance level. This is a CLI-expanding change. --- crates/samedec/README.md | 25 ++++++++++++++---- crates/samedec/src/spawner.rs | 41 +++++++++++++++++++++++++----- sample/long_message.22050.s16le.sh | 1 + sample/npt.22050.s16le.sh | 1 + sample/two_and_two.22050.s16le.sh | 1 + 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/crates/samedec/README.md b/crates/samedec/README.md index 7a72af5..0e255f3 100644 --- a/crates/samedec/README.md +++ b/crates/samedec/README.md @@ -282,11 +282,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`" diff --git a/crates/samedec/src/spawner.rs b/crates/samedec/src/spawner.rs index ca8b75f..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,10 +55,14 @@ 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) @@ -117,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 diff --git a/sample/long_message.22050.s16le.sh b/sample/long_message.22050.s16le.sh index 7429290..3189577 100644 --- a/sample/long_message.22050.s16le.sh +++ b/sample/long_message.22050.s16le.sh @@ -9,6 +9,7 @@ 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" = "" ] diff --git a/sample/npt.22050.s16le.sh b/sample/npt.22050.s16le.sh index 392bb01..8bf6409 100644 --- a/sample/npt.22050.s16le.sh +++ b/sample/npt.22050.s16le.sh @@ -8,6 +8,7 @@ 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" ] diff --git a/sample/two_and_two.22050.s16le.sh b/sample/two_and_two.22050.s16le.sh index f8bf4e6..a180011 100644 --- a/sample/two_and_two.22050.s16le.sh +++ b/sample/two_and_two.22050.s16le.sh @@ -8,6 +8,7 @@ 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)) From 760b476c1e59ac8bd0b804b7e7def62c037db95b Mon Sep 17 00:00:00 2001 From: Colin S <3526918+cbs228@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:26:02 -0600 Subject: [PATCH 3/3] samedec: doc: update with new environment variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the samedec README to use the new environment variables. Many distros have replaced pulseaudio with PipeWire, and `paplay` is no longer available on Ubuntu 23.10. While we could switch to `pw-play`—the PipeWire equivalent—rewrite the tutorial around sox instead. --- crates/samedec/README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/samedec/README.md b/crates/samedec/README.md index 0e255f3..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 > ``` @@ -368,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: