diff --git a/Cargo.lock b/Cargo.lock index f02927df0134fb32f4a62ae9410d533f5dc1af5c..f174c172464ac7f3fceed395645e6e9f82005939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,7 @@ dependencies = [ "cortex-m", "cortex-m-rt", "cortex-m-rtic", + "cortex-m-semihosting", "crc", "embedded-storage", "half", @@ -168,6 +169,15 @@ dependencies = [ "syn", ] +[[package]] +name = "cortex-m-semihosting" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c23234600452033cc77e4b761e740e02d2c4168e11dbf36ab14a0f58973592b0" +dependencies = [ + "cortex-m", +] + [[package]] name = "crc" version = "3.0.1" @@ -523,7 +533,7 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rf4463" version = "0.1.0" -source = "git+https://gitlab.scd31.com/stephen/rf4463-lib#9857af6eb45851ea8e68ee628db4c6e2bf25335b" +source = "git+https://gitlab.scd31.com/stephen/rf4463-lib#79c8def87540f8ab2663bfa3c9fb13db344ef84e" dependencies = [ "embedded-hal 0.2.7", ] diff --git a/Cargo.toml b/Cargo.toml index 0a8c49cb7d6e2b8bedb5fb764432b8d10ff1228b..bf6195dea3cb5fee8d2565f3f3de8c84e2671427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ stm32f4xx-hal = { version = "0.16", features = ["stm32f411", "rtic", "otg-fs", " cortex-m = "0.7" cortex-m-rt = "0.7" cortex-m-rtic = { version = "1.1.4" } -#cortex-m-semihosting = "0.5" +cortex-m-semihosting = "0.5" #panic-semihosting = "0.6" panic-reset = "0.1.1" rf4463 = { git = "https://gitlab.scd31.com/stephen/rf4463-lib" } @@ -34,4 +34,3 @@ crc = "3.0.1" embedded-storage = "0.2.0" num-traits = { version = "0.2.17", default-features = false, features = ["libm"] } rand = { version = "0.8", default_features = false, features = ["small_rng"] } -# TODO need to add panic-reset at some point \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 5a16b12d80c24b7c5344e1268faf623665ae4ce6..4d0f42c5ac8e64b3cb18641edea4ec3fa07198ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use stm32f4xx_hal::flash::{FlashExt, LockedFlash}; use crate::geo::distance_km; +const CONFIG_LEN: usize = 844; const CASTAGNOLI: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI); #[derive(Clone)] @@ -21,6 +22,8 @@ pub struct Config { pub enable_transmit: bool, pub enable_digipeating: bool, pub black_hole: MaybeBlackHole, + pub min_voltage: Maybe<f32>, + pub antenna_height: Maybe<u8>, pub antenna_gain: Maybe<f32>, pub tx_power: Maybe<f32>, @@ -34,19 +37,25 @@ impl Config { NorFlash::erase(&mut unlocked, 384 * 1024, 512 * 1024).unwrap(); // Write our config - let mut buf = [0; 839]; + let mut buf = [0; CONFIG_LEN]; self.serialize(&mut buf); NorFlash::write(&mut unlocked, 384 * 1024, &buf).unwrap(); } pub fn load_from_flash(flash: &mut LockedFlash) -> Option<Self> { - let mut buf = [0; 839]; + let mut buf = [0; CONFIG_LEN]; ReadNorFlash::read(flash, 384 * 1024, &mut buf).unwrap(); Self::deserialize(&buf) } - fn serialize(&self, buf: &mut [u8; 839]) { + pub fn should_enable_gps(&self) -> bool { + self.enable_transmit + && (matches!(self.black_hole, MaybeBlackHole::Some(_)) + || matches!(self.gps, GpsSetting::Receiver)) + } + + fn serialize(&self, buf: &mut [u8; CONFIG_LEN]) { // version // allows us to update the config structure in the future // while maintaining backwards compatibility @@ -110,21 +119,31 @@ impl Config { } } + match self.min_voltage { + Maybe::Some(v) => { + buf[835] = 1; + buf[836..840].copy_from_slice(&v.to_le_bytes()); + } + Maybe::None => { + buf[835] = 0; + } + } + // Checksum - let checksum = CASTAGNOLI.checksum(&buf[0..835]); - buf[835..839].copy_from_slice(&checksum.to_le_bytes()); + let checksum = CASTAGNOLI.checksum(&buf[0..840]); + buf[840..844].copy_from_slice(&checksum.to_le_bytes()); } - fn deserialize(buf: &[u8; 839]) -> Option<Self> { + fn deserialize(buf: &[u8; CONFIG_LEN]) -> Option<Self> { match buf[0] { 0 => Self::deserialize_v0(buf), _ => None, } } - fn deserialize_v0(buf: &[u8; 839]) -> Option<Self> { - let expected_checksum = CASTAGNOLI.checksum(&buf[0..835]); - let actual_checksum = u32::from_le_bytes(buf[835..839].try_into().unwrap()); + fn deserialize_v0(buf: &[u8; CONFIG_LEN]) -> Option<Self> { + let expected_checksum = CASTAGNOLI.checksum(&buf[0..840]); + let actual_checksum = u32::from_le_bytes(buf[840..844].try_into().unwrap()); if expected_checksum != actual_checksum { return None; } @@ -179,6 +198,12 @@ impl Config { Maybe::None }; + let min_voltage = if buf[835] > 0 { + Maybe::Some(f32::from_le_bytes(buf[836..840].try_into().unwrap())) + } else { + Maybe::None + }; + Some(Config { callsign, icon, @@ -190,6 +215,8 @@ impl Config { enable_transmit, enable_digipeating, black_hole, + min_voltage, + antenna_height, antenna_gain, tx_power, @@ -210,6 +237,8 @@ impl Default for Config { enable_transmit: false, enable_digipeating: true, black_hole: MaybeBlackHole::None, + min_voltage: Maybe::None, + antenna_height: Maybe::None, antenna_gain: Maybe::None, tx_power: Maybe::Some(30.0), @@ -335,8 +364,17 @@ where { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Maybe::Some(x) => write!(f, "{x}"), + Maybe::Some(x) => x.fmt(f), Maybe::None => write!(f, "<None>"), } } } + +impl<T> From<Maybe<T>> for Option<T> { + fn from(val: Maybe<T>) -> Self { + match val { + Maybe::Some(x) => Some(x), + Maybe::None => None, + } + } +} diff --git a/src/main.rs b/src/main.rs index f160fdea60e6f366679ec03f564dc09fd6ce0e98..87181c922762413e318c5be7d6c6875a0c2b782e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ mod app { use usb_device::prelude::*; use crate::{ - config::{Config, GpsSetting, Maybe, MaybeBlackHole}, + config::{Config, GpsSetting, Maybe}, gps::{GpsModule, GpsPos}, radio::RadioManager, shell::Shell, @@ -53,6 +53,7 @@ mod app { const H_CLK: u32 = 21_000_000; const LED_BLINK_RATE: u64 = 250; + const UNDERVOLT_THRES_LEN: u8 = 20; // in # LED blinks const HARDWARE_ID: u16 = 0x7c84; const SOFTWARE_ID: u8 = 1; @@ -74,6 +75,7 @@ mod app { buf: &'static mut [u8; MAX_PACKET_LEN], gps_enable: gpio::Pin<'C', 8, gpio::Output<gpio::OpenDrain>>, bootup_time: Instant<u64, 1, { SYS_TICK_FREQ }>, + cycles_below_thres: u8, } #[shared] @@ -124,7 +126,7 @@ mod app { // setup volt meter let volt_pin = gpioc.pc0.into_analog(); let volt_adc = Adc::adc1(dp.ADC1, true, AdcConfig::default()); - let volt_meter = VoltMeter::new(volt_pin, volt_adc); + let mut volt_meter = VoltMeter::new(volt_pin, volt_adc, config.min_voltage.into()); // Detect if we're connected to USB or not let usb_detect = gpioa.pa5.into_pull_down_input(); @@ -205,8 +207,12 @@ mod app { let buf = cortex_m::singleton!(: [u8; MAX_PACKET_LEN] = [0; MAX_PACKET_LEN]).unwrap(); + let undervolt = volt_meter.below_threshold(); + let status = if usb_connected { Status::Programming(false) + } else if undervolt { + Status::Sleep } else if radio.is_none() { Status::Error(false) } else if !config.enable_transmit { @@ -216,20 +222,15 @@ mod app { } else { green.set_high(); - if config.enable_transmit - && (matches!(config.black_hole, MaybeBlackHole::Some(_)) - || matches!(config.gps, GpsSetting::Receiver)) - { - gps_enable.set_low(); // enable GPS + if config.should_enable_gps() { + gps_enable.set_low(); } - Status::Normal - }; - - if config.enable_transmit && radio.is_some() { transmit_position::spawn().unwrap(); radio_tick::spawn().unwrap(); - } + + Status::Normal + }; led_handler::spawn().unwrap(); @@ -253,6 +254,7 @@ mod app { buf, gps_enable, bootup_time, + cycles_below_thres: 0, }, init::Monotonics(Systick::new(ctx.core.SYST, H_CLK)), ) @@ -374,7 +376,8 @@ mod app { } } - #[task(priority = 2, local = [green, usb_detect, gps_enable], shared = [red, status])] + #[task(priority = 2, local = [green, usb_detect, gps_enable, cycles_below_thres], + shared = [red, status, volt_meter, radio])] fn led_handler(mut ctx: led_handler::Context) { led_handler::spawn_after(LED_BLINK_RATE.millis()).unwrap(); @@ -388,8 +391,34 @@ mod app { ctx.local.gps_enable.set_high(); } + match cur_status { + Status::Normal | Status::TxDisabled(_) | Status::Error(_) => { + if ctx.shared.volt_meter.lock(|vm| vm.below_threshold()) { + *ctx.local.cycles_below_thres += 1; + } else { + *ctx.local.cycles_below_thres = 0; + } + + if *ctx.local.cycles_below_thres > UNDERVOLT_THRES_LEN { + status.lock(|s| *s = Status::Sleep); + ctx.local.gps_enable.set_high(); + ctx.local.green.set_low(); + ctx.shared.red.lock(|red| red.set_low()); + ctx.shared.radio.lock(|r| r.as_mut().map(|x| x.sleep())); + } + } + + Status::Sleep => { + if ctx.shared.volt_meter.lock(|vm| vm.above_threshold()) { + cortex_m::peripheral::SCB::sys_reset(); + } + } + + Status::Programming(_) => {} + } + status.lock(|s| match s { - Status::Normal => {} + Status::Normal | Status::Sleep => {} Status::Programming(b) => { ctx.local.green.set_state((*b).into()); *b = !*b; diff --git a/src/radio.rs b/src/radio.rs index b98cd3644e43abeef6077ab8a48f23e1b7f368ed..d4e6d2ed4270fef704e82d914154a6dcd04598ad 100644 --- a/src/radio.rs +++ b/src/radio.rs @@ -39,8 +39,6 @@ impl<'a> RadioManager<'a> { // sets us up for the default CATS frequency, 430.500 MHz radio.set_channel(20); - radio.start_rx(None, false).ok()?; - let enable_digipeating = config.enable_digipeating; let seed = rand_seed_from_str(&config.callsign) ^ u64::from(config.ssid); let rng = SmallRng::seed_from_u64(seed); @@ -53,6 +51,10 @@ impl<'a> RadioManager<'a> { }) } + pub fn sleep(&mut self) { + //self.radio.sleep() + } + pub fn get_temp(&mut self) -> Option<i8> { self.radio .get_temp() diff --git a/src/shell.rs b/src/shell.rs index 38540d43ff68aa50b759c1f69b68feee387eb3f4..ff0e0a97c130d7428c5d882e946eaeec2a4357cd 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -72,6 +72,8 @@ impl Shell { enable_transmit, enable_digipeating, black_hole, + min_voltage, + antenna_height, antenna_gain, tx_power, @@ -94,6 +96,7 @@ impl Shell { transmit_period {transmit_period_seconds} (seconds)\r\n\ gps {gps}\r\n\ black_hole {black_hole}\r\n\ + min_voltage {min_voltage:.2}\r\n\ enable_transmit {enable_transmit}\r\n\ enable_digipeating {enable_digipeating}\r\n\ \r\n\ @@ -175,6 +178,13 @@ impl Shell { } Err(e) => Err(e), }, + "min_voltage" => match parse_maybe_f32(value) { + Ok(v) => { + self.config.min_voltage = v; + Ok(()) + } + Err(e) => Err(e), + }, "enable_digipeating" => match parse_bool(value) { Ok(v) => { self.config.enable_digipeating = v; @@ -268,7 +278,8 @@ impl Shell { black_hole: MaybeBlackHole::None, antenna_height: Maybe::None, antenna_gain: Maybe::None, - tx_power: Maybe::Some(30.0) + tx_power: Maybe::Some(30.0), + min_voltage: Maybe::Some(9.0), }; self.config.save_to_flash(flash); diff --git a/src/status.rs b/src/status.rs index 3406318c45295cf006459f8539ff15bbac252a36..3d9d2bd0acf09a1a37ea725c66e35a7b98856c28 100644 --- a/src/status.rs +++ b/src/status.rs @@ -4,4 +4,5 @@ pub enum Status { Programming(bool), TxDisabled(bool), Error(bool), + Sleep, } diff --git a/src/voltage.rs b/src/voltage.rs index d6193befe5ebfac59b3f6007761a1518353fcab9..ec69964a4ced13ade918c916fec05041f9eda800 100644 --- a/src/voltage.rs +++ b/src/voltage.rs @@ -9,12 +9,17 @@ const FACTOR: f64 = 1.0 / 0.0757856; pub struct VoltMeter { pin: Pin<'C', 0, Analog>, adc: Adc<ADC1>, + threshold: Option<f32>, } impl VoltMeter { - pub fn new(pin: Pin<'C', 0, Analog>, mut adc: Adc<ADC1>) -> Self { + pub fn new(pin: Pin<'C', 0, Analog>, mut adc: Adc<ADC1>, threshold: Option<f32>) -> Self { adc.calibrate(); - Self { pin, adc } + Self { + pin, + adc, + threshold, + } } pub fn voltage(&mut self) -> f64 { @@ -23,4 +28,19 @@ impl VoltMeter { mv as f64 * FACTOR / 1000.0 } + + pub fn below_threshold(&mut self) -> bool { + match self.threshold { + Some(v) => self.voltage() < v.into(), + None => false, + } + } + + // 0.1V buffer for hysteresis reasons + pub fn above_threshold(&mut self) -> bool { + match self.threshold { + Some(v) => self.voltage() >= (v + 0.1).into(), + None => true, + } + } }