A RPi Pico powered Lightning Detector
1mod analysis;
2mod data;
3
4use core::cell::Cell;
5
6use alloc::vec::Vec;
7use sachy_fmt::{info, unwrap};
8
9use embassy_futures::select::select3;
10use embassy_rp::peripherals::DMA_CH1;
11use embassy_sync::{
12 blocking_mutex::raw::NoopRawMutex,
13 zerocopy_channel::{Channel as ZChannel, Receiver, Sender},
14};
15use embassy_time::{Duration, Instant, Ticker, Timer};
16
17use crate::{
18 adc::AdcDriver,
19 constants::{BLIP_SIZE, BLOCK_SIZE},
20 pwm::PwmDriver,
21 rtc::GlobalRtc,
22 updates::UpdateConnection,
23 utils::{static_alloc, try_buffer, try_static_timestamped_block_vecs},
24};
25
26#[embassy_executor::task]
27pub async fn detector_task(
28 adc: AdcDriver<'static, NoopRawMutex, DMA_CH1>,
29 pwm: PwmDriver<'static>,
30 rtc: GlobalRtc<'static>,
31) {
32 info!("Waiting for RTC to start running");
33 while !rtc.is_ready().await {
34 Timer::after_secs(2).await;
35 }
36
37 info!("Allocating detector resources");
38
39 let blocks = unwrap!(
40 try_static_timestamped_block_vecs(4, BLOCK_SIZE),
41 "Couldn't allocate block buffers"
42 );
43
44 let buf_channel: &mut ZChannel<NoopRawMutex, (i64, Vec<u16>)> =
45 static_alloc(ZChannel::new(blocks));
46
47 let (mut sender, mut receiver) = buf_channel.split();
48
49 let mut detector = Detector::new(adc, pwm);
50 let mut samples = unwrap!(try_buffer(64), "Failed to allocate sample buffer");
51 let mut peaks = Vec::new();
52
53 unwrap!(
54 peaks.try_reserve_exact(BLOCK_SIZE),
55 "Failed to allocate peaks buffer"
56 );
57
58 loop {
59 let average = detector.tune(samples.as_mut_slice()).await;
60
61 select3(
62 detector.sample(&mut sender, rtc),
63 detector.analyse(&mut receiver, average, &mut peaks),
64 detector.tick(rtc),
65 )
66 .await;
67 }
68}
69
70struct Detector<'device> {
71 adc: AdcDriver<'device, NoopRawMutex, DMA_CH1>,
72 pwm: PwmDriver<'device>,
73 state: DetectorState,
74}
75
76#[derive(Debug)]
77struct DetectorState {
78 strikes: Cell<u16>,
79 warn_level: Cell<u16>,
80}
81
82impl Default for DetectorState {
83 fn default() -> Self {
84 Self {
85 strikes: Cell::new(0),
86 warn_level: Cell::new(255),
87 }
88 }
89}
90
91impl<'device> Detector<'device> {
92 fn new(adc: AdcDriver<'device, NoopRawMutex, DMA_CH1>, mut pwm: PwmDriver<'device>) -> Self {
93 pwm.enable();
94
95 Self {
96 adc,
97 pwm,
98 state: DetectorState::default(),
99 }
100 }
101}
102
103impl Detector<'_> {
104 async fn tune(&mut self, samples: &mut [u16]) -> u16 {
105 info!("Tuning Detector for correct voltage settings");
106 let mut duty = 0;
107 self.pwm.set_duty(duty);
108 Timer::after_secs(2).await;
109 let mut act_value = self.adc.sample_average(samples).await;
110
111 info!("initial ACT: {}", act_value);
112
113 while act_value < 1364 {
114 duty += 2;
115 if duty >= 256 {
116 duty = 0;
117 self.pwm.set_duty(duty);
118 Timer::after_secs(2).await;
119 act_value = self.adc.sample_average(samples).await;
120 info!("Restarting tuning");
121 continue;
122 }
123 self.pwm.set_duty(duty);
124 Timer::after_millis(250).await;
125 act_value = self.adc.sample_average(samples).await;
126 info!("ACT: {}, Duty: {}", act_value, duty);
127 }
128
129 duty = (duty / 3) * 2;
130 self.pwm.set_duty(duty);
131 info!("Set detection duty to: {}", duty);
132 // Allow voltage level to stabilize after tuning
133 Timer::after_secs(2).await;
134 info!("Ready for strike detection");
135
136 self.adc.sample_average(samples).await
137 }
138
139 async fn sample(
140 &self,
141 data: &mut Sender<'static, NoopRawMutex, (i64, Vec<u16>)>,
142 rtc: GlobalRtc<'static>,
143 ) {
144 let track_time = rtc.track_time().await;
145
146 loop {
147 let (timestamp, buf) = data.send().await;
148 *timestamp = track_time.timestamp();
149 self.adc.sample(buf).await;
150 data.send_done();
151 }
152 }
153
154 async fn analyse(
155 &self,
156 data: &mut Receiver<'static, NoopRawMutex, (i64, Vec<u16>)>,
157 mut average: u16,
158 peaks: &mut Vec<u16>,
159 ) {
160 loop {
161 peaks.clear();
162
163 let (timestamp, buf) = data.receive().await;
164
165 let new_avg =
166 analysis::analyse_buffer_by_stepped_windows(buf.as_slice(), average, peaks);
167
168 let blips = peaks.len();
169
170 if blips >= BLIP_SIZE {
171 self.state
172 .strikes
173 .update(|strike| strike.saturating_add(32));
174
175 if let Some(net_data) = UpdateConnection::can_update() {
176 data::transmit_strike(
177 *timestamp,
178 buf.as_slice(),
179 peaks.as_slice(),
180 average,
181 &net_data,
182 );
183 }
184
185 info!(
186 "Strikes detected at {}s! avg {}, signal strength: {}, blip {}, level {}",
187 Instant::now().as_secs(),
188 average,
189 blips,
190 peaks.iter().max(),
191 self.state.warn_level.get()
192 );
193 }
194
195 data.receive_done();
196
197 average = new_avg;
198 }
199 }
200
201 async fn tick(&self, rtc: GlobalRtc<'static>) {
202 let mut inactive: Option<Instant> = None;
203 let mut interval = Ticker::every(Duration::from_secs(1));
204 let track_time = rtc.track_time().await;
205
206 loop {
207 interval.next().await;
208 let strikes = self.state.strikes.get();
209 let mut warn_level = self.state.warn_level.get();
210
211 if strikes > 32 {
212 warn_level = warn_level.saturating_add(strikes);
213 }
214
215 let decay = warn_level >> 8;
216
217 self.state.strikes.set(0);
218 self.state.warn_level.set(warn_level - decay);
219
220 match inactive {
221 Some(_) if decay > 0 => {
222 inactive = None;
223 }
224 Some(val) if val.elapsed() >= Duration::from_secs(3600) => break,
225 None if decay == 0 => {
226 inactive = Some(Instant::now());
227 }
228 None => {
229 if let Some(net_data) = UpdateConnection::can_update() {
230 let now = track_time.timestamp();
231 data::transmit_level_update(now, decay, &net_data);
232 }
233 }
234 _ => continue,
235 }
236 }
237 }
238}