Hosting Indicator Modules on a Raspberry Pi

To build an application that resides on a Raspberry Pi (but that we can test on our workstations) that hosts our indicator modules, we’ll need to employ a few techniques that we haven’t covered in this book yet. The requirements for our Generic Indicator Module System are as follows:

  • Load a WebAssembly module at launch and immediately run, controlling LEDs

  • Enforce a fixed frame rate for animated modules

  • Trap the SIGINT and SIGTERM signals, gracefully turning off active LEDs before shutdown

  • Hot Reloading—if a new module is copied into a monitored location, pause, then load the new module and continue

The application is designed to start and continue running forever, constantly getting new sensor inputs (though we’re only faking one sensor) and feeding that data to the indicator module. To make all of this work, you’ll get into some new Rust patterns like using multi-threaded code, channels, and conditional compilation.

Creating a Raspberry Pi Application

In the previous section, you created an empty application called pihost. At the moment there’s nothing special that you need to do to make this application suitable for a Raspberry Pi. We do need to pick some dependencies—crates that will help us monitor the file system, read and execute WebAssembly modules, respond to OS signals, and operate the Blinkt hardware module. This is a Cargo.toml that pulls in those dependencies:

 [package]
 name = ​"pihost"
 version = ​"0.1.0"
 authors = [​"your Email <[email protected]>"​]
 
 [dependencies]
 notify = ​"4.0.0"
 wasmi = ​"0.4.1"
 ctrlc = ​{​ ​version​ ​=​ ​"3.0"​, ​features​ ​=​ ​["termination"]​ ​}
 
»[target.'cfg(any(target_arch = ​"arm"​, ​target_arch​ ​=​ ​"armv7"​​))'.dependencies]
»blinkt = ​"0.4.0"

This is the first time you’ve seen conditional compilation in action. The highlighted lines will only include the blinkt dependency when you’re compiling for the ARM architecture.

Watching for New Modules

There are a number of mission-critical tasks in the application and, as you’ll see, coordinating them can be really tricky. Thankfully Rust makes it pretty easy. The first task is monitoring the file system and then letting the main module runner know that it needs to reload a module. The following two functions make that possible:

 #​[​cfg​(​any​(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
 extern​ crate blinkt;
 
 extern​ crate ctrlc;
 extern​ crate notify;
 extern​ crate wasmi;
 
 use​ ​notify​::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
 use​ ​std​::​path​::Path;
 use​ ​std​::​sync​::​mpsc​::{channel, RecvTimeoutError, Sender};
 use​ ​std​::thread;
 use​ ​std​::​time​::Duration;
 use​ ​wasm​::Runtime;
 use​ ​wasmi​::RuntimeValue;
 
 const​ MODULE_FILE: &'static str = ​"/home/kevin/indicators/indicator.wasm"​;
 const​ MODULE_DIR: &'static str = ​"/home/kevin/indicators"​;
 
 enum​ RunnerCommand {
  Reload,
  Stop,
 }
 
 fn​ ​watch​(tx_wasm: Sender<RunnerCommand>) ​->​ ​notify​::Result<()> {
let​ (tx, rx) = ​channel​();
 
 let​ ​mut​ watcher: RecommendedWatcher =
 Watcher​::​new​(tx, ​Duration​::​from_secs​(1))?;
  watcher​.watch​(MODULE_DIR, ​RecursiveMode​::NonRecursive)?;
 
 loop​ {
match​ rx​.recv​() {
 Ok​(event) ​=>​ ​handle_event​(event, &tx_wasm),
 Err​(e) ​=>​ println!(​"watch error: {:?}"​, e),
  }
  }
 }
 
 fn​ ​handle_event​(event: DebouncedEvent, tx_wasm: &Sender<RunnerCommand>) {
 match​ event {
 DebouncedEvent​::​NoticeWrite​(path) ​=>​ {
 let​ path = ​Path​::​new​(&path);
 let​ filename = path​.file_name​()​.unwrap​();
 if​ filename == ​"indicator.wasm"​ {
tx_wasm​.send​(​RunnerCommand​::Reload)​.unwrap​();
  } ​else​ {
  println!(​"write (unexpected file): {:?}"​, path);
  }
  }
  _ ​=>​ {}
  }
 }

