/ firmware / examples / esp32s3 / modbus_sensors.rs
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  }