modbus_sensors.rs
1 //! Modbus RTU sensor example for ESP32-S3 2 //! 3 //! Reads solar radiation, wind speed, and wind direction sensors via RS485/Modbus RTU. 4 //! Originally from the ceratina application, preserved here as a reference example. 5 //! 6 //! Wiring: 7 //! - UART TX: GPIO45 8 //! - UART RX: GPIO48 9 //! - DE/RE: GPIO47 10 //! - Relay: GPIO5 (sensor power) 11 12 #![no_std] 13 #![no_main] 14 15 use async_modbus::client::read_holdings; 16 use defmt::info; 17 use embassy_executor::Spawner; 18 use embassy_time::{Duration, Timer}; 19 use embedded_hal::digital::OutputPin; 20 use embedded_io_async::{ErrorType, Read, Write}; 21 use esp_hal::{ 22 clock::CpuClock, 23 gpio::{Level, Output, OutputConfig}, 24 interrupt::software::SoftwareInterruptControl, 25 timer::timg::TimerGroup, 26 uart::{Config as UartConfig, Uart}, 27 }; 28 use panic_rtt_target as _; 29 30 // ─── RS485 driver ─────────────────────────────────────────────────────────── 31 32 const RS485_BAUD_RATE: u32 = 9_600; 33 const WIND_SENSOR_DELAY_MILLIS: u64 = 100; 34 35 struct Rs485<UART, PIN> { 36 uart: UART, 37 direction_enable_pin: PIN, 38 } 39 40 impl<UART, PIN> Rs485<UART, PIN> { 41 fn new(uart: UART, direction_enable_pin: PIN) -> Self { 42 Self { 43 uart, 44 direction_enable_pin, 45 } 46 } 47 } 48 49 impl<UART, PIN> ErrorType for Rs485<UART, PIN> 50 where 51 UART: ErrorType, 52 { 53 type Error = UART::Error; 54 } 55 56 impl<UART, PIN> Read for Rs485<UART, PIN> 57 where 58 UART: Read, 59 PIN: OutputPin, 60 { 61 async fn read(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> { 62 let _ = self.direction_enable_pin.set_low(); 63 self.uart.read(buffer).await 64 } 65 } 66 67 impl<UART, PIN> Write for Rs485<UART, PIN> 68 where 69 UART: Write, 70 PIN: OutputPin, 71 { 72 async fn write(&mut self, buffer: &[u8]) -> Result<usize, Self::Error> { 73 let _ = self.direction_enable_pin.set_high(); 74 self.uart.write(buffer).await 75 } 76 77 async fn flush(&mut self) -> Result<(), Self::Error> { 78 self.uart.flush().await?; 79 let _ = self.direction_enable_pin.set_low(); 80 Ok(()) 81 } 82 } 83 84 // ─── Sensor constants ─────────────────────────────────────────────────────── 85 86 const SOLAR_RADIATION_SLAVE_ID: u8 = 40; 87 const SOLAR_RADIATION_REGISTER_ADDRESS: u16 = 0; 88 89 const WIND_SPEED_SLAVE_ID: u8 = 20; 90 const WIND_SPEED_REGISTER_ADDRESS: u16 = 0; 91 92 const WIND_DIRECTION_SLAVE_ID: u8 = 30; 93 const WIND_DIRECTION_REGISTER_ADDRESS: u16 = 0; 94 95 // ─── Sensor read helpers ──────────────────────────────────────────────────── 96 97 async fn read_solar_radiation_watts_per_square_meter<UART, PIN>( 98 rs485: &mut Rs485<UART, PIN>, 99 ) -> Result<u16, async_modbus::client::Error<UART::Error>> 100 where 101 UART: Read + Write + ErrorType, 102 PIN: OutputPin, 103 { 104 let registers = read_holdings::<1, _>( 105 rs485, 106 SOLAR_RADIATION_SLAVE_ID, 107 SOLAR_RADIATION_REGISTER_ADDRESS, 108 ) 109 .await?; 110 111 Ok(registers[0].get()) 112 } 113 114 async fn read_wind_speed_kilometers_per_hour<UART, PIN>( 115 rs485: &mut Rs485<UART, PIN>, 116 ) -> Result<f32, async_modbus::client::Error<UART::Error>> 117 where 118 UART: Read + Write + ErrorType, 119 PIN: OutputPin, 120 { 121 let registers = 122 read_holdings::<1, _>(rs485, WIND_SPEED_SLAVE_ID, WIND_SPEED_REGISTER_ADDRESS).await?; 123 let raw_value = registers[0].get(); 124 125 Ok((raw_value as f32 * 3.6) / 10.0) 126 } 127 128 struct WindDirectionReading { 129 angle_degrees: f32, 130 slice: u8, 131 } 132 133 async fn read_wind_direction<UART, PIN>( 134 rs485: &mut Rs485<UART, PIN>, 135 ) -> Result<Option<WindDirectionReading>, async_modbus::client::Error<UART::Error>> 136 where 137 UART: Read + Write + ErrorType, 138 PIN: OutputPin, 139 { 140 let registers = read_holdings::<2, _>( 141 rs485, 142 WIND_DIRECTION_SLAVE_ID, 143 WIND_DIRECTION_REGISTER_ADDRESS, 144 ) 145 .await?; 146 147 let raw_angle_times_ten = registers[0].get(); 148 let raw_slice = registers[1].get() as u8; 149 150 if raw_slice > 15 { 151 return Ok(None); 152 } 153 154 Ok(Some(WindDirectionReading { 155 angle_degrees: raw_angle_times_ten as f32 / 10.0, 156 slice: raw_slice, 157 })) 158 } 159 160 // ─── Main ─────────────────────────────────────────────────────────────────── 161 162 esp_bootloader_esp_idf::esp_app_desc!(); 163 164 #[esp_rtos::main] 165 async fn main(_spawner: Spawner) -> ! { 166 rtt_target::rtt_init_defmt!(); 167 168 let hal_config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); 169 let peripherals = esp_hal::init(hal_config); 170 171 esp_alloc::heap_allocator!(size: 64 * 1024); 172 173 let timer_group0 = TimerGroup::new(peripherals.TIMG0); 174 let sw_ints = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); 175 esp_rtos::start(timer_group0.timer0, sw_ints.software_interrupt0); 176 177 let _sensor_power_relay = 178 Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); 179 180 let uart = Uart::new( 181 peripherals.UART1, 182 UartConfig::default().with_baudrate(RS485_BAUD_RATE), 183 ) 184 .unwrap() 185 .with_tx(peripherals.GPIO45) 186 .with_rx(peripherals.GPIO48) 187 .into_async(); 188 189 let direction_enable_pin = 190 Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default()); 191 let mut rs485 = Rs485::new(uart, direction_enable_pin); 192 193 info!("modbus sensors example started"); 194 195 loop { 196 match embassy_time::with_timeout( 197 Duration::from_millis(500), 198 read_solar_radiation_watts_per_square_meter(&mut rs485), 199 ) 200 .await 201 { 202 Ok(Ok(watts_per_square_meter)) => { 203 info!("solar radiation: {} W/m^2", watts_per_square_meter) 204 } 205 Ok(Err(_)) => info!("solar radiation modbus read failed"), 206 Err(_) => info!("solar radiation modbus read timed out"), 207 } 208 209 match embassy_time::with_timeout( 210 Duration::from_millis(500), 211 read_wind_speed_kilometers_per_hour(&mut rs485), 212 ) 213 .await 214 { 215 Ok(Ok(kilometers_per_hour)) => { 216 info!("wind speed: {} km/h", kilometers_per_hour) 217 } 218 Ok(Err(_)) => info!("wind speed modbus read failed"), 219 Err(_) => info!("wind speed modbus read timed out"), 220 } 221 222 Timer::after(Duration::from_millis(WIND_SENSOR_DELAY_MILLIS)).await; 223 224 match embassy_time::with_timeout( 225 Duration::from_millis(500), 226 read_wind_direction(&mut rs485), 227 ) 228 .await 229 { 230 Ok(Ok(Some(reading))) => { 231 info!( 232 "wind direction: {} deg (slice {})", 233 reading.angle_degrees, reading.slice 234 ) 235 } 236 Ok(Ok(None)) => info!("wind direction read invalid slice"), 237 Ok(Err(_)) => info!("wind direction modbus read failed"), 238 Err(_) => info!("wind direction modbus read timed out"), 239 } 240 241 Timer::after(Duration::from_secs(1)).await; 242 } 243 }