Creates a multi-producer, single-consumer communication channel

Block the receive channel until a message arrives

Send a message on the channel, indicating that we should reload the WebAssembly module

In Rust, multi-producer-single-consumer (mpsc) channels are used as a safe way to communicate between threads. In the case of the watch function, the main thread sits in a loop, awaiting file system notifications from the monitored directory. When we see the NoticeWrite event, it’s time to tell another thread to reload the module from disk.

Creating the Module Runner Thread

In the last section, you saw code that sends a message on a channel to tell another thread to reload a WebAssembly module. In this section, you’ll create that main thread. This thread has a number of jobs. It obviously needs to listen for the RunnerCommand::Reload message, but it also needs to handle the RunnerCommand::Stop message (which it will get from us monitoring OS signals). It also needs to invoke methods on the module itself, setting the fake sensor input value and calling apply to take care of animations.

This is where it would be so much easier to throw up our hands and walk away. Juggling file system monitoring, two different threads, signal trapping, and ensuring a consistent frame rate in the WebAssembly module sounds as complicated as dealing with the three-body problem.[34]

Luckily, there’s a solution that doesn’t require creating yet another coordination thread. Instead, we can use the timeout feature of channel receives. If we wait for the inverse of the frame rate in milliseconds for a message to arrive, and no messages comes, then we can invoke the apply function on the WebAssembly module. For example, if we want to enforce 20fps, then we would set our receive timeout delay to 50 milliseconds. For a frame rate of 10fps, we’d set the delay to 100 milliseconds, which actually produces a pretty good effect on the Blinkt hardware.

Let’s take a look at the code for the main function (anything from the wasm module is code that we’ll write shortly):

 fn​ ​main​() {
 let​ (tx_wasm, rx_wasm) = ​channel​();
 let​ _indicator_runner = ​thread​::​spawn​(​move​ || {
 let​ ​mut​ runtime = ​Runtime​::​new​();
 let​ ​mut​ module = ​wasm​::​get_module_instance​(MODULE_FILE);
  println!(​"Starting wasm runner thread..."​);
 loop​ {
match​ rx_wasm​.recv_timeout​(​Duration​::​from_millis​(100)) {
 Ok​(​RunnerCommand​::Reload) ​=>​ {
  println!(​"Received a reload signal, sleeping for 2s"​);
 thread​::​sleep​(​Duration​::​from_secs​(2));
  module = ​wasm​::​get_module_instance​(MODULE_FILE);
  }
 Ok​(​RunnerCommand​::Stop) ​=>​ {
  runtime​.shutdown​();
 break​;
  }
 Err​(​RecvTimeoutError​::Timeout) ​=>​ {
  runtime​.reduce_battery​();
  runtime​.advance_frame​();
  module
 .invoke_export​(
 "sensor_update"​,
  &[
 RuntimeValue​::​from​(​wasm​::SENSOR_BATTERY),
 RuntimeValue​::​F64​(
  runtime.remaining_battery​.into​()),
  ][..],
  &​mut​ runtime,
  )​.unwrap​();
 
  module
 .invoke_export​(
 "apply"​,
  &[​RuntimeValue​::​from​(runtime.frame)][..],
  &​mut​ runtime,
  )​.unwrap​();
  }
 Err​(_) ​=>​ ​break​,
  }
  }
  });
 
let​ tx_wasm_sig = tx_wasm​.clone​();
 
ctrlc​::​set_handler​(​move​ || {
  tx_wasm_sig​.send​(​RunnerCommand​::Stop)​.unwrap​();
  })​.expect​(​"Error setting Ctrl-C handler"​);
 
if​ ​let​ ​Err​(e) = ​watch​(tx_wasm) {
  println!(​"error: {:?}"​, e)
  }
 }
 
 mod​ wasm;

Enforce the frame rate with a 100ms timeout value on receive

Send channels can be cloned, hence their presence in the multi-producer module

Use the ctrlc crate to trap SIGTERM and SIGINT, sending a Stop command in response

The watch function blocks the main thread with an infinite loop

Creating the WebAssembly Module Runtime

