Skip to content

Commit

Permalink
Merge branch 'feature/more_samedec_vars' into develop
Browse files Browse the repository at this point in the history
Add the following new environment variables to spawned child
processes:

* `SAMEDEC_IS_NATIONAL`: "`Y`" for national activations;
  otherwise present but empty

* `SAMEDEC_SIG_NUM`: a numeric representation of the
  significance level

The samedec README is updated to account for Ubuntu's migration
from pulseaudio to PipeWire.

This is an API-expanding and CLI-expanding change.

See #35
  • Loading branch information
cbs228 committed Feb 24, 2024
2 parents 6cc9860 + 760b476 commit b9e8d03
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 28 deletions.
67 changes: 48 additions & 19 deletions crates/samedec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
> ```
Expand Down Expand Up @@ -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`"
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
73 changes: 66 additions & 7 deletions crates/samedec/src/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ where
};

let locations: Vec<&str> = header.location_str_iter().collect();
let evt = header.event();

Command::new(cmd)
.stdin(Stdio::piped())
Expand All @@ -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()
}

Expand Down Expand Up @@ -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
Expand All @@ -143,13 +174,41 @@ 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
fn time_to_unix_str(tm: DateTime<Utc>) -> 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::*;
Expand Down
46 changes: 44 additions & 2 deletions crates/sameold/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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"]);
Expand Down Expand Up @@ -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());
}
}
2 changes: 2 additions & 0 deletions sample/long_message.22050.s16le.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions sample/npt.22050.s16le.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 )) ]
Expand Down
2 changes: 2 additions & 0 deletions sample/two_and_two.22050.s16le.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 )) ]
Expand Down

0 comments on commit b9e8d03

Please sign in to comment.