| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #![no_std] |
|
|
| extern crate alloc; |
|
|
| |
| |
| |
| use alloc::vec::Vec; |
|
|
| #[cfg(target_arch = "wasm32")] |
| #[global_allocator] |
| static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; |
|
|
| #[cfg(target_arch = "wasm32")] |
| #[panic_handler] |
| fn panic(_info: &core::panic::PanicInfo) -> ! { |
| core::arch::wasm32::unreachable() |
| } |
| use synapse_core::{ |
| Calibration, MeasurementUnit, Reading, ReadingQuality, |
| SensorConfig, TransmissionPayload, |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| extern "C" { |
| |
| |
| |
| fn host_read_i2c(address: u8, register: u8, buf: *mut u8, buf_len: u8) -> i32; |
|
|
| |
| |
| fn host_read_adc(channel: u8) -> i32; |
|
|
| |
| fn host_get_timestamp_ms() -> u64; |
|
|
| |
| |
| fn host_transmit(buf: *const u8, buf_len: u32) -> i32; |
|
|
| |
| |
| fn host_sleep_ms(duration_ms: u32); |
|
|
| |
| fn host_log(level: u8, msg: *const u8, msg_len: u32); |
| } |
|
|
| |
| |
| |
|
|
| fn read_i2c(address: u8, register: u8, buf: &mut [u8]) -> Result<usize, ReadingQuality> { |
| let result = unsafe { host_read_i2c(address, register, buf.as_mut_ptr(), buf.len() as u8) }; |
| if result < 0 { |
| Err(ReadingQuality::Fault) |
| } else { |
| Ok(result as usize) |
| } |
| } |
|
|
| fn read_adc(channel: u8) -> Result<u16, ReadingQuality> { |
| let result = unsafe { host_read_adc(channel) }; |
| if result < 0 { |
| Err(ReadingQuality::Fault) |
| } else { |
| Ok(result as u16) |
| } |
| } |
|
|
| fn timestamp_ms() -> u64 { |
| unsafe { host_get_timestamp_ms() } |
| } |
|
|
| fn transmit(data: &[u8]) -> Result<usize, ReadingQuality> { |
| let result = unsafe { host_transmit(data.as_ptr(), data.len() as u32) }; |
| if result < 0 { |
| Err(ReadingQuality::Fault) |
| } else { |
| Ok(result as usize) |
| } |
| } |
|
|
| fn sleep(ms: u32) { |
| unsafe { host_sleep_ms(ms) } |
| } |
|
|
| fn log_info(msg: &str) { |
| unsafe { host_log(1, msg.as_ptr(), msg.len() as u32) } |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| static mut STATE: Option<ModuleState> = None; |
|
|
| struct ModuleState { |
| config: SensorConfig, |
| sequence: u16, |
| node_id: u16, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| const ATLAS_CMD_READ: u8 = b'R'; |
| const ATLAS_STATUS_SUCCESS: u8 = 1; |
|
|
| |
| |
| fn read_atlas_ezo(i2c_address: u8) -> Result<i32, ReadingQuality> { |
| |
| let cmd = [ATLAS_CMD_READ]; |
| let result = unsafe { |
| host_read_i2c(i2c_address, cmd[0], core::ptr::null_mut(), 0) |
| }; |
| if result < 0 { |
| return Err(ReadingQuality::Fault); |
| } |
|
|
| |
| sleep(1000); |
|
|
| |
| let mut buf = [0u8; 16]; |
| let bytes_read = read_i2c(i2c_address, 0, &mut buf)?; |
|
|
| if bytes_read < 2 || buf[0] != ATLAS_STATUS_SUCCESS { |
| return Err(ReadingQuality::Fault); |
| } |
|
|
| |
| |
| parse_ascii_fixed_point(&buf[1..bytes_read]) |
| .ok_or(ReadingQuality::Degraded) |
| } |
|
|
| |
| |
| fn parse_ascii_fixed_point(bytes: &[u8]) -> Option<i32> { |
| let mut result: i32 = 0; |
| let mut decimal_places: i32 = -1; |
| let mut negative = false; |
|
|
| for &b in bytes { |
| match b { |
| b'-' if result == 0 => negative = true, |
| b'0'..=b'9' => { |
| result = result * 10 + (b - b'0') as i32; |
| if decimal_places >= 0 { |
| decimal_places += 1; |
| } |
| } |
| b'.' => { |
| if decimal_places >= 0 { |
| return None; |
| } |
| decimal_places = 0; |
| } |
| 0 | b'\r' | b'\n' => break, |
| _ => return None, |
| } |
| } |
|
|
| |
| let scale = match decimal_places { |
| -1 | 0 => 1000, |
| 1 => 100, |
| 2 => 10, |
| 3 => 1, |
| _ => return None, |
| }; |
|
|
| result *= scale; |
| if negative { |
| result = -result; |
| } |
| Some(result) |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| #[no_mangle] |
| pub extern "C" fn guest_init(config_ptr: *const u8, config_len: u32, node_id: u16) -> u32 { |
| let config_bytes = unsafe { |
| core::slice::from_raw_parts(config_ptr, config_len as usize) |
| }; |
|
|
| match minicbor::decode::<SensorConfig>(config_bytes) { |
| Ok(config) => { |
| log_info("sensor module initialized"); |
| unsafe { |
| STATE = Some(ModuleState { |
| config, |
| sequence: 0, |
| node_id, |
| }); |
| } |
| 1 |
| } |
| Err(_) => { |
| log_info("failed to parse config"); |
| 0 |
| } |
| } |
| } |
|
|
| |
| |
| |
| #[no_mangle] |
| pub extern "C" fn guest_sample() -> u32 { |
| let state = unsafe { |
| match STATE.as_mut() { |
| Some(s) => s, |
| None => return 0, |
| } |
| }; |
|
|
| let now = timestamp_ms(); |
| let mut readings = Vec::new(); |
|
|
| |
| for ch in 0..8u8 { |
| if !state.config.is_channel_active(ch) { |
| continue; |
| } |
|
|
| let cal = state.config.cal_for(ch); |
|
|
| |
| |
| |
| |
| let (raw, unit, quality) = match ch { |
| 0 => match read_atlas_ezo(0x63) { |
| Ok(v) => (v, MeasurementUnit::Ph, ReadingQuality::Good), |
| Err(q) => (0, MeasurementUnit::Ph, q), |
| }, |
| 1 => match read_atlas_ezo(0x64) { |
| Ok(v) => (v, MeasurementUnit::Ec, ReadingQuality::Good), |
| Err(q) => (0, MeasurementUnit::Ec, q), |
| }, |
| 2 => match read_atlas_ezo(0x61) { |
| Ok(v) => (v, MeasurementUnit::DissolvedOxygen, ReadingQuality::Good), |
| Err(q) => (0, MeasurementUnit::DissolvedOxygen, q), |
| }, |
| 3 => match read_atlas_ezo(0x62) { |
| Ok(v) => (v, MeasurementUnit::Orp, ReadingQuality::Good), |
| Err(q) => (0, MeasurementUnit::Orp, q), |
| }, |
| 4..=7 => match read_adc(ch - 4) { |
| Ok(v) => (v as i32, MeasurementUnit::MoistureVwc, ReadingQuality::Good), |
| Err(q) => (0, MeasurementUnit::MoistureVwc, q), |
| }, |
| _ => continue, |
| }; |
|
|
| readings.push(Reading { |
| timestamp_ms: now, |
| channel: ch, |
| raw_value: raw, |
| calibrated_value: cal.apply(raw), |
| unit, |
| quality, |
| }); |
| } |
|
|
| |
| let payload = TransmissionPayload { |
| node_id: state.node_id, |
| sequence: state.sequence, |
| battery_mv: read_adc(7).unwrap_or(0), |
| readings, |
| }; |
|
|
| |
| state.sequence = state.sequence.wrapping_add(1); |
|
|
| |
| let mut buf = Vec::new(); |
| match minicbor::encode(&payload, &mut buf) { |
| Ok(()) => { |
| match transmit(&buf) { |
| Ok(n) => n as u32, |
| Err(_) => { |
| log_info("transmit failed"); |
| 0 |
| } |
| } |
| } |
| Err(_) => { |
| log_info("cbor encode failed"); |
| 0 |
| } |
| } |
| } |
|
|
| |
| #[no_mangle] |
| pub extern "C" fn guest_reconfigure(config_ptr: *const u8, config_len: u32) -> u32 { |
| let config_bytes = unsafe { |
| core::slice::from_raw_parts(config_ptr, config_len as usize) |
| }; |
|
|
| match minicbor::decode::<SensorConfig>(config_bytes) { |
| Ok(config) => { |
| if let Some(state) = unsafe { STATE.as_mut() } { |
| state.config = config; |
| log_info("reconfigured"); |
| 1 |
| } else { |
| 0 |
| } |
| } |
| Err(_) => 0, |
| } |
| } |
|
|
| |
| |
| |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn parse_ascii_ph() { |
| assert_eq!(parse_ascii_fixed_point(b"7.23"), Some(7230)); |
| assert_eq!(parse_ascii_fixed_point(b"4.0"), Some(4000)); |
| assert_eq!(parse_ascii_fixed_point(b"14"), Some(14000)); |
| assert_eq!(parse_ascii_fixed_point(b"0.5"), Some(500)); |
| assert_eq!(parse_ascii_fixed_point(b"-1.5"), Some(-1500)); |
| } |
|
|
| #[test] |
| fn parse_ascii_null_terminated() { |
| assert_eq!(parse_ascii_fixed_point(b"7.23\0\0\0"), Some(7230)); |
| assert_eq!(parse_ascii_fixed_point(b"4.01\r\n"), Some(4010)); |
| } |
|
|
| #[test] |
| fn parse_ascii_rejects_garbage() { |
| assert_eq!(parse_ascii_fixed_point(b"abc"), None); |
| assert_eq!(parse_ascii_fixed_point(b"7.2.3"), None); |
| } |
| } |
|
|