Let’s create the wasm.rs module that was referenced by the previous set of code:

 use​ ​std​::fmt;
 use​ ​std​::​fs​::File;
 use​ ​wasmi​::{
  Error ​as​ InterpreterError, Externals, FuncInstance, FuncRef,
  HostError, ImportsBuilder, Module, ModuleImportResolver, ModuleInstance,
  ModuleRef, RuntimeArgs, RuntimeValue, Signature, Trap, ValueType,
 };
 
#[cfg(any(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
 use​ ​blinkt​::Blinkt;
 
 fn​ ​load_module​(path: &str) ​->​ Module {
 use​ ​std​::​io​::​prelude​::*;
 let​ ​mut​ file = ​File​::​open​(path)​.unwrap​();
 let​ ​mut​ wasm_buf = ​Vec​::​new​();
  file​.read_to_end​(&​mut​ wasm_buf)​.unwrap​();
 Module​::​from_buffer​(&wasm_buf)​.unwrap​()
 }
 
 pub​ ​fn​ ​get_module_instance​(path: &str) ​->​ ModuleRef {
 let​ module = ​load_module​(path);
 let​ ​mut​ imports = ​ImportsBuilder​::​new​();
  imports​.push_resolver​(​"env"​, &RuntimeModuleImportResolver);
 
 ModuleInstance​::​new​(&module, &imports)
 .expect​(​"Failed to instantiate module"​)
 .assert_no_start​()
 }
 
 pub​ ​const​ SENSOR_BATTERY: i32 = 20;
 
 
 #[derive(Debug)]
 pub​ ​enum​ Error {
 Interpreter​(InterpreterError),
 }
 
 impl​ ​fmt​::Display ​for​ Error {
 fn​ ​fmt​(&​self​, f: &​mut​ ​fmt​::Formatter) ​->​ ​fmt​::Result {
  write!(f, ​"{:?}"​, ​self​)
  }
 }
 
 impl​ From<InterpreterError> ​for​ Error {
 fn​ ​from​(e: InterpreterError) ​->​ Self {
 Error​::​Interpreter​(e)
  }
 }
 
 impl​ HostError ​for​ Error {}
 
 pub​ ​struct​ Runtime {
  #[cfg(any(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
blinkt: Blinkt,
 pub​ frame: i32,
 pub​ remaining_battery: f64,
 }
 
 impl​ Runtime {
  #[cfg(any(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
 pub​ ​fn​ ​new​() ​->​ Runtime {
  println!(​"Instiantiating WASM runtime (ARM)"​);
  Runtime {
  blinkt: ​Blinkt​::​new​()​.unwrap​(),
  frame: 0,
  remaining_battery: 100.0,
  }
  }
 
  #[cfg(not(any(target_arch = ​"armv7"​, target_arch = ​"arm"​)))]
 pub​ ​fn​ ​new​() ​->​ Runtime {
  println!(​"Instantiating WASM runtime (non-ARM)"​);
  Runtime {
  frame: 0,
  remaining_battery: 100.0,
  }
  }
 }
 
 impl​ Externals ​for​ Runtime {
 fn​ ​invoke_index​(
  &​mut​ ​self​,
  index: usize,
  args: RuntimeArgs,
  ) ​->​ Result<Option<RuntimeValue>, Trap> {
 
match​ index {
  0 ​=>​ {
 let​ idx: i32 = args​.nth​(0);
 let​ red: i32 = args​.nth​(1);
 let​ green: i32 = args​.nth​(2);
 let​ blue: i32 = args​.nth​(3);
 self​​.set_led​(idx, red, green, blue);
 Ok​(None)
  }
  _ ​=>​ panic!(​"Unknown function index!"​),
  }
  }
 }
 
 impl​ Runtime {
  #[cfg(not(any(target_arch = ​"armv7"​, target_arch = ​"arm"​)))]
 fn​ ​set_led​(&​self​, idx: i32, red: i32, green: i32, blue: i32) {
  println!(​"[LED {}]: {}, {}, {}"​, idx, red, green, blue);
  }
 
  #[cfg(any(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
 fn​ ​set_led​(&​mut​ ​self​, idx: i32, red: i32, green: i32, blue: i32) {
 self​.blinkt
 .set_pixel​(idx ​as​ usize, red ​as​ u8, green ​as​ u8, blue ​as​ u8);
 self​.blinkt​.show​()​.unwrap​();
  }
 
  #[cfg(not(any(target_arch = ​"armv7"​, target_arch = ​"arm"​)))]
 pub​ ​fn​ ​shutdown​(&​mut​ ​self​) {
  println!(​"WASM runtime shut down."​);
 self​​.halt​();
  }
 
  #[cfg(any(target_arch = ​"armv7"​, target_arch = ​"arm"​))]
 pub​ ​fn​ ​shutdown​(&​mut​ ​self​) {
  println!(​"WASM runtime shut down."​);
 self​.blinkt​.clear​();
 self​.blinkt​.cleanup​()​.unwrap​();
 self​​.halt​();
  }
 
 fn​ ​halt​(&​self​) {
  ::​std​::​process​::​exit​(0);
  }
 
 pub​ ​fn​ ​reduce_battery​(&​mut​ ​self​) {
 self​.remaining_battery ​-​= 1.0;
 if​ ​self​.remaining_battery < 0.0 {
 self​.remaining_battery = 100.0;
  }
  }
 
 pub​ ​fn​ ​advance_frame​(&​mut​ ​self​) {
 self​.frame += 1;
 if​ ​self​.frame > 1_000_000_000 {
 self​.frame = 0;
  }
  }
 }
 
 struct​ RuntimeModuleImportResolver;
 
 impl​<'a> ModuleImportResolver ​for​ RuntimeModuleImportResolver {
 fn​ ​resolve_func​(
  &​self​,
  field_name: &str,
  _signature: &Signature,
  ) ​->​ Result<FuncRef, InterpreterError> {
  println!(​"Resolving {}"​, field_name);
 let​ func_ref = ​match​ field_name {
"set_led"​ ​=>​ ​FuncInstance​::​alloc_host​(
 Signature​::​new​(
  &[
 ValueType​::I32,
 ValueType​::I32,
 ValueType​::I32,
 ValueType​::I32,
  ][..],
  None,
  ),
  0,
  ),
  _ ​=>​ {
 return​ ​Err​(​InterpreterError​::​Function​(format!(
 "host module doesn't export function with name {}"​,
  field_name
  )))
  }
  };
 Ok​(func_ref)
  }
 }

