aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/rp/src/bin/orchestrate_tasks.rs311
1 files changed, 156 insertions, 155 deletions
diff --git a/examples/rp/src/bin/orchestrate_tasks.rs b/examples/rp/src/bin/orchestrate_tasks.rs
index 0e21d5833..7ff004860 100644
--- a/examples/rp/src/bin/orchestrate_tasks.rs
+++ b/examples/rp/src/bin/orchestrate_tasks.rs
@@ -1,20 +1,18 @@
1//! This example demonstrates some approaches to communicate between tasks in order to orchestrate the state of the system. 1//! This example demonstrates some approaches to communicate between tasks in order to orchestrate the state of the system.
2//! 2//!
3//! We demonstrate how to: 3//! The system consists of several tasks:
4//! - use a channel to send messages between tasks, in this case here in order to have one task control the state of the system. 4//! - Three tasks that generate random numbers at different intervals (simulating i.e. sensor readings)
5//! - use a signal to terminate a task. 5//! - A task that monitors USB power connection (hardware event handling)
6//! - use command channels to send commands to another task. 6//! - A task that reads system voltage (ADC sampling)
7//! - use different ways to receive messages, from a straightforwar awaiting on one channel to a more complex awaiting on multiple futures. 7//! - A consumer task that processes all this information
8//! 8//!
9//! There are more patterns to orchestrate tasks, this is just one example. 9//! The system maintains state in a single place, wrapped in a Mutex.
10//! 10//!
11//! We will use these tasks to generate example "state information": 11//! We demonstrate how to:
12//! - a task that generates random numbers in intervals of 60s 12//! - use a mutex to maintain shared state between tasks
13//! - a task that generates random numbers in intervals of 30s 13//! - use a channel to send events between tasks
14//! - a task that generates random numbers in intervals of 90s 14//! - use an orchestrator task to coordinate tasks and handle state transitions
15//! - a task that notifies about being attached/disattached from usb power 15//! - use signals to notify about state changes and terminate tasks
16//! - a task that measures vsys voltage in intervals of 30s
17//! - a task that consumes the state information and reacts to it
18 16
19#![no_std] 17#![no_std]
20#![no_main] 18#![no_main]
@@ -28,15 +26,13 @@ use embassy_rp::clocks::RoscRng;
28use embassy_rp::gpio::{Input, Pull}; 26use embassy_rp::gpio::{Input, Pull};
29use embassy_rp::{bind_interrupts, peripherals}; 27use embassy_rp::{bind_interrupts, peripherals};
30use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; 28use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
29use embassy_sync::mutex::Mutex;
31use embassy_sync::{channel, signal}; 30use embassy_sync::{channel, signal};
32use embassy_time::{Duration, Timer}; 31use embassy_time::{Duration, Timer};
33use rand::RngCore; 32use rand::RngCore;
34use {defmt_rtt as _, panic_probe as _}; 33use {defmt_rtt as _, panic_probe as _};
35 34
36// This is just some preparation, see example `assign_resources.rs` for more information on this. We prep the rresources that we will be using in different tasks. 35// Hardware resource assignment. See other examples for different ways of doing this.
37// **Note**: This will not work with a board that has a wifi chip, because the wifi chip uses pins 24 and 29 for its own purposes. A way around this in software
38// is not trivial, at least if you intend to use wifi, too. Workaround is to wire from vsys and vbus pins to appropriate pins on the board through a voltage divider. Then use those pins.
39// For this example it will not matter much, the concept of what we are showing remains valid.
40assign_resources! { 36assign_resources! {
41 vsys: Vsys { 37 vsys: Vsys {
42 adc: ADC, 38 adc: ADC,
@@ -47,228 +43,233 @@ assign_resources! {
47 }, 43 },
48} 44}
49 45
46// Interrupt binding - required for hardware peripherals like ADC
50bind_interrupts!(struct Irqs { 47bind_interrupts!(struct Irqs {
51 ADC_IRQ_FIFO => InterruptHandler; 48 ADC_IRQ_FIFO => InterruptHandler;
52}); 49});
53 50
54/// This is the type of Events that we will send from the worker tasks to the orchestrating task. 51/// Events that worker tasks send to the orchestrator
55enum Events { 52enum Events {
56 UsbPowered(bool), 53 UsbPowered(bool), // USB connection state changed
57 VsysVoltage(f32), 54 VsysVoltage(f32), // New voltage reading
58 FirstRandomSeed(u32), 55 FirstRandomSeed(u32), // Random number from 30s timer
59 SecondRandomSeed(u32), 56 SecondRandomSeed(u32), // Random number from 60s timer
60 ThirdRandomSeed(u32), 57 ThirdRandomSeed(u32), // Random number from 90s timer
61 ResetFirstRandomSeed, 58 ResetFirstRandomSeed, // Signal to reset the first counter
62} 59}
63 60
64/// This is the type of Commands that we will send from the orchestrating task to the worker tasks. 61/// Commands that can control task behavior.
65/// Note that we are lazy here and only have one command, you might want to have more. 62/// Currently only used to stop tasks, but could be extended for other controls.
66enum Commands { 63enum Commands {
67 /// This command will stop the appropriate worker task 64 /// Signals a task to stop execution
68 Stop, 65 Stop,
69} 66}
70 67
71/// This is the state of the system, we will use this to orchestrate the system. This is a simple example, in a real world application this would be more complex. 68/// The central state of our system, shared between tasks.
72#[derive(Default, Debug, Clone, Format)] 69#[derive(Clone, Format)]
73struct State { 70struct State {
74 usb_powered: bool, 71 usb_powered: bool,
75 vsys_voltage: f32, 72 vsys_voltage: f32,
76 first_random_seed: u32, 73 first_random_seed: u32,
77 second_random_seed: u32, 74 second_random_seed: u32,
78 third_random_seed: u32, 75 third_random_seed: u32,
76 first_random_seed_task_running: bool,
79 times_we_got_first_random_seed: u8, 77 times_we_got_first_random_seed: u8,
80 maximum_times_we_want_first_random_seed: u8, 78 maximum_times_we_want_first_random_seed: u8,
81} 79}
82 80
81/// A formatted view of the system status, used for logging. Used for the below `get_system_summary` fn.
82#[derive(Format)]
83struct SystemStatus {
84 power_source: &'static str,
85 voltage: f32,
86}
87
83impl State { 88impl State {
84 fn new() -> Self { 89 const fn new() -> Self {
85 Self { 90 Self {
86 usb_powered: false, 91 usb_powered: false,
87 vsys_voltage: 0.0, 92 vsys_voltage: 0.0,
88 first_random_seed: 0, 93 first_random_seed: 0,
89 second_random_seed: 0, 94 second_random_seed: 0,
90 third_random_seed: 0, 95 third_random_seed: 0,
96 first_random_seed_task_running: false,
91 times_we_got_first_random_seed: 0, 97 times_we_got_first_random_seed: 0,
92 maximum_times_we_want_first_random_seed: 3, 98 maximum_times_we_want_first_random_seed: 3,
93 } 99 }
94 } 100 }
101
102 /// Returns a formatted summary of power state and voltage.
103 /// Shows how to create methods that work with shared state.
104 fn get_system_summary(&self) -> SystemStatus {
105 SystemStatus {
106 power_source: if self.usb_powered {
107 "USB powered"
108 } else {
109 "Battery powered"
110 },
111 voltage: self.vsys_voltage,
112 }
113 }
95} 114}
96 115
97/// Channel for the events that we want the orchestrator to react to, all state events are of the type Enum Events. 116/// The shared state protected by a mutex
98/// We use a channel with an arbitrary size of 10, the precise size of the queue depends on your use case. This depends on how many events we 117static SYSTEM_STATE: Mutex<CriticalSectionRawMutex, State> = Mutex::new(State::new());
99/// expect to be generated in a given time frame and how fast the orchestrator can react to them. And then if we rather want the senders to wait for 118
100/// new slots in the queue or if we want the orchestrator to have a backlog of events to process. In this case here we expect to always be enough slots 119/// Channel for events from worker tasks to the orchestrator
101/// in the queue, so the worker tasks can in all nominal cases send their events and continue with their work without waiting.
102/// For the events we - in this case here - do not want to loose any events, so a channel is a good choice. See embassy_sync docs for other options.
103static EVENT_CHANNEL: channel::Channel<CriticalSectionRawMutex, Events, 10> = channel::Channel::new(); 120static EVENT_CHANNEL: channel::Channel<CriticalSectionRawMutex, Events, 10> = channel::Channel::new();
104 121
105/// Signal for stopping the first random signal task. We use a signal here, because we need no queue. It is suffiient to have one signal active. 122/// Signal used to stop the first random number task
106static STOP_FIRST_RANDOM_SIGNAL: signal::Signal<CriticalSectionRawMutex, Commands> = signal::Signal::new(); 123static STOP_FIRST_RANDOM_SIGNAL: signal::Signal<CriticalSectionRawMutex, Commands> = signal::Signal::new();
107 124
108/// Channel for the state that we want the consumer task to react to. We use a channel here, because we want to have a queue of state changes, although 125/// Signal for notifying about state changes
109/// we want the queue to be of size 1, because we want to finish rwacting to the state change before the next one comes in. This is just a design choice 126static STATE_CHANGED: signal::Signal<CriticalSectionRawMutex, ()> = signal::Signal::new();
110/// and depends on your use case.
111static CONSUMER_CHANNEL: channel::Channel<CriticalSectionRawMutex, State, 1> = channel::Channel::new();
112
113// And now we can put all this into use
114 127
115/// This is the main task, that will not do very much besides spawning the other tasks. This is a design choice, you could do the
116/// orchestrating here. This is to show that we do not need a main loop here, the system will run indefinitely as long as at least one task is running.
117#[embassy_executor::main] 128#[embassy_executor::main]
118async fn main(spawner: Spawner) { 129async fn main(spawner: Spawner) {
119 // initialize the peripherals
120 let p = embassy_rp::init(Default::default()); 130 let p = embassy_rp::init(Default::default());
121 // split the resources, for convenience - see above
122 let r = split_resources! {p}; 131 let r = split_resources! {p};
123 132
124 // spawn the tasks
125 spawner.spawn(orchestrate(spawner)).unwrap(); 133 spawner.spawn(orchestrate(spawner)).unwrap();
126 spawner.spawn(random_60s(spawner)).unwrap(); 134 spawner.spawn(random_60s(spawner)).unwrap();
127 spawner.spawn(random_90s(spawner)).unwrap(); 135 spawner.spawn(random_90s(spawner)).unwrap();
136 // `random_30s` is not spawned here, butin the orchestrate task depending on state
128 spawner.spawn(usb_power(spawner, r.vbus)).unwrap(); 137 spawner.spawn(usb_power(spawner, r.vbus)).unwrap();
129 spawner.spawn(vsys_voltage(spawner, r.vsys)).unwrap(); 138 spawner.spawn(vsys_voltage(spawner, r.vsys)).unwrap();
130 spawner.spawn(consumer(spawner)).unwrap(); 139 spawner.spawn(consumer(spawner)).unwrap();
131} 140}
132 141
133/// This is the task handling the system state and orchestrating the other tasks. WEe can regard this as the "main loop" of the system. 142/// Main task that processes all events and updates system state.
134#[embassy_executor::task] 143#[embassy_executor::task]
135async fn orchestrate(_spawner: Spawner) { 144async fn orchestrate(spawner: Spawner) {
136 let mut state = State::new();
137
138 // we need to have a receiver for the events
139 let receiver = EVENT_CHANNEL.receiver(); 145 let receiver = EVENT_CHANNEL.receiver();
140 146
141 // and we need a sender for the consumer task
142 let state_sender = CONSUMER_CHANNEL.sender();
143
144 loop { 147 loop {
145 // we await on the receiver, this will block until a new event is available 148 // Do nothing until we receive any event
146 // as an alternative to this, we could also await on multiple channels, this would block until at least one of the channels has an event
147 // see the embassy_futures docs: https://docs.embassy.dev/embassy-futures/git/default/select/index.html
148 // The task random_30s does a select, if you want to have a look at that.
149 // Another reason to use select may also be that we want to have a timeout, so we can react to the absence of events within a time frame.
150 // We keep it simple here.
151 let event = receiver.receive().await; 149 let event = receiver.receive().await;
152 150
153 // react to the events 151 // Scope in which we want to lock the system state. As an alternative we could also call `drop` on the state
154 match event { 152 {
155 Events::UsbPowered(usb_powered) => { 153 let mut state = SYSTEM_STATE.lock().await;
156 // update the state and/or react to the event here 154
157 state.usb_powered = usb_powered; 155 match event {
158 info!("Usb powered: {}", usb_powered); 156 Events::UsbPowered(usb_powered) => {
159 } 157 state.usb_powered = usb_powered;
160 Events::VsysVoltage(voltage) => { 158 info!("Usb powered: {}", usb_powered);
161 // update the state and/or react to the event here 159 info!("System summary: {}", state.get_system_summary());
162 state.vsys_voltage = voltage; 160 }
163 info!("Vsys voltage: {}", voltage); 161 Events::VsysVoltage(voltage) => {
164 } 162 state.vsys_voltage = voltage;
165 Events::FirstRandomSeed(seed) => { 163 info!("Vsys voltage: {}", voltage);
166 // update the state and/or react to the event here 164 }
167 state.first_random_seed = seed; 165 Events::FirstRandomSeed(seed) => {
168 // here we change some meta state, we count how many times we got the first random seed 166 state.first_random_seed = seed;
169 state.times_we_got_first_random_seed += 1; 167 state.times_we_got_first_random_seed += 1;
170 info!( 168 info!(
171 "First random seed: {}, and that was iteration {} of receiving this.", 169 "First random seed: {}, and that was iteration {} of receiving this.",
172 seed, &state.times_we_got_first_random_seed 170 seed, &state.times_we_got_first_random_seed
173 ); 171 );
174 } 172 }
175 Events::SecondRandomSeed(seed) => { 173 Events::SecondRandomSeed(seed) => {
176 // update the state and/or react to the event here 174 state.second_random_seed = seed;
177 state.second_random_seed = seed; 175 info!("Second random seed: {}", seed);
178 info!("Second random seed: {}", seed); 176 }
179 } 177 Events::ThirdRandomSeed(seed) => {
180 Events::ThirdRandomSeed(seed) => { 178 state.third_random_seed = seed;
181 // update the state and/or react to the event here 179 info!("Third random seed: {}", seed);
182 state.third_random_seed = seed; 180 }
183 info!("Third random seed: {}", seed); 181 Events::ResetFirstRandomSeed => {
182 state.times_we_got_first_random_seed = 0;
183 state.first_random_seed = 0;
184 info!("Resetting the first random seed counter");
185 }
184 } 186 }
185 Events::ResetFirstRandomSeed => { 187
186 // update the state and/or react to the event here 188 // Handle task orchestration based on state
187 state.times_we_got_first_random_seed = 0; 189 // Just placed as an example here, could be hooked into the event system, puton a timer, ...
188 state.first_random_seed = 0; 190 match state.times_we_got_first_random_seed {
189 info!("Resetting the first random seed counter"); 191 max if max == state.maximum_times_we_want_first_random_seed => {
192 info!("Stopping the first random signal task");
193 STOP_FIRST_RANDOM_SIGNAL.signal(Commands::Stop);
194 EVENT_CHANNEL.sender().send(Events::ResetFirstRandomSeed).await;
195 }
196 0 => {
197 let respawn_first_random_seed_task = !state.first_random_seed_task_running;
198 // Deliberately dropping the Mutex lock here to release it before a lengthy operation
199 drop(state);
200 if respawn_first_random_seed_task {
201 info!("(Re)-Starting the first random signal task");
202 spawner.spawn(random_30s(spawner)).unwrap();
203 }
204 }
205 _ => {}
190 } 206 }
191 } 207 }
192 // we now have an altered state 208
193 // there is a crate for detecting field changes on crates.io (https://crates.io/crates/fieldset) that might be useful here 209 STATE_CHANGED.signal(());
194 // for now we just keep it simple
195
196 // we send the state to the consumer task
197 // since the channel has a size of 1, this will block until the consumer task has received the state, which is what we want here in this example
198 // **Note:** It is bad design to send too much data between tasks, with no clear definition of what "too much" is. In this example we send the
199 // whole state, in a real world application you might want to send only the data, that is relevant to the consumer task AND only when it has changed.
200 // We keep it simple here.
201 state_sender.send(state.clone()).await;
202 } 210 }
203} 211}
204 212
205/// This task will consume the state information and react to it. This is a simple example, in a real world application this would be more complex 213/// Task that monitors state changes and logs system status.
206/// and we could have multiple consumer tasks, each reacting to different parts of the state.
207#[embassy_executor::task] 214#[embassy_executor::task]
208async fn consumer(spawner: Spawner) { 215async fn consumer(_spawner: Spawner) {
209 // we need to have a receiver for the state
210 let receiver = CONSUMER_CHANNEL.receiver();
211 let sender = EVENT_CHANNEL.sender();
212 loop { 216 loop {
213 // we await on the receiver, this will block until a new state is available 217 // Wait for state change notification
214 let state = receiver.receive().await; 218 STATE_CHANGED.wait().await;
215 // react to the state, in this case here we just log it 219
216 info!("The consumer has reveived this state: {:?}", &state); 220 let state = SYSTEM_STATE.lock().await;
217 221 info!(
218 // here we react to the state, in this case here we want to start or stop the first random signal task depending on the state of the system 222 "State update - {} | Seeds - First: {} (count: {}/{}, running: {}), Second: {}, Third: {}",
219 match state.times_we_got_first_random_seed { 223 state.get_system_summary(),
220 max if max == state.maximum_times_we_want_first_random_seed => { 224 state.first_random_seed,
221 info!("Stopping the first random signal task"); 225 state.times_we_got_first_random_seed,
222 // we send a command to the task 226 state.maximum_times_we_want_first_random_seed,
223 STOP_FIRST_RANDOM_SIGNAL.signal(Commands::Stop); 227 state.first_random_seed_task_running,
224 // we notify the orchestrator that we have sent the command 228 state.second_random_seed,
225 sender.send(Events::ResetFirstRandomSeed).await; 229 state.third_random_seed
226 } 230 );
227 0 => {
228 // we start the task, which presents us with an interesting problem, because we may return here before the task has started
229 // here we just try and log if the task has started, in a real world application you might want to handle this more gracefully
230 info!("Starting the first random signal task");
231 match spawner.spawn(random_30s(spawner)) {
232 Ok(_) => info!("Successfully spawned random_30s task"),
233 Err(e) => info!("Failed to spawn random_30s task: {:?}", e),
234 }
235 }
236 _ => {}
237 }
238 } 231 }
239} 232}
240 233
241/// This task will generate random numbers in intervals of 30s 234/// Task that generates random numbers every 30 seconds until stopped.
242/// The task will terminate after it has received a command signal to stop, see the orchestrate task for that. 235/// Shows how to handle both timer events and stop signals.
243/// Note that we are not spawning this task from main, as we will show how such a task can be spawned and closed dynamically. 236/// As an example of some routine we want to be on or off depending on other needs.
244#[embassy_executor::task] 237#[embassy_executor::task]
245async fn random_30s(_spawner: Spawner) { 238async fn random_30s(_spawner: Spawner) {
239 {
240 let mut state = SYSTEM_STATE.lock().await;
241 state.first_random_seed_task_running = true;
242 }
243
246 let mut rng = RoscRng; 244 let mut rng = RoscRng;
247 let sender = EVENT_CHANNEL.sender(); 245 let sender = EVENT_CHANNEL.sender();
246
248 loop { 247 loop {
249 // we either await on the timer or the signal, whichever comes first. 248 // Wait for either 30s timer or stop signal (like select() in Go)
250 let futures = select(Timer::after(Duration::from_secs(30)), STOP_FIRST_RANDOM_SIGNAL.wait()).await; 249 match select(Timer::after(Duration::from_secs(30)), STOP_FIRST_RANDOM_SIGNAL.wait()).await {
251 match futures {
252 Either::First(_) => { 250 Either::First(_) => {
253 // we received are operating on the timer
254 info!("30s are up, generating random number"); 251 info!("30s are up, generating random number");
255 let random_number = rng.next_u32(); 252 let random_number = rng.next_u32();
256 sender.send(Events::FirstRandomSeed(random_number)).await; 253 sender.send(Events::FirstRandomSeed(random_number)).await;
257 } 254 }
258 Either::Second(_) => { 255 Either::Second(_) => {
259 // we received the signal to stop
260 info!("Received signal to stop, goodbye!"); 256 info!("Received signal to stop, goodbye!");
257
258 let mut state = SYSTEM_STATE.lock().await;
259 state.first_random_seed_task_running = false;
260
261 break; 261 break;
262 } 262 }
263 } 263 }
264 } 264 }
265} 265}
266 266
267/// This task will generate random numbers in intervals of 60s 267/// Task that generates random numbers every 60 seconds. As an example of some routine.
268#[embassy_executor::task] 268#[embassy_executor::task]
269async fn random_60s(_spawner: Spawner) { 269async fn random_60s(_spawner: Spawner) {
270 let mut rng = RoscRng; 270 let mut rng = RoscRng;
271 let sender = EVENT_CHANNEL.sender(); 271 let sender = EVENT_CHANNEL.sender();
272
272 loop { 273 loop {
273 Timer::after(Duration::from_secs(60)).await; 274 Timer::after(Duration::from_secs(60)).await;
274 let random_number = rng.next_u32(); 275 let random_number = rng.next_u32();
@@ -276,11 +277,12 @@ async fn random_60s(_spawner: Spawner) {
276 } 277 }
277} 278}
278 279
279/// This task will generate random numbers in intervals of 90s 280/// Task that generates random numbers every 90 seconds. . As an example of some routine.
280#[embassy_executor::task] 281#[embassy_executor::task]
281async fn random_90s(_spawner: Spawner) { 282async fn random_90s(_spawner: Spawner) {
282 let mut rng = RoscRng; 283 let mut rng = RoscRng;
283 let sender = EVENT_CHANNEL.sender(); 284 let sender = EVENT_CHANNEL.sender();
285
284 loop { 286 loop {
285 Timer::after(Duration::from_secs(90)).await; 287 Timer::after(Duration::from_secs(90)).await;
286 let random_number = rng.next_u32(); 288 let random_number = rng.next_u32();
@@ -288,31 +290,30 @@ async fn random_90s(_spawner: Spawner) {
288 } 290 }
289} 291}
290 292
291/// This task will notify if we are connected to usb power 293/// Task that monitors USB power connection. As an example of some Interrupt somewhere.
292#[embassy_executor::task] 294#[embassy_executor::task]
293pub async fn usb_power(_spawner: Spawner, r: Vbus) { 295pub async fn usb_power(_spawner: Spawner, r: Vbus) {
294 let mut vbus_in = Input::new(r.pin_24, Pull::None); 296 let mut vbus_in = Input::new(r.pin_24, Pull::None);
295 let sender = EVENT_CHANNEL.sender(); 297 let sender = EVENT_CHANNEL.sender();
298
296 loop { 299 loop {
297 sender.send(Events::UsbPowered(vbus_in.is_high())).await; 300 sender.send(Events::UsbPowered(vbus_in.is_high())).await;
298 vbus_in.wait_for_any_edge().await; 301 vbus_in.wait_for_any_edge().await;
299 } 302 }
300} 303}
301 304
302/// This task will measure the vsys voltage in intervals of 30s 305/// Task that reads system voltage through ADC. As an example of some continuous sensor reading.
303#[embassy_executor::task] 306#[embassy_executor::task]
304pub async fn vsys_voltage(_spawner: Spawner, r: Vsys) { 307pub async fn vsys_voltage(_spawner: Spawner, r: Vsys) {
305 let mut adc = Adc::new(r.adc, Irqs, Config::default()); 308 let mut adc = Adc::new(r.adc, Irqs, Config::default());
306 let vsys_in = r.pin_29; 309 let vsys_in = r.pin_29;
307 let mut channel = Channel::new_pin(vsys_in, Pull::None); 310 let mut channel = Channel::new_pin(vsys_in, Pull::None);
308 let sender = EVENT_CHANNEL.sender(); 311 let sender = EVENT_CHANNEL.sender();
312
309 loop { 313 loop {
310 // read the adc value 314 Timer::after(Duration::from_secs(30)).await;
311 let adc_value = adc.read(&mut channel).await.unwrap(); 315 let adc_value = adc.read(&mut channel).await.unwrap();
312 // convert the adc value to voltage.
313 // 3.3 is the reference voltage, 3.0 is the factor for the inbuilt voltage divider and 4096 is the resolution of the adc
314 let voltage = (adc_value as f32) * 3.3 * 3.0 / 4096.0; 316 let voltage = (adc_value as f32) * 3.3 * 3.0 / 4096.0;
315 sender.send(Events::VsysVoltage(voltage)).await; 317 sender.send(Events::VsysVoltage(voltage)).await;
316 Timer::after(Duration::from_secs(30)).await;
317 } 318 }
318} 319}