diff options
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/rp/src/bin/orchestrate_tasks.rs | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/examples/rp/src/bin/orchestrate_tasks.rs b/examples/rp/src/bin/orchestrate_tasks.rs new file mode 100644 index 000000000..b9282e273 --- /dev/null +++ b/examples/rp/src/bin/orchestrate_tasks.rs | |||
| @@ -0,0 +1,267 @@ | |||
| 1 | //! This example demonstrates some approaches to communicate between tasks in order to orchestrate the state of the system. | ||
| 2 | //! | ||
| 3 | //! We demonstrate how to: | ||
| 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. | ||
| 5 | //! - use a signal to terminate a task. | ||
| 6 | //! - use command channels to send commands to another task. | ||
| 7 | //! - use different ways to receive messages, from a straightforwar awaiting on one channel to a more complex awaiting on multiple channels. | ||
| 8 | //! | ||
| 9 | //! There are more patterns to orchestrate tasks, this is just one example. | ||
| 10 | //! | ||
| 11 | //! We will use these tasks to generate example "state information": | ||
| 12 | //! - a task that generates random numbers in intervals of 60s | ||
| 13 | //! - a task that generates random numbers in intervals of 30s | ||
| 14 | //! - a task that generates random numbers in intervals of 90s | ||
| 15 | //! - a task that notifies about being attached/disattached from usb power | ||
| 16 | //! - a task that measures vsys voltage in intervals of 30s | ||
| 17 | |||
| 18 | #![no_std] | ||
| 19 | #![no_main] | ||
| 20 | |||
| 21 | use assign_resources::assign_resources; | ||
| 22 | use defmt::*; | ||
| 23 | use embassy_executor::Spawner; | ||
| 24 | use embassy_futures::select::{select, Either}; | ||
| 25 | use embassy_rp::adc::{Adc, Channel, Config, InterruptHandler}; | ||
| 26 | use embassy_rp::bind_interrupts; | ||
| 27 | use embassy_rp::clocks::RoscRng; | ||
| 28 | use embassy_rp::gpio::{Input, Pull}; | ||
| 29 | use embassy_rp::peripherals; | ||
| 30 | use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | ||
| 31 | use embassy_sync::{channel, signal}; | ||
| 32 | use embassy_time::{Duration, Timer}; | ||
| 33 | use rand::RngCore; | ||
| 34 | use {defmt_rtt as _, panic_probe as _}; | ||
| 35 | |||
| 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. | ||
| 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. | ||
| 40 | assign_resources! { | ||
| 41 | vsys: Vsys { | ||
| 42 | adc: ADC, | ||
| 43 | pin_29: PIN_29, | ||
| 44 | }, | ||
| 45 | vbus: Vbus { | ||
| 46 | pin_24: PIN_24, | ||
| 47 | }, | ||
| 48 | } | ||
| 49 | |||
| 50 | bind_interrupts!(struct Irqs { | ||
| 51 | ADC_IRQ_FIFO => InterruptHandler; | ||
| 52 | }); | ||
| 53 | |||
| 54 | /// This is the type of Events that we will send from the worker tasks to the orchestrating task. | ||
| 55 | enum Events { | ||
| 56 | UsbPowered(bool), | ||
| 57 | VsysVoltage(f32), | ||
| 58 | FirstRandomSeed(u32), | ||
| 59 | SecondRandomSeed(u32), | ||
| 60 | ThirdRandomSeed(u32), | ||
| 61 | } | ||
| 62 | |||
| 63 | /// This is the type of Commands that we will send from the orchestrating task to the worker tasks. | ||
| 64 | /// Note that we are lazy here and only have one command, you might want to have more. | ||
| 65 | enum Commands { | ||
| 66 | /// This command will stop the appropriate worker task | ||
| 67 | Stop, | ||
| 68 | } | ||
| 69 | |||
| 70 | /// 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. | ||
| 71 | #[derive(Default, Debug, Clone, Format)] | ||
| 72 | struct State { | ||
| 73 | usb_powered: bool, | ||
| 74 | vsys_voltage: f32, | ||
| 75 | first_random_seed: u32, | ||
| 76 | second_random_seed: u32, | ||
| 77 | third_random_seed: u32, | ||
| 78 | times_we_got_first_random_seed: u8, | ||
| 79 | maximum_times_we_want_first_random_seed: u8, | ||
| 80 | } | ||
| 81 | |||
| 82 | impl State { | ||
| 83 | fn new() -> Self { | ||
| 84 | Self { | ||
| 85 | usb_powered: false, | ||
| 86 | vsys_voltage: 0.0, | ||
| 87 | first_random_seed: 0, | ||
| 88 | second_random_seed: 0, | ||
| 89 | third_random_seed: 0, | ||
| 90 | times_we_got_first_random_seed: 0, | ||
| 91 | maximum_times_we_want_first_random_seed: 3, | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | /// Channel for the events that we want the orchestrator to react to, all state events are of the type Enum Events. | ||
| 97 | /// 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 | ||
| 98 | /// 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 | ||
| 99 | /// 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 | ||
| 100 | /// in the queue, so the worker tasks can in all nominal cases send their events and continue with their work without waiting. | ||
| 101 | /// 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. | ||
| 102 | static EVENT_CHANNEL: channel::Channel<CriticalSectionRawMutex, Events, 10> = channel::Channel::new(); | ||
| 103 | |||
| 104 | /// 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. | ||
| 105 | static STOP_FIRST_RANDOM_SIGNAL: signal::Signal<CriticalSectionRawMutex, Commands> = signal::Signal::new(); | ||
| 106 | |||
| 107 | // And now we can put all this into use | ||
| 108 | |||
| 109 | /// 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 | ||
| 110 | /// 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. | ||
| 111 | #[embassy_executor::main] | ||
| 112 | async fn main(spawner: Spawner) { | ||
| 113 | // initialize the peripherals | ||
| 114 | let p = embassy_rp::init(Default::default()); | ||
| 115 | // split the resources, for convenience - see above | ||
| 116 | let r = split_resources! {p}; | ||
| 117 | |||
| 118 | // spawn the tasks | ||
| 119 | spawner.spawn(orchestrate(spawner)).unwrap(); | ||
| 120 | spawner.spawn(random_30s(spawner)).unwrap(); | ||
| 121 | spawner.spawn(random_60s(spawner)).unwrap(); | ||
| 122 | spawner.spawn(random_90s(spawner)).unwrap(); | ||
| 123 | spawner.spawn(usb_power(spawner, r.vbus)).unwrap(); | ||
| 124 | spawner.spawn(vsys_voltage(spawner, r.vsys)).unwrap(); | ||
| 125 | } | ||
| 126 | |||
| 127 | /// This is the task handling the system state and orchestrating the other tasks. WEe can regard this as the "main loop" of the system. | ||
| 128 | #[embassy_executor::task] | ||
| 129 | async fn orchestrate(_spawner: Spawner) { | ||
| 130 | let mut state = State::new(); | ||
| 131 | |||
| 132 | // we need to have a receiver for the events | ||
| 133 | let receiver = EVENT_CHANNEL.receiver(); | ||
| 134 | |||
| 135 | loop { | ||
| 136 | // we await on the receiver, this will block until a new event is available | ||
| 137 | // 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 | ||
| 138 | // see the embassy_futures docs: https://docs.embassy.dev/embassy-futures/git/default/select/index.html | ||
| 139 | // The task random_30s does a select, if you want to have a look at that. | ||
| 140 | // 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. | ||
| 141 | // We keep it simple here. | ||
| 142 | let event = receiver.receive().await; | ||
| 143 | |||
| 144 | // react to the events | ||
| 145 | match event { | ||
| 146 | Events::UsbPowered(usb_powered) => { | ||
| 147 | // update the state and/or react to the event here | ||
| 148 | state.usb_powered = usb_powered; | ||
| 149 | info!("Usb powered: {}", usb_powered); | ||
| 150 | } | ||
| 151 | Events::VsysVoltage(voltage) => { | ||
| 152 | // update the state and/or react to the event here | ||
| 153 | state.vsys_voltage = voltage; | ||
| 154 | info!("Vsys voltage: {}", voltage); | ||
| 155 | } | ||
| 156 | Events::FirstRandomSeed(seed) => { | ||
| 157 | // update the state and/or react to the event here | ||
| 158 | state.first_random_seed = seed; | ||
| 159 | // here we change some meta state, we count how many times we got the first random seed | ||
| 160 | state.times_we_got_first_random_seed += 1; | ||
| 161 | info!( | ||
| 162 | "First random seed: {}, and that was iteration {} of receiving this.", | ||
| 163 | seed, &state.times_we_got_first_random_seed | ||
| 164 | ); | ||
| 165 | } | ||
| 166 | Events::SecondRandomSeed(seed) => { | ||
| 167 | // update the state and/or react to the event here | ||
| 168 | state.second_random_seed = seed; | ||
| 169 | info!("Second random seed: {}", seed); | ||
| 170 | } | ||
| 171 | Events::ThirdRandomSeed(seed) => { | ||
| 172 | // update the state and/or react to the event here | ||
| 173 | state.third_random_seed = seed; | ||
| 174 | info!("Third random seed: {}", seed); | ||
| 175 | } | ||
| 176 | } | ||
| 177 | // we now have an altered state | ||
| 178 | // there is a crate for detecting field changes on crates.io (https://crates.io/crates/fieldset) that might be useful here | ||
| 179 | // for now we just keep it simple | ||
| 180 | info!("State: {:?}", &state); | ||
| 181 | |||
| 182 | // here we react to the state, in this case here we want to stop the first random seed task after we got it a defined number of times | ||
| 183 | if state.times_we_got_first_random_seed == state.maximum_times_we_want_first_random_seed { | ||
| 184 | info!("Stopping the first random signal task"); | ||
| 185 | // we send a command to the task | ||
| 186 | STOP_FIRST_RANDOM_SIGNAL.signal(Commands::Stop); | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | /// This task will generate random numbers in intervals of 30s | ||
| 192 | /// The task will terminate after it has received a command signal to stop, see the orchestrate task for that. | ||
| 193 | #[embassy_executor::task] | ||
| 194 | async fn random_30s(_spawner: Spawner) { | ||
| 195 | let mut rng = RoscRng; | ||
| 196 | let sender = EVENT_CHANNEL.sender(); | ||
| 197 | loop { | ||
| 198 | // we either await on the timer or the signal, whichever comes first. | ||
| 199 | let futures = select(Timer::after(Duration::from_secs(30)), STOP_FIRST_RANDOM_SIGNAL.wait()).await; | ||
| 200 | match futures { | ||
| 201 | Either::First(_) => { | ||
| 202 | // we received are operating on the timer | ||
| 203 | info!("30s are up, generating random number"); | ||
| 204 | let random_number = rng.next_u32(); | ||
| 205 | sender.send(Events::FirstRandomSeed(random_number)).await; | ||
| 206 | } | ||
| 207 | Either::Second(_) => { | ||
| 208 | // we received the signal to stop | ||
| 209 | info!("Received signal to stop, goodbye!"); | ||
| 210 | break; | ||
| 211 | } | ||
| 212 | } | ||
| 213 | } | ||
| 214 | } | ||
| 215 | |||
| 216 | /// This task will generate random numbers in intervals of 60s | ||
| 217 | #[embassy_executor::task] | ||
| 218 | async fn random_60s(_spawner: Spawner) { | ||
| 219 | let mut rng = RoscRng; | ||
| 220 | let sender = EVENT_CHANNEL.sender(); | ||
| 221 | loop { | ||
| 222 | Timer::after(Duration::from_secs(60)).await; | ||
| 223 | let random_number = rng.next_u32(); | ||
| 224 | sender.send(Events::SecondRandomSeed(random_number)).await; | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | /// This task will generate random numbers in intervals of 90s | ||
| 229 | #[embassy_executor::task] | ||
| 230 | async fn random_90s(_spawner: Spawner) { | ||
| 231 | let mut rng = RoscRng; | ||
| 232 | let sender = EVENT_CHANNEL.sender(); | ||
| 233 | loop { | ||
| 234 | Timer::after(Duration::from_secs(90)).await; | ||
| 235 | let random_number = rng.next_u32(); | ||
| 236 | sender.send(Events::ThirdRandomSeed(random_number)).await; | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | /// This task will notify if we are connected to usb power | ||
| 241 | #[embassy_executor::task] | ||
| 242 | pub async fn usb_power(_spawner: Spawner, r: Vbus) { | ||
| 243 | let mut vbus_in = Input::new(r.pin_24, Pull::None); | ||
| 244 | let sender = EVENT_CHANNEL.sender(); | ||
| 245 | loop { | ||
| 246 | sender.send(Events::UsbPowered(vbus_in.is_high())).await; | ||
| 247 | vbus_in.wait_for_any_edge().await; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | /// This task will measure the vsys voltage in intervals of 30s | ||
| 252 | #[embassy_executor::task] | ||
| 253 | pub async fn vsys_voltage(_spawner: Spawner, r: Vsys) { | ||
| 254 | let mut adc = Adc::new(r.adc, Irqs, Config::default()); | ||
| 255 | let vsys_in = r.pin_29; | ||
| 256 | let mut channel = Channel::new_pin(vsys_in, Pull::None); | ||
| 257 | let sender = EVENT_CHANNEL.sender(); | ||
| 258 | loop { | ||
| 259 | // read the adc value | ||
| 260 | let adc_value = adc.read(&mut channel).await.unwrap(); | ||
| 261 | // convert the adc value to voltage. | ||
| 262 | // 3.3 is the reference voltage, 3.0 is the factor for the inbuilt voltage divider and 4096 is the resolution of the adc | ||
| 263 | let voltage = (adc_value as f32) * 3.3 * 3.0 / 4096.0; | ||
| 264 | sender.send(Events::VsysVoltage(voltage)).await; | ||
| 265 | Timer::after(Duration::from_secs(30)).await; | ||
| 266 | } | ||
| 267 | } | ||
