Repo for designs & driver for a TA7642 powered lightning detector

feat: Embassy driver

+707
+255
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "byteorder" 7 + version = "1.5.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 10 + 11 + [[package]] 12 + name = "cfg-if" 13 + version = "1.0.4" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 16 + 17 + [[package]] 18 + name = "critical-section" 19 + version = "1.2.0" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 22 + 23 + [[package]] 24 + name = "document-features" 25 + version = "0.2.12" 26 + source = "registry+https://github.com/rust-lang/crates.io-index" 27 + checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" 28 + dependencies = [ 29 + "litrs", 30 + ] 31 + 32 + [[package]] 33 + name = "embassy-executor-timer-queue" 34 + version = "0.1.0" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" 37 + 38 + [[package]] 39 + name = "embassy-strike-driver" 40 + version = "0.1.0" 41 + dependencies = [ 42 + "critical-section", 43 + "embassy-sync", 44 + "embassy-time", 45 + "pollster", 46 + ] 47 + 48 + [[package]] 49 + name = "embassy-sync" 50 + version = "0.7.2" 51 + source = "registry+https://github.com/rust-lang/crates.io-index" 52 + checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" 53 + dependencies = [ 54 + "cfg-if", 55 + "critical-section", 56 + "embedded-io-async", 57 + "futures-core", 58 + "futures-sink", 59 + "heapless", 60 + ] 61 + 62 + [[package]] 63 + name = "embassy-time" 64 + version = "0.5.0" 65 + source = "registry+https://github.com/rust-lang/crates.io-index" 66 + checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65" 67 + dependencies = [ 68 + "cfg-if", 69 + "critical-section", 70 + "document-features", 71 + "embassy-time-driver", 72 + "embassy-time-queue-utils", 73 + "embedded-hal 0.2.7", 74 + "embedded-hal 1.0.0", 75 + "embedded-hal-async", 76 + "futures-core", 77 + ] 78 + 79 + [[package]] 80 + name = "embassy-time-driver" 81 + version = "0.2.1" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6" 84 + dependencies = [ 85 + "document-features", 86 + ] 87 + 88 + [[package]] 89 + name = "embassy-time-queue-utils" 90 + version = "0.3.0" 91 + source = "registry+https://github.com/rust-lang/crates.io-index" 92 + checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" 93 + dependencies = [ 94 + "embassy-executor-timer-queue", 95 + "heapless", 96 + ] 97 + 98 + [[package]] 99 + name = "embedded-hal" 100 + version = "0.2.7" 101 + source = "registry+https://github.com/rust-lang/crates.io-index" 102 + checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" 103 + dependencies = [ 104 + "nb 0.1.3", 105 + "void", 106 + ] 107 + 108 + [[package]] 109 + name = "embedded-hal" 110 + version = "1.0.0" 111 + source = "registry+https://github.com/rust-lang/crates.io-index" 112 + checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 113 + 114 + [[package]] 115 + name = "embedded-hal-async" 116 + version = "1.0.0" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" 119 + dependencies = [ 120 + "embedded-hal 1.0.0", 121 + ] 122 + 123 + [[package]] 124 + name = "embedded-io" 125 + version = "0.6.1" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 128 + 129 + [[package]] 130 + name = "embedded-io-async" 131 + version = "0.6.1" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" 134 + dependencies = [ 135 + "embedded-io", 136 + ] 137 + 138 + [[package]] 139 + name = "futures-core" 140 + version = "0.3.31" 141 + source = "registry+https://github.com/rust-lang/crates.io-index" 142 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 143 + 144 + [[package]] 145 + name = "futures-sink" 146 + version = "0.3.31" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 149 + 150 + [[package]] 151 + name = "hash32" 152 + version = "0.3.1" 153 + source = "registry+https://github.com/rust-lang/crates.io-index" 154 + checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 155 + dependencies = [ 156 + "byteorder", 157 + ] 158 + 159 + [[package]] 160 + name = "heapless" 161 + version = "0.8.0" 162 + source = "registry+https://github.com/rust-lang/crates.io-index" 163 + checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 164 + dependencies = [ 165 + "hash32", 166 + "stable_deref_trait", 167 + ] 168 + 169 + [[package]] 170 + name = "litrs" 171 + version = "1.0.0" 172 + source = "registry+https://github.com/rust-lang/crates.io-index" 173 + checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" 174 + 175 + [[package]] 176 + name = "nb" 177 + version = "0.1.3" 178 + source = "registry+https://github.com/rust-lang/crates.io-index" 179 + checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" 180 + dependencies = [ 181 + "nb 1.1.0", 182 + ] 183 + 184 + [[package]] 185 + name = "nb" 186 + version = "1.1.0" 187 + source = "registry+https://github.com/rust-lang/crates.io-index" 188 + checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" 189 + 190 + [[package]] 191 + name = "pollster" 192 + version = "0.4.0" 193 + source = "registry+https://github.com/rust-lang/crates.io-index" 194 + checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" 195 + dependencies = [ 196 + "pollster-macro", 197 + ] 198 + 199 + [[package]] 200 + name = "pollster-macro" 201 + version = "0.4.0" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "ac5da421106a50887c5b51d20806867db377fbb86bacf478ee0500a912e0c113" 204 + dependencies = [ 205 + "proc-macro2", 206 + "quote", 207 + "syn", 208 + ] 209 + 210 + [[package]] 211 + name = "proc-macro2" 212 + version = "1.0.106" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 215 + dependencies = [ 216 + "unicode-ident", 217 + ] 218 + 219 + [[package]] 220 + name = "quote" 221 + version = "1.0.44" 222 + source = "registry+https://github.com/rust-lang/crates.io-index" 223 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 224 + dependencies = [ 225 + "proc-macro2", 226 + ] 227 + 228 + [[package]] 229 + name = "stable_deref_trait" 230 + version = "1.2.1" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 233 + 234 + [[package]] 235 + name = "syn" 236 + version = "2.0.114" 237 + source = "registry+https://github.com/rust-lang/crates.io-index" 238 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 239 + dependencies = [ 240 + "proc-macro2", 241 + "quote", 242 + "unicode-ident", 243 + ] 244 + 245 + [[package]] 246 + name = "unicode-ident" 247 + version = "1.0.22" 248 + source = "registry+https://github.com/rust-lang/crates.io-index" 249 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 250 + 251 + [[package]] 252 + name = "void" 253 + version = "1.0.2" 254 + source = "registry+https://github.com/rust-lang/crates.io-index" 255 + checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+16
Cargo.toml
··· 1 + [workspace] 2 + resolver = "3" 3 + members = ["embassy-strike-driver"] 4 + 5 + [workspace.package] 6 + authors = ["Sachy.dev <sachymetsu@tutamail.com>"] 7 + edition = "2024" 8 + repository = "https://tangled.org/sachy.dev/strike-sensor" 9 + version = "0.1.0" 10 + license = "AGPL-3.0-only" 11 + rust-version = "1.92.0" 12 + 13 + [workspace.dependencies] 14 + embassy-time = "0.5" 15 + embassy-sync = "0.7" 16 + defmt = "1"
+4
README.md
··· 4 4 5 5 The sensor designs can be found in the [kicad](./kicad) folder. 6 6 7 + ## Driver 8 + 9 + Currently, the driver needs to be tested and coded according to how the new PCB/circuit behaves, so this will require a bit of work before considered "ready". 10 + 7 11 ## Licenses 8 12 9 13 The sensor hardware designs are licensed under the [CERN Open Hardware Licence Version 2 - Strongly Reciprocal](./kicad/LICENSE-CERN-OHL-S) license.
+17
embassy-strike-driver/Cargo.toml
··· 1 + [package] 2 + name = "embassy-strike-driver" 3 + authors.workspace = true 4 + edition.workspace = true 5 + repository.workspace = true 6 + version.workspace = true 7 + license.workspace = true 8 + rust-version.workspace = true 9 + 10 + [dependencies] 11 + embassy-time.workspace = true 12 + embassy-sync.workspace = true 13 + 14 + [dev-dependencies] 15 + embassy-time = { workspace = true, features = ["mock-driver", "generic-queue-8"] } 16 + critical-section = { version = "1.1", features = ["std"] } 17 + pollster = { version = "0.4", features = ["macro"] }
+415
embassy-strike-driver/src/lib.rs
··· 1 + #![no_std] 2 + 3 + use core::cell::Cell; 4 + 5 + use embassy_sync::{ 6 + blocking_mutex::raw::NoopRawMutex, 7 + zerocopy_channel::{Channel, Receiver, Sender}, 8 + }; 9 + use embassy_time::{Duration, Instant, Ticker, Timer}; 10 + 11 + pub const BLOCK_SIZE: usize = 512; 12 + pub type ZeroCopyChannel<'device> = Channel<'device, NoopRawMutex, (i64, [u16; BLOCK_SIZE])>; 13 + 14 + pub trait TimeSource { 15 + fn timestamp(&self) -> i64; 16 + fn get_source() -> Self; 17 + } 18 + 19 + pub trait AdcSource { 20 + fn sample_average(&self, samples: &mut [u16]) -> impl Future<Output = u16>; 21 + fn sample(&self, samples: &mut [u16]) -> impl Future<Output = ()>; 22 + } 23 + 24 + pub trait PwmSource { 25 + fn set_duty(&mut self, duty: u16); 26 + } 27 + 28 + pub trait BufferMut<T> { 29 + fn push(&mut self, value: T); 30 + fn clear(&mut self); 31 + fn as_slice(&self) -> &[T]; 32 + fn len(&self) -> usize; 33 + fn is_empty(&self) -> bool; 34 + } 35 + 36 + #[derive(Debug)] 37 + struct DetectorState { 38 + max_duty: Cell<u8>, 39 + duty: Cell<u8>, 40 + avg: Cell<u16>, 41 + strikes: Cell<u16>, 42 + warn_level: Cell<u16>, 43 + } 44 + 45 + impl Default for DetectorState { 46 + fn default() -> Self { 47 + Self { 48 + max_duty: Cell::new(0), 49 + duty: Cell::new(0), 50 + avg: Cell::new(0), 51 + strikes: Cell::new(0), 52 + warn_level: Cell::new(255), 53 + } 54 + } 55 + } 56 + 57 + #[derive(Debug)] 58 + pub struct DetectorConfig { 59 + blip_threshold: Cell<u16>, 60 + blip_size: Cell<usize>, 61 + } 62 + 63 + impl Default for DetectorConfig { 64 + fn default() -> Self { 65 + Self { 66 + blip_threshold: Cell::new(14), 67 + blip_size: Cell::new(2), 68 + } 69 + } 70 + } 71 + 72 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 73 + pub enum DetectorUpdate<'a> { 74 + Tick { 75 + timestamp: i64, 76 + level: u16, 77 + }, 78 + Detection { 79 + timestamp: i64, 80 + samples: &'a [u16], 81 + peaks: &'a [usize], 82 + }, 83 + } 84 + 85 + pub struct DetectorDriver<T: TimeSource, P: PwmSource, A: AdcSource> { 86 + state: DetectorState, 87 + config: DetectorConfig, 88 + timer_source: T, 89 + pwm: P, 90 + adc: A, 91 + } 92 + 93 + impl<T, P, A> DetectorDriver<T, P, A> 94 + where 95 + T: TimeSource, 96 + P: PwmSource, 97 + A: AdcSource, 98 + { 99 + pub fn new(config: DetectorConfig, timer_source: T, pwm: P, adc: A) -> Self { 100 + Self { 101 + state: Default::default(), 102 + config, 103 + timer_source, 104 + pwm, 105 + adc, 106 + } 107 + } 108 + 109 + pub fn reset_timer_source(&mut self) { 110 + self.timer_source = T::get_source(); 111 + } 112 + 113 + pub fn get_timestamp(&self) -> i64 { 114 + self.timer_source.timestamp() 115 + } 116 + 117 + pub fn set_blip_threshold(&self, threshold: u16) { 118 + self.config.blip_threshold.set(threshold); 119 + } 120 + 121 + pub fn set_blip_size(&self, size: usize) { 122 + self.config.blip_size.set(size); 123 + } 124 + 125 + pub async fn tune(&mut self, samples: &mut [u16]) { 126 + // info!("Tuning Detector for correct voltage settings"); 127 + let mut duty = 0; 128 + self.pwm.set_duty(duty); 129 + Timer::after_secs(2).await; 130 + let mut act_value = self.adc.sample_average(samples).await; 131 + 132 + // info!("initial ACT: {}", act_value); 133 + 134 + while act_value < 1364 { 135 + duty += 2; 136 + if duty >= 256 { 137 + duty = 0; 138 + self.pwm.set_duty(duty); 139 + Timer::after_secs(2).await; 140 + act_value = self.adc.sample_average(samples).await; 141 + // info!("Restarting tuning"); 142 + continue; 143 + } 144 + self.pwm.set_duty(duty); 145 + Timer::after_millis(250).await; 146 + act_value = self.adc.sample_average(samples).await; 147 + // info!("ACT: {}, Duty: {}", act_value, duty); 148 + } 149 + self.state.max_duty.set(duty as u8); 150 + duty = (duty / 3) * 2; 151 + self.pwm.set_duty(duty); 152 + self.state.duty.set(duty as u8); 153 + // info!("Set detection duty to: {}", duty); 154 + // Allow voltage level to stabilize after tuning 155 + Timer::after_secs(2).await; 156 + let avg = self.adc.sample_average(samples).await; 157 + self.state.avg.set(avg); 158 + } 159 + 160 + /// Samples and returns an `i64` timestamp. 161 + pub async fn sample_with_zerocopy<'a>( 162 + &self, 163 + dma: &mut Sender<'a, NoopRawMutex, (i64, [u16; BLOCK_SIZE])>, 164 + ) { 165 + loop { 166 + let (time, samples) = dma.send().await; 167 + *time = self.timer_source.timestamp(); 168 + self.adc.sample(samples).await; 169 + dma.send_done(); 170 + } 171 + } 172 + 173 + pub fn detect_from_sample<B, F>( 174 + &self, 175 + timestamp: i64, 176 + samples: &[u16], 177 + peaks: &mut B, 178 + update: F, 179 + ) where 180 + B: BufferMut<usize>, 181 + F: Fn(DetectorUpdate<'_>), 182 + { 183 + peaks.clear(); 184 + 185 + let new_avg = analyse_buffer_by_stepped_windows( 186 + self.config.blip_threshold.get(), 187 + samples, 188 + self.state.avg.get(), 189 + peaks, 190 + ); 191 + 192 + let blips = peaks.len(); 193 + 194 + if blips >= self.config.blip_size.get() { 195 + self.state 196 + .strikes 197 + .update(|strike| strike.saturating_add(32)); 198 + 199 + update(DetectorUpdate::Detection { 200 + timestamp, 201 + samples, 202 + peaks: peaks.as_slice(), 203 + }); 204 + } 205 + 206 + self.state.avg.set(new_avg); 207 + } 208 + 209 + pub async fn detect_with_zerocopy<'a, 'd, B, F>( 210 + &self, 211 + dma: &mut Receiver<'a, NoopRawMutex, (i64, [u16; BLOCK_SIZE])>, 212 + peaks: &'d mut B, 213 + update: F, 214 + ) where 215 + B: BufferMut<usize>, 216 + F: Fn(DetectorUpdate<'_>), 217 + { 218 + loop { 219 + let (timestamp, samples) = dma.receive().await; 220 + 221 + self.detect_from_sample(*timestamp, samples, peaks, &update); 222 + 223 + dma.receive_done(); 224 + } 225 + } 226 + 227 + pub async fn tick<F>(&self, update: F) 228 + where 229 + F: Fn(DetectorUpdate), 230 + { 231 + let mut inactive: Option<Instant> = None; 232 + let mut interval = Ticker::every(Duration::from_secs(1)); 233 + 234 + loop { 235 + interval.next().await; 236 + let strikes = self.state.strikes.get(); 237 + let mut warn_level = self.state.warn_level.get(); 238 + 239 + if strikes > 32 { 240 + warn_level = warn_level.saturating_add(strikes); 241 + } 242 + 243 + let decay = warn_level >> 8; 244 + 245 + self.state.strikes.set(0); 246 + self.state.warn_level.set(warn_level - decay); 247 + 248 + match inactive { 249 + Some(_) if decay > 0 => { 250 + inactive = None; 251 + } 252 + Some(val) if val.elapsed() >= Duration::from_secs(3600) => break, 253 + None if decay == 0 => { 254 + inactive = Some(Instant::now()); 255 + } 256 + None => { 257 + update(DetectorUpdate::Tick { 258 + timestamp: self.timer_source.timestamp(), 259 + level: warn_level - 255, 260 + }); 261 + } 262 + _ => continue, 263 + } 264 + } 265 + } 266 + } 267 + 268 + fn analyse_buffer_by_stepped_windows<B: BufferMut<usize>>( 269 + threshold: u16, 270 + buf: &[u16], 271 + average: u16, 272 + peaks: &mut B, 273 + ) -> u16 { 274 + const CHUNK_SIZE: usize = BLOCK_SIZE / 32; 275 + const CHUNK_STEP: usize = CHUNK_SIZE / 2; 276 + 277 + let mut total = 0u32; 278 + let mut len = 0u32; 279 + 280 + for (i, window) in buf.windows(CHUNK_SIZE).enumerate().step_by(CHUNK_STEP) { 281 + let window_total = window.iter().copied().sum::<u16>(); 282 + let window_avg = window_total / CHUNK_SIZE as u16; 283 + let diff = average.saturating_sub(window_avg); 284 + 285 + if diff > threshold { 286 + peaks.push(i); 287 + } else { 288 + total += window_total as u32; 289 + len += CHUNK_SIZE as u32; 290 + } 291 + } 292 + 293 + (total / len) as u16 294 + } 295 + 296 + #[cfg(test)] 297 + mod tests { 298 + use core::future::poll_fn; 299 + 300 + use embassy_time::MockDriver; 301 + 302 + use super::*; 303 + 304 + #[derive(Debug, Default)] 305 + struct MockMachine { 306 + pwm: Cell<u16>, 307 + } 308 + 309 + impl MockMachine { 310 + fn adc_sample_avg(&self) -> u16 { 311 + self.pwm.get().saturating_mul(14) 312 + } 313 + } 314 + 315 + struct MockPwm<'a>(&'a MockMachine); 316 + 317 + impl PwmSource for MockPwm<'_> { 318 + fn set_duty(&mut self, duty: u16) { 319 + self.0.pwm.set(duty); 320 + } 321 + } 322 + 323 + struct MockAdc<'a>(&'a MockMachine); 324 + 325 + impl AdcSource for MockAdc<'_> { 326 + async fn sample(&self, samples: &mut [u16]) { 327 + samples.fill(self.0.adc_sample_avg()); 328 + } 329 + 330 + async fn sample_average(&self, _samples: &mut [u16]) -> u16 { 331 + self.0.adc_sample_avg() 332 + } 333 + } 334 + 335 + struct MockTimeSource; 336 + 337 + impl TimeSource for MockTimeSource { 338 + fn get_source() -> Self { 339 + Self 340 + } 341 + fn timestamp(&self) -> i64 { 342 + 0 343 + } 344 + } 345 + 346 + async fn tune_detector_manually<'a>(detector: &mut DetectorDriver<MockTimeSource, MockPwm<'a>, MockAdc<'a>>, buf: &mut [u16], driver: &MockDriver) { 347 + let mut tuning = core::pin::pin!(detector.tune(buf)); 348 + 349 + poll_fn(|cx| match tuning.as_mut().poll(cx) { 350 + core::task::Poll::Ready(_) => core::task::Poll::Ready(()), 351 + core::task::Poll::Pending => { 352 + cx.waker().wake_by_ref(); 353 + driver.advance(Duration::from_secs(2)); 354 + 355 + core::task::Poll::Pending 356 + } 357 + }) 358 + .await; 359 + } 360 + 361 + #[pollster::test] 362 + async fn tuning_cycle_completes() { 363 + let driver = embassy_time::MockDriver::get(); 364 + driver.reset(); 365 + let device = MockMachine::default(); 366 + let pwm = MockPwm(&device); 367 + let adc = MockAdc(&device); 368 + 369 + let mut detector = DetectorDriver::new(Default::default(), MockTimeSource, pwm, adc); 370 + let mut buf = [0; 16]; 371 + 372 + tune_detector_manually(&mut detector, &mut buf, driver).await; 373 + 374 + assert_eq!(detector.state.max_duty.get(), 98); 375 + assert_eq!(detector.state.duty.get(), 64); 376 + assert_eq!(detector.adc.sample_average(&mut buf).await, 896); 377 + } 378 + 379 + #[pollster::test] 380 + async fn tick_updates_only_on_raised_levels() { 381 + let driver = embassy_time::MockDriver::get(); 382 + driver.reset(); 383 + let device = MockMachine::default(); 384 + let pwm = MockPwm(&device); 385 + let adc = MockAdc(&device); 386 + 387 + let mut detector = DetectorDriver::new(Default::default(), MockTimeSource, pwm, adc); 388 + let mut buf = [0; 16]; 389 + 390 + tune_detector_manually(&mut detector, &mut buf, driver).await; 391 + 392 + { 393 + let mut tick = core::pin::pin!(detector.tick(|a| { 394 + assert_eq!(a, DetectorUpdate::Tick { timestamp: 0, level: 300 }); 395 + })); 396 + 397 + poll_fn(|cx| { 398 + match tick.as_mut().poll(cx) { 399 + core::task::Poll::Ready(_) => core::task::Poll::Ready(()), 400 + core::task::Poll::Pending => { 401 + if detector.state.warn_level.get() > 255 { 402 + return core::task::Poll::Ready(()); 403 + } 404 + detector.state.strikes.set(300); 405 + driver.advance(Duration::from_secs(1)); 406 + cx.waker().wake_by_ref(); 407 + core::task::Poll::Pending 408 + }, 409 + } 410 + }).await; 411 + } 412 + 413 + assert_eq!(detector.state.warn_level.get(), 553); 414 + } 415 + }