Conditionally add Blinkt to the module’s scope

Conditionally add a blinkt field to the Runtime struct

The apply function will have an index of 0.

The set_led function is the only one exported by the host/imported by the module

Most of this should look pretty familiar to you as a lot of it is just the boilerplate required to load the WebAssembly module, resolve its imports, and allow function calls.

You may have noticed one interesting thing: because of the conditional compilation, there are actually multiple functions with the same name. If we are building for ARM, then the Runtime struct gets an extra field called blinkt and the set_led function uses that field to control real hardware, whereas the “regular” version of that struct and function just emit debug text to the console.

Running the Application

To run the application on your workstation, just build the binary and execute it or type cargo run. This will launch the application and it will load the module at /home/kevin/indicators/indicator.wasm (feel free to change that location to suit your needs).

Running on a Mac, Windows, or Linux, you’ll see some console output spam as the module tells the host what LEDs to light up 10 times per second (the following is output from the animated pulse indicator):

 Instantiating Wasm runtime (non-ARM)
 Resolving set_led
 Starting wasm runner thread...
 [LED 0]: 0, 0, 0
 [LED 1]: 0, 0, 0
 [LED 2]: 0, 0, 0
 [LED 3]: 0, 0, 0
 [LED 4]: 0, 0, 0
 [LED 5]: 0, 0, 0
 [LED 6]: 0, 0, 0
 [LED 7]: 0, 0, 0
 [LED 1]: 255, 0, 0
 [LED 0]: 0, 0, 0
 [LED 1]: 0, 0, 0
 [LED 2]: 0, 0, 0
 [LED 3]: 0, 0, 0
 [LED 4]: 0, 0, 0
 [LED 5]: 0, 0, 0
 [LED 6]: 0, 0, 0
 ^CWasm runtime shut down.

Here I’ve tested the signal handling capabilities and the application shut down nicely in response to a Control-C. Next, we can try this out on real hardware.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.84.169