firmware_sensecap_solar_console/
main.rs1#![cfg_attr(target_os = "none", no_std)]
40#![cfg_attr(target_os = "none", no_main)]
41
42#[cfg(not(target_os = "none"))]
43fn main() {}
44
45#[cfg(target_os = "none")]
46mod panic;
47
48#[cfg(target_os = "none")]
49mod defmt_logger {
50 #[defmt::global_logger]
54 struct Logger;
55 unsafe impl defmt::Logger for Logger {
56 fn acquire() {}
57 unsafe fn flush() {}
58 unsafe fn release() {}
59 unsafe fn write(_: &[u8]) {}
60 }
61 defmt::timestamp!("{=u32}", 0u32);
62}
63
64#[cfg(target_os = "none")]
65mod firmware {
66 use embassy_executor::Spawner;
67 use embassy_futures::join::join;
68 use embassy_futures::select::{Either3, select3};
69 use embassy_nrf::bind_interrupts;
70 use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
71 use embassy_nrf::peripherals;
72 use embassy_nrf::usb::Driver;
73 use embassy_nrf::usb::vbus_detect::HardwareVbusDetect;
74 use embassy_nrf::wdt::{Config as WdtConfig, Watchdog, WatchdogHandle};
75 use embassy_time::{Instant, Timer};
76 use embassy_usb::class::cdc_acm::{CdcAcmClass, Sender, State};
77 use embassy_usb::{Builder, Config};
78 use static_cell::StaticCell;
79 use umsh_bsp_nrf52840::cdc_rescue::CdcAcmRescue;
80 use umsh_bsp_nrf52840::panic_persist::PanicSlot;
81 use umsh_ux_tracker::led::{LedEngine, LedTimings};
82
83 bind_interrupts!(struct Irqs {
84 USBD => embassy_nrf::usb::InterruptHandler<peripherals::USBD>;
85 CLOCK_POWER => embassy_nrf::usb::vbus_detect::InterruptHandler;
86 });
87
88 type SolarUsbDriver = Driver<'static, HardwareVbusDetect>;
90 type SolarSender = Sender<'static, SolarUsbDriver>;
91 type SolarRescue = CdcAcmRescue<'static, SolarUsbDriver>;
92
93 async fn write_line(tx: &mut SolarSender, s: &str) {
98 for chunk in s.as_bytes().chunks(64) {
99 let _ = tx.write_packet(chunk).await;
100 }
101 let _ = tx.write_packet(b"\r\n").await;
102 }
103
104 const HELP: &str = "\
105SenseCAP Solar Node bringup (Phase 1) — GPIO identification\r
106 1 toggle LED_A (P0.15, white user LED)\r
107 ? this help + current pin states\r
108LED_B (P0.19, blue) blinks as the heartbeat.\r
109USER_BUTTON (P1.01) and PWR (P1.07) edges auto-report.";
110
111 #[embassy_executor::task]
115 async fn ident_task(
116 mut tx: SolarSender,
117 mut rx: SolarRescue,
118 mut led_a: Output<'static>,
119 mut btn1: Input<'static>,
120 mut btn2: Input<'static>,
121 prev_panic_buf: &'static [u8; 256],
122 prev_panic_len: usize,
123 ) {
124 rx.wait_connection().await;
127
128 let sha = env!("GIT_SHORT_SHA");
129 write_line(&mut tx, "").await;
130 write_line(&mut tx, "UMSH SenseCAP Solar Node bringup (Phase 1)").await;
131 write_line(&mut tx, sha).await;
132 if prev_panic_len > 0 {
133 write_line(&mut tx, "[PREV PANIC]:").await;
134 if let Ok(s) = core::str::from_utf8(&prev_panic_buf[..prev_panic_len]) {
135 write_line(&mut tx, s).await;
136 }
137 }
138 write_line(&mut tx, HELP).await;
139
140 let mut a_level = false; led_a.set_low();
142
143 let mut pkt = [0u8; 64];
144 loop {
145 rx.wait_connection().await;
148
149 match select3(
150 rx.read_packet(&mut pkt),
151 btn1.wait_for_any_edge(),
152 btn2.wait_for_any_edge(),
153 )
154 .await
155 {
156 Either3::First(res) => {
158 let n = match res {
159 Ok(0) | Err(_) => continue, Ok(n) => n,
161 };
162 for &b in &pkt[..n] {
163 match b {
164 b'1' => {
165 a_level = !a_level;
166 if a_level {
167 led_a.set_high();
168 } else {
169 led_a.set_low();
170 }
171 report(&mut tx, "LED_A (P0.15)", a_level).await;
172 }
173 b'?' => {
174 write_line(&mut tx, HELP).await;
175 report(&mut tx, "LED_A (P0.15)", a_level).await;
176 report_button(&mut tx, "USER_BUTTON (P1.01)", &btn1).await;
177 report_button(&mut tx, "PWR (P1.07)", &btn2).await;
178 }
179 b'\r' | b'\n' => {}
180 _ => {} }
182 }
183 }
184
185 Either3::Second(()) => {
188 report_button(&mut tx, "USER_BUTTON (P1.01)", &btn1).await;
189 }
190 Either3::Third(()) => {
191 report_button(&mut tx, "PWR (P1.07)", &btn2).await;
192 }
193 }
194 }
195 }
196
197 async fn report(tx: &mut SolarSender, name: &str, level: bool) {
198 write_line(tx, name).await;
199 write_line(tx, if level { " -> HIGH" } else { " -> LOW" }).await;
200 }
201
202 async fn report_button(tx: &mut SolarSender, name: &str, btn: &Input<'static>) {
203 write_line(tx, name).await;
204 write_line(
206 tx,
207 if btn.is_low() {
208 " = LOW (pressed)"
209 } else {
210 " = HIGH (released)"
211 },
212 )
213 .await;
214 }
215
216 async fn heartbeat(mut led_b: Output<'static>, mut wdt: WatchdogHandle) -> ! {
223 let mut engine = LedEngine::new(LedTimings::default(), Instant::now().as_millis());
224 loop {
225 wdt.pet();
226 let decision = engine.tick(Instant::now().as_millis());
227 if decision.on {
228 led_b.set_high();
229 } else {
230 led_b.set_low();
231 }
232 Timer::at(Instant::from_millis(decision.next_deadline_ms)).await;
233 }
234 }
235
236 #[embassy_executor::main]
239 async fn main(spawner: Spawner) {
240 let p = embassy_nrf::init(umsh_bsp_nrf52840::clocks::default_config());
241
242 let mut wdt_config = WdtConfig::default();
243 wdt_config.timeout_ticks = 32768 * 8; let (_wdt, [wdt_handle]) =
245 Watchdog::try_new::<_, 1>(p.WDT, wdt_config).unwrap_or_else(|_| panic!("wdt"));
246
247 let _radio_rxen = Output::new(p.P0_05, Level::Low, OutputDrive::Standard);
253 let _gnss_enable = Output::new(p.P1_05, Level::Low, OutputDrive::Standard);
254
255 static PREV_PANIC_BUF: StaticCell<[u8; 256]> = StaticCell::new();
257 let mut prev_panic_tmp = [0u8; 256];
258 let prev_panic_len = {
259 let mut slot = PanicSlot::new(super::panic::panic_region());
260 if let Some(msg) = slot.read() {
261 let n = msg.len().min(prev_panic_tmp.len());
262 prev_panic_tmp[..n].copy_from_slice(&msg[..n]);
263 slot.clear();
264 n
265 } else {
266 0
267 }
268 };
269 let prev_panic_buf: &'static [u8; 256] = PREV_PANIC_BUF.init(prev_panic_tmp);
270
271 let led_a = Output::new(p.P0_15, Level::Low, OutputDrive::Standard);
273 let led_b = Output::new(p.P0_19, Level::Low, OutputDrive::Standard);
274 let btn1 = Input::new(p.P1_01, Pull::Up);
275 let btn2 = Input::new(p.P1_07, Pull::Up);
276
277 let driver = Driver::new(p.USBD, Irqs, HardwareVbusDetect::new(Irqs));
279
280 let mut config = Config::new(0x2886, 0x0059);
282 config.manufacturer = Some("UMSH");
283 config.product = Some("SenseCAP Solar Node Bringup");
284 config.serial_number = Some("sensecap-solar-console");
285 config.max_power = 100;
286 config.max_packet_size_0 = 64;
287
288 static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell::new();
289 static BOS_DESC: StaticCell<[u8; 256]> = StaticCell::new();
290 static MSOS_DESC: StaticCell<[u8; 0]> = StaticCell::new();
291 static CONTROL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
292 static STATE: StaticCell<State> = StaticCell::new();
293
294 let mut builder = Builder::new(
295 driver,
296 config,
297 CONFIG_DESC.init([0; 256]),
298 BOS_DESC.init([0; 256]),
299 MSOS_DESC.init([0; 0]),
300 CONTROL_BUF.init([0; 64]),
301 );
302
303 let class = CdcAcmClass::new(&mut builder, STATE.init(State::new()), 64);
304 let mut usb = builder.build();
305
306 let (tx, raw_rx, ctrl) = class.split_with_control();
307 let rx = CdcAcmRescue::new(raw_rx, ctrl);
308
309 spawner
310 .spawn(ident_task(tx, rx, led_a, btn1, btn2, prev_panic_buf, prev_panic_len).unwrap());
311
312 join(usb.run(), heartbeat(led_b, wdt_handle)).await;
313 }
314}