diff --git a/Cargo.lock b/Cargo.lock index 93d20d8d..6e04a122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3800,6 +3800,15 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rppal" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612e1a22e21f08a246657c6433fe52b773ae43d07c9ef88ccfc433cc8683caba" +dependencies = [ + "libc", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4279,6 +4288,7 @@ dependencies = [ "slight-core", "slight-distributed-locking", "slight-file", + "slight-gpio", "slight-http-client", "slight-http-server", "slight-keyvalue", @@ -4370,6 +4380,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "slight-gpio" +version = "0.1.0" +dependencies = [ + "anyhow", + "rppal", + "slight-common", + "slight-file", + "tracing", + "wit-bindgen-wasmtime 0.2.0 (git+https://github.com/fermyon/wit-bindgen-backport)", + "wit-error-rs", +] + [[package]] name = "slight-http-api" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9f17d3f1..59eea5c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ slight-common = { workspace = true } slight-sql = { workspace = true, features = ["postgres"], optional = true } slight-http-server = { workspace = true, optional = true } slight-http-client = { workspace = true, optional = true } +slight-gpio = { workspace = true, optional = true } anyhow = { workspace = true } log = { workspace = true } tokio = { workspace = true } @@ -45,7 +46,7 @@ tempfile = { workspace = true } rand = { worspace = true } [features] -default = ["blob-store", "keyvalue", "distributed-locking", "messaging", "runtime-configs", "sql", "http-server", "http-client"] +default = ["blob-store", "keyvalue", "distributed-locking", "messaging", "runtime-configs", "sql", "http-server", "http-client", "gpio"] blob-store = ["dep:slight-blob-store"] keyvalue = ["dep:slight-keyvalue"] distributed-locking = ["dep:slight-distributed-locking"] @@ -54,6 +55,7 @@ runtime-configs = ["dep:slight-runtime-configs"] sql = ["dep:slight-sql"] http-server = ["dep:slight-http-server"] http-client = ["dep:slight-http-client"] +gpio = ["dep:slight-gpio"] [workspace.package] version = "0.5.1" @@ -76,6 +78,7 @@ slight-sql = { path = "./crates/sql" } slight-http-server = { path = "./crates/http-server" } slight-http-client = { path = "./crates/http-client" } slight-http-api = { path = "./crates/http-api" } +slight-gpio = { path = "./crates/gpio" } wit-bindgen-wasmtime = { git = "https://github.com/fermyon/wit-bindgen-backport", features = ["async"] } wit-error-rs = { git = "https://github.com/danbugs/wit-error-rs", rev = "05362f1a4a3a9dc6a1de39195e06d2d5d6491a5e" } wasmtime = "8.0.1" diff --git a/crates/gpio/Cargo.toml b/crates/gpio/Cargo.toml new file mode 100644 index 00000000..22529e43 --- /dev/null +++ b/crates/gpio/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "slight-gpio" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +rppal = { version = "0.14.1", optional = true } +slight-common = { workspace = true } +slight-file = { workspace = true } +tracing = { workspace = true } +wit-bindgen-wasmtime = { workspace = true } +wit-error-rs = { workspace = true } + +[features] +default = ["raspberry_pi"] +raspberry_pi = ["dep:rppal"] diff --git a/crates/gpio/README.md b/crates/gpio/README.md new file mode 100644 index 00000000..718374b2 --- /dev/null +++ b/crates/gpio/README.md @@ -0,0 +1,17 @@ +# GPIO support + +This crate provides the ability for SpiderLighting applications to interact with GPIO pins. + +Pins are named in the slightfile and passed a configuration string with the following slash-separated parameters: + +- Pin number +- Pin mode + - `input` + - `output` + - `pwm_output` +- Optional mode-specific configuration + - For input: `pullup` or `pulldown` + - For output: initially set to `low` or `high` + - For PWM output: the PWM period in microseconds, if supported by the implementation + +See the [example application](../../examples/gpio-demo/). diff --git a/crates/gpio/src/implementors/mod.rs b/crates/gpio/src/implementors/mod.rs new file mode 100644 index 00000000..9629208f --- /dev/null +++ b/crates/gpio/src/implementors/mod.rs @@ -0,0 +1,154 @@ +use std::fmt::{self, Debug, Formatter}; +use std::str::FromStr; +use std::sync::Arc; + +use crate::{gpio, Pin, Pull}; + +#[cfg(feature = "raspberry_pi")] +pub mod raspberry_pi; + +/// A GPIO implementation. +/// +/// This trait is not referred to directly by the linked capability, but is used to construct pin resources implementing the other traits in this module. +pub(crate) trait GpioImplementor { + /// Constructs an input pin resource. + fn new_input_pin( + &mut self, + pin: u8, + pull: Option, + ) -> Result, gpio::GpioError>; + /// Constructs an output pin resource. + fn new_output_pin( + &mut self, + pin: u8, + init_level: Option, + ) -> Result, gpio::GpioError>; + /// Constructs a PWM output pin resource. + fn new_pwm_output_pin( + &mut self, + pin: u8, + period_microseconds: Option, + ) -> Result, gpio::GpioError>; +} + +impl dyn GpioImplementor { + /// Parse the provided configuration string and construct the appropriate pin resource. + pub(crate) fn parse_pin_config(&mut self, config: &str) -> Result { + let mut config_iter = config.split('/'); + + let pin_number = config_iter + .next() + .ok_or_else(|| gpio::GpioError::ConfigurationError(String::from("no pin number")))?; + let pin_number = u8::from_str(pin_number).map_err(|e| { + gpio::GpioError::ConfigurationError(format!("invalid pin number '{pin_number}': {e}")) + })?; + + match config_iter.next() { + Some("input") => { + let pull = if let Some(pull) = config_iter.next() { + Some(match pull { + "pullup" => Pull::Up, + "pulldown" => Pull::Down, + _ => Err(gpio::GpioError::ConfigurationError(format!( + "unknown pull setting '{pull}'" + )))?, + }) + } else { + None + }; + if config_iter.next().is_some() { + return Err(gpio::GpioError::ConfigurationError(String::from( + "too many fields for input pin", + ))); + } + self.new_input_pin(pin_number, pull).map(Pin::Input) + } + Some("output") => { + let init_level = if let Some(init_level) = config_iter.next() { + Some(match init_level { + "low" => gpio::LogicLevel::Low, + "high" => gpio::LogicLevel::High, + _ => Err(gpio::GpioError::ConfigurationError(format!( + "unknown initial level '{init_level}'" + )))?, + }) + } else { + None + }; + if config_iter.next().is_some() { + return Err(gpio::GpioError::ConfigurationError(String::from( + "too many fields for output pin", + ))); + } + self.new_output_pin(pin_number, init_level).map(Pin::Output) + } + Some("pwm_output") => { + let period_microseconds = if let Some(period_microseconds) = config_iter.next() { + u32::from_str(period_microseconds).map(Some).map_err(|e| { + gpio::GpioError::ConfigurationError(format!( + "invalid period length '{period_microseconds}': {e}" + )) + })? + } else { + None + }; + if config_iter.next().is_some() { + return Err(gpio::GpioError::ConfigurationError(String::from( + "too many fields for PWM output pin", + ))); + } + self.new_pwm_output_pin(pin_number, period_microseconds) + .map(Pin::PwmOutput) + } + Some(unknown_type) => Err(gpio::GpioError::ConfigurationError(format!( + "unknown pin type '{unknown_type}'" + ))), + None => Err(gpio::GpioError::ConfigurationError(String::from( + "no pin type", + ))), + } + } +} + +/// An implementation of an input pin resource. +pub trait InputPinImplementor: Send + Sync { + /// Read the current logic level to the pin. + fn read(&self) -> gpio::LogicLevel; +} + +impl Debug for dyn InputPinImplementor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("InputPinImplementor") + .finish_non_exhaustive() + } +} + +/// An implementation of an output pin resource. +pub trait OutputPinImplementor: Send + Sync { + /// Write the given logic level to the pin. + fn write(&self, level: gpio::LogicLevel); +} + +impl Debug for dyn OutputPinImplementor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("OutputPinImplementor") + .finish_non_exhaustive() + } +} + +/// An implementation of a PWM output pin resource. +pub trait PwmOutputPinImplementor: Send + Sync { + /// Configure the pin with the given parameters. This does not enable it if it is disabled. + fn set_duty_cycle(&self, duty_cycle: f32); + /// Enable the pin's output. + fn enable(&self); + /// Disable the pin's output. + fn disable(&self); +} + +impl Debug for dyn PwmOutputPinImplementor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("PwmOutputPinImplementor") + .finish_non_exhaustive() + } +} diff --git a/crates/gpio/src/implementors/raspberry_pi.rs b/crates/gpio/src/implementors/raspberry_pi.rs new file mode 100644 index 00000000..92c3a6d6 --- /dev/null +++ b/crates/gpio/src/implementors/raspberry_pi.rs @@ -0,0 +1,172 @@ +use std::ops::DerefMut; +use std::sync::Mutex; +use std::time::Duration; + +use rppal::gpio::{Gpio, InputPin, Level, OutputPin}; + +use super::*; +use crate::{gpio, Pull}; + +/// A GPIO implementation backed by the [rppal] crate. +pub(crate) struct PiGpioImplementor { + gpio: Result, +} + +impl PiGpioImplementor { + /// Construct a new [PiGpioImplementor]. + /// + /// If constructing the underlying [Gpio] object fails, the error will be stored for reporting to the application code. + pub(crate) fn new() -> Self { + Self { + gpio: Gpio::new().map_err(|e| gpio::GpioError::HardwareError(e.to_string())), + } + } +} + +impl GpioImplementor for PiGpioImplementor { + fn new_input_pin( + &mut self, + pin: u8, + pull: Option, + ) -> Result, gpio::GpioError> { + let gpio = self.gpio.as_mut().map_err(|e| e.clone())?; + let pin = gpio + .get(pin) + .map_err(|e| gpio::GpioError::HardwareError(e.to_string()))?; + let input_pin = match pull { + Some(Pull::Up) => pin.into_input_pullup(), + Some(Pull::Down) => pin.into_input_pulldown(), + None => pin.into_input(), + }; + Ok(Arc::new(PiInputPinImplementor { input_pin })) + } + + fn new_output_pin( + &mut self, + pin: u8, + init_level: Option, + ) -> Result, gpio::GpioError> { + let gpio = self.gpio.as_mut().map_err(|e| e.clone())?; + let mut output_pin = gpio + .get(pin) + .map_err(|e| gpio::GpioError::HardwareError(e.to_string()))? + .into_output(); + match init_level { + Some(gpio::LogicLevel::Low) => output_pin.set_low(), + Some(gpio::LogicLevel::High) => output_pin.set_high(), + None => (), + } + let output_pin = Mutex::new(output_pin); + Ok(Arc::new(PiOutputPinImplementor { output_pin })) + } + + fn new_pwm_output_pin( + &mut self, + pin: u8, + period_microseconds: Option, + ) -> Result, gpio::GpioError> { + let gpio = self.gpio.as_mut().map_err(|e| e.clone())?; + let output_pin = gpio + .get(pin) + .map_err(|e| gpio::GpioError::HardwareError(e.to_string()))? + .into_output(); + + let period = Duration::from_micros(period_microseconds.unwrap_or(1000) as u64); + let pulse_width = period / 2; + + Ok(Arc::new(PiPwmOutputPinImplementor { + inner: Mutex::new(PiPwmInner { + output_pin, + period, + pulse_width, + enabled: false, + }), + })) + } +} + +/// An input pin resource backed by [rppal]'s [InputPin]. +pub struct PiInputPinImplementor { + input_pin: InputPin, +} + +impl InputPinImplementor for PiInputPinImplementor { + fn read(&self) -> gpio::LogicLevel { + match self.input_pin.read() { + Level::Low => gpio::LogicLevel::Low, + Level::High => gpio::LogicLevel::High, + } + } +} + +/// An output pin resource backed by [rppal]'s [OutputPin]. +pub struct PiOutputPinImplementor { + output_pin: Mutex, +} + +impl OutputPinImplementor for PiOutputPinImplementor { + fn write(&self, level: gpio::LogicLevel) { + self.output_pin.lock().unwrap().write(match level { + gpio::LogicLevel::Low => Level::Low, + gpio::LogicLevel::High => Level::High, + }); + } +} + +/// A PWM output pin resource backed by [rppal]'s [OutputPin]'s software PWM. +pub struct PiPwmOutputPinImplementor { + inner: Mutex, +} + +struct PiPwmInner { + output_pin: OutputPin, + period: Duration, + pulse_width: Duration, + enabled: bool, +} + +impl PwmOutputPinImplementor for PiPwmOutputPinImplementor { + fn set_duty_cycle(&self, duty_cycle: f32) { + let mut inner = self.inner.lock().unwrap(); + let PiPwmInner { + output_pin, + period, + pulse_width, + enabled, + } = inner.deref_mut(); + // panic safety: duty_cycle is defo a finite number between 0.0 and 1.0, so this can't go negative or overflow + *pulse_width = period.mul_f32(duty_cycle); + if *enabled { + if let Err(e) = output_pin.set_pwm(*period, *pulse_width) { + tracing::warn!("error enabling Raspberry Pi PWM: {e}"); + } + } + } + + fn enable(&self) { + let mut inner = self.inner.lock().unwrap(); + let PiPwmInner { + output_pin, + period, + pulse_width, + enabled, + } = inner.deref_mut(); + *enabled = true; + if let Err(e) = output_pin.set_pwm(*period, *pulse_width) { + tracing::warn!("error enabling Raspberry Pi PWM: {e}"); + } + } + + fn disable(&self) { + let mut inner = self.inner.lock().unwrap(); + let PiPwmInner { + output_pin, + enabled, + .. + } = inner.deref_mut(); + *enabled = false; + if let Err(e) = output_pin.clear_pwm() { + tracing::warn!("error disabling Raspberry Pi PWM: {e}"); + } + } +} diff --git a/crates/gpio/src/lib.rs b/crates/gpio/src/lib.rs new file mode 100644 index 00000000..a7cc65fb --- /dev/null +++ b/crates/gpio/src/lib.rs @@ -0,0 +1,172 @@ +use std::collections::hash_map::HashMap; +use std::sync::Arc; + +use implementors::*; + +use slight_common::{impl_resource, BasicState}; +use slight_file::capability_store::CapabilityStore; +use slight_file::resource::GpioResource::*; +use slight_file::Resource; + +mod implementors; +#[cfg(test)] +mod tests; + +wit_bindgen_wasmtime::export!("../../wit/gpio.wit"); +wit_error_rs::impl_error!(gpio::GpioError); + +/// Implements the GPIO interface defined by gpio.wit. +/// +/// This structure is responsible for constructing the pin resources described in the slightfile and providing them to the application upon request. +/// +/// It must be [Send], [Sync], and [Clone]. +#[derive(Clone)] +pub struct Gpio { + pins: HashMap>, +} + +/// A type for storing constructed pin resources. +/// +/// There should be one variant for each pin type, holding an [Arc] reference to the implementor trait object. +#[derive(Debug, Clone)] +enum Pin { + Input(Arc), + Output(Arc), + PwmOutput(Arc), +} + +impl Gpio { + /// Construct a new [Gpio] object. + /// + /// This function reads in the pin descriptors from the named state in `capability_store`. + pub fn new(name: &str, capability_store: CapabilityStore) -> Self { + let state = capability_store.get(name, "gpio").unwrap().clone(); + let mut implementor: Box = + match GpioImplementors::from(state.implementor) { + #[cfg(feature = "raspberry_pi")] + GpioImplementors::RaspberryPi => Box::new(raspberry_pi::PiGpioImplementor::new()), + }; + + let pins = state + .configs_map + .map(|configs| { + configs + .iter() + .map(|(name, config)| (name.clone(), implementor.parse_pin_config(config))) + .collect() + }) + .unwrap_or_else(HashMap::new); + + Self { pins } + } +} + +/// Directions that internal resistors can be configured to pull a floating wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Pull { + Up, + Down, +} + +/// A list of GPIO implementations that the slightfile can refer to. +#[derive(Debug, Clone)] +pub enum GpioImplementors { + #[cfg(feature = "raspberry_pi")] + RaspberryPi, +} + +impl From for GpioImplementors { + fn from(s: Resource) -> Self { + match s { + #[cfg(feature = "raspberry_pi")] + Resource::Gpio(RaspberryPi) => Self::RaspberryPi, + p => panic!( + "failed to match provided name (i.e., '{p}') to any known host implementations" + ), + } + } +} + +impl_resource!( + Gpio, + gpio::GpioTables, + gpio::add_to_linker, + "gpio".to_string() +); + +///converts between the wit and slight file config to be used +impl gpio::Gpio for Gpio { + type InputPin = Arc; + type OutputPin = Arc; + type PwmOutputPin = Arc; + + ///for input pins, gives the pin number + fn input_pin_get_named(&mut self, name: &str) -> Result { + match self.pins.get(name) { + Some(Ok(Pin::Input(pin))) => Ok(pin.clone()), + Some(Ok(_)) => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not an input pin" + ))), + Some(Err(e)) => Err(e.clone()), + None => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not a named pin" + ))), + } + } + + ///read the LogicLevel from pin (high/low) + fn input_pin_read(&mut self, self_: &Self::InputPin) -> gpio::LogicLevel { + self_.read() + } + + ///for output pins, gives the pin number + fn output_pin_get_named(&mut self, name: &str) -> Result { + match self.pins.get(name) { + Some(Ok(Pin::Output(pin))) => Ok(pin.clone()), + Some(Ok(_)) => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not an output pin" + ))), + Some(Err(e)) => Err(e.clone()), + None => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not a named pin" + ))), + } + } + + ///for output pins, stores the logic level + fn output_pin_write(&mut self, self_: &Self::OutputPin, level: gpio::LogicLevel) -> () { + self_.write(level) + } + + fn pwm_output_pin_get_named( + &mut self, + name: &str, + ) -> Result { + match self.pins.get(name) { + Some(Ok(Pin::PwmOutput(pin))) => Ok(pin.clone()), + Some(Ok(_)) => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not a PWM output pin" + ))), + Some(Err(e)) => Err(e.clone()), + None => Err(gpio::GpioError::PinUsageError(format!( + "'{name}' is not a named pin" + ))), + } + } + + fn pwm_output_pin_set_duty_cycle(&mut self, self_: &Self::PwmOutputPin, duty_cycle: f32) -> () { + self_.set_duty_cycle(if duty_cycle.is_nan() { + 0.0 + } else { + duty_cycle.clamp(0.0, 1.0) + }) + } + + fn pwm_output_pin_enable(&mut self, self_: &Self::PwmOutputPin) -> () { + self_.enable() + } + + fn pwm_output_pin_disable(&mut self, self_: &Self::PwmOutputPin) -> () { + self_.disable() + } +} diff --git a/crates/gpio/src/tests.rs b/crates/gpio/src/tests.rs new file mode 100644 index 00000000..2644f491 --- /dev/null +++ b/crates/gpio/src/tests.rs @@ -0,0 +1,191 @@ +use std::sync::Arc; + +use crate::implementors::*; +use crate::{gpio, Pull}; + +/// A no-op GPIO implementation used for testing. +/// +/// It stores the last [MockPin] it constructed to compare against what is expected for a given configuration. +#[derive(Default)] +struct MockGpioImplementor { + /// The last [MockPin] constructed, if any. + last_construction: Option, +} +///defines functions for new test gpioImplementor to be used in testing. Creates input/output pins +impl GpioImplementor for MockGpioImplementor { + fn new_input_pin( + &mut self, + pin: u8, + pull: Option, + ) -> Result, gpio::GpioError> { + let pin = MockPin::Input { pin, pull }; + self.last_construction.replace(pin); + Ok(Arc::new(pin)) + } + + fn new_output_pin( + &mut self, + pin: u8, + init_level: Option, + ) -> Result, gpio::GpioError> { + let pin = MockPin::Output { pin, init_level }; + self.last_construction.replace(pin); + Ok(Arc::new(pin)) + } + + fn new_pwm_output_pin( + &mut self, + pin: u8, + period_microseconds: Option, + ) -> Result, gpio::GpioError> { + let pin = MockPin::PwmOutput { + pin, + period_microseconds, + }; + self.last_construction.replace(pin); + Ok(Arc::new(pin)) + } +} + +/// A no-op implementation of every pin type, used for testing. +/// +/// It stores its type and the parameters it was constructed with to compare against what is expected for a given configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MockPin { + Input { + pin: u8, + pull: Option, + }, + Output { + pin: u8, + init_level: Option, + }, + PwmOutput { + pin: u8, + period_microseconds: Option, + }, +} + +/// Defines read for inputPins +impl InputPinImplementor for MockPin { + fn read(&self) -> gpio::LogicLevel { + gpio::LogicLevel::Low + } +} +/// Defines write for outputPins +impl OutputPinImplementor for MockPin { + fn write(&self, _: gpio::LogicLevel) {} +} + +impl PwmOutputPinImplementor for MockPin { + fn set_duty_cycle(&self, _: f32) {} + fn enable(&self) {} + fn disable(&self) {} +} + +/// First test checks that the format pinNum/type\[/subType\] is followed +#[test] +fn good_pin_configs() { + let mut gpio = MockGpioImplementor::default(); + for (config, expected) in [ + ("1/input", MockPin::Input { pin: 1, pull: None }), + ( + "2/input/pullup", + MockPin::Input { + pin: 2, + pull: Some(Pull::Up), + }, + ), + ( + "3/input/pulldown", + MockPin::Input { + pin: 3, + pull: Some(Pull::Down), + }, + ), + ( + "4/output", + MockPin::Output { + pin: 4, + init_level: None, + }, + ), + ( + "5/output/low", + MockPin::Output { + pin: 5, + init_level: Some(gpio::LogicLevel::Low), + }, + ), + ( + "6/output/high", + MockPin::Output { + pin: 6, + init_level: Some(gpio::LogicLevel::High), + }, + ), + ( + "7/pwm_output", + MockPin::PwmOutput { + pin: 7, + period_microseconds: None, + }, + ), + ( + "8/pwm_output/250", + MockPin::PwmOutput { + pin: 8, + period_microseconds: Some(250), + }, + ), + ] { + // parse through pin configs and checks if it is valid. This goes through the slight file config. + let result = (&mut gpio as &mut dyn GpioImplementor).parse_pin_config(config); + assert!(result.is_ok(), "good config '{config}' returned {result:?}"); + match gpio.last_construction { + Some(actual) => assert_eq!( + expected, actual, + "config '{config}': expected {expected:?}, got {actual:?}" + ), + None => panic!("no pin constructed for '{config}' (result is {result:?})"), + } + } +} + +/// Tests for bad pin inputs that do not follow pinNum/type\[/subType\] +#[test] +fn bad_pin_configs() { + let mut gpio = MockGpioImplementor::default(); + let gpio: &mut dyn GpioImplementor = &mut gpio; + for config in [ + "", + "some", + "body/once", + "told/me/the", + "1/world", + "1/input/was", + "1/output/gonna", + "1/input/pullup/roll", + "1/output/low/me", + "-1/input", + "420/output", + "3.1415/input", + "///", + "2.71828/Eureka!", + "1/2/3", + "input/input/input", + "1/pwm_output/high", + "1/pwm_output/-4", + "1/pwm_output/99999999999999999999999999999", + ] { + match gpio.parse_pin_config(config) { + Err(gpio::GpioError::ConfigurationError(_)) => (), + Err(wrong) => { + panic!( + "bad config '{config}' returned {wrong:?}, which is not a ConfigurationError" + ) + } + Ok(pin) => panic!("bad config '{config}' somehow returned Ok({pin:?}"), + } + } +} diff --git a/crates/slightfile/src/resource.rs b/crates/slightfile/src/resource.rs index 8703005a..f981b7af 100644 --- a/crates/slightfile/src/resource.rs +++ b/crates/slightfile/src/resource.rs @@ -173,6 +173,20 @@ impl Display for SqlResource { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] +pub enum GpioResource { + #[serde(rename = "gpio.raspberry_pi")] + RaspberryPi, +} + +impl Display for GpioResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GpioResource::RaspberryPi => write!(f, "gpio.raspberry_pi"), + } + } +} + /// All the resources that slightfile supports. This is used in the /// `Capability` section in slightfile to specify what resource a /// capability is for. @@ -187,6 +201,7 @@ pub enum Resource { Configs(ConfigsResource), DistributedLocking(DistributedLockingResource), Sql(SqlResource), + Gpio(GpioResource), } impl Default for Resource { @@ -206,6 +221,7 @@ impl Resource { Resource::Configs(_) => "configs".into(), Resource::DistributedLocking(_) => "distributed_locking".into(), Resource::Sql(_) => "sql".into(), + Resource::Gpio(_) => "gpio".into(), } } } @@ -223,6 +239,7 @@ impl Display for Resource { write!(f, "{distributed_locking}") } Resource::Sql(sql_postgres) => write!(f, "{sql_postgres}"), + Resource::Gpio(gpio) => write!(f, "{gpio}"), } } } diff --git a/examples/gpio-demo/Cargo.lock b/examples/gpio-demo/Cargo.lock new file mode 100644 index 00000000..39625e46 --- /dev/null +++ b/examples/gpio-demo/Cargo.lock @@ -0,0 +1,234 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "gpio-demo" +version = "0.1.0" +dependencies = [ + "anyhow", + "wit-bindgen-rust", + "wit-error-rs", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "proc-macro2" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wit-bindgen-gen-core" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "anyhow", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-gen-rust" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "heck", + "wit-bindgen-gen-core", +] + +[[package]] +name = "wit-bindgen-gen-rust-wasm" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "heck", + "wit-bindgen-gen-core", + "wit-bindgen-gen-rust", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "async-trait", + "bitflags", + "wit-bindgen-rust-impl", +] + +[[package]] +name = "wit-bindgen-rust-impl" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wit-bindgen-gen-core", + "wit-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wit-error-rs" +version = "0.1.0" +source = "git+https://github.com/danbugs/wit-error-rs?rev=05362f1a4a3a9dc6a1de39195e06d2d5d6491a5e#05362f1a4a3a9dc6a1de39195e06d2d5d6491a5e" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wit-parser" +version = "0.2.0" +source = "git+https://github.com/fermyon/wit-bindgen-backport#ba1636af0338623b54db84e2224be9a124e231f6" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] diff --git a/examples/gpio-demo/Cargo.toml b/examples/gpio-demo/Cargo.toml new file mode 100644 index 00000000..b0f185dc --- /dev/null +++ b/examples/gpio-demo/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gpio-demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +wit-bindgen-rust = { git = "https://github.com/fermyon/wit-bindgen-backport" } +wit-error-rs = { git = "https://github.com/danbugs/wit-error-rs", rev = "05362f1a4a3a9dc6a1de39195e06d2d5d6491a5e" } + +[workspace] diff --git a/examples/gpio-demo/slightfile.toml b/examples/gpio-demo/slightfile.toml new file mode 100644 index 00000000..42dc4f6c --- /dev/null +++ b/examples/gpio-demo/slightfile.toml @@ -0,0 +1,10 @@ +specversion = "0.2" + +[[capability]] +resource = "gpio.raspberry_pi" +name = "my-gpio" + [capability.configs] + push_down_button = "17/input/pulldown" + led = "5/output" + pwm_control_button = "18/input/pullup" + pwm_led = "6/pwm_output" diff --git a/examples/gpio-demo/src/main.rs b/examples/gpio-demo/src/main.rs new file mode 100644 index 00000000..b294e0e6 --- /dev/null +++ b/examples/gpio-demo/src/main.rs @@ -0,0 +1,67 @@ +//! GPIO demo +//! Authors: Kai Page, Brendan Burmeister, Joey Vongphasouk +//! +//! Expected Output: +//! - Have a red LED blink, LED blinks faster when +//! a button input is received +//! - Have a blue LED get brighter when a button is held down +//! and dimmer when button is released +//! +//! Tested Output: +//! - Red LED blinks on startup and blinks faster when +//! button input received. Demo works when multiple inputs to GPIO +//! given. Red LED turns off at the end when demo ends. +//! - Blue LED is initially off when demo is run. When button is pushed +//! down, blue LED gets brighter. LED immediately starts getting dimmer +//! when button is let go. + +use anyhow::Result; +use gpio::*; +use std::thread; +use std::time::Duration; +wit_bindgen_rust::import!("../../wit/gpio.wit"); +wit_error_rs::impl_error!(GpioError); + +const BLINK_THRESHOLD: u32 = 500; + +fn main() -> Result<()> { + // Define variables based on configurations in demo slightfile + let input_pin = InputPin::get_named("push_down_button")?; + let output_pin = OutputPin::get_named("led")?; + let pwm_control_pin = InputPin::get_named("pwm_control_button")?; + let pwm_output_pin = PwmOutputPin::get_named("pwm_led")?; + + let mut blink_progress = 0; + let mut blink_current = LogicLevel::High; + let mut pwm_duty_cycle: f32 = 0.0; + + output_pin.write(LogicLevel::High); + pwm_output_pin.set_duty_cycle(0.0); + pwm_output_pin.enable(); + + // Run infinite loop that updates outputs based on inputs + loop { + blink_progress += match input_pin.read() { + LogicLevel::Low => 1, + LogicLevel::High => 2, + }; + if blink_progress >= BLINK_THRESHOLD { + blink_current = match blink_current { + LogicLevel::Low => LogicLevel::High, + LogicLevel::High => LogicLevel::Low, + }; + output_pin.write(blink_current); + blink_progress = 0; + } + + pwm_duty_cycle = (pwm_duty_cycle + + match pwm_control_pin.read() { + LogicLevel::Low => -0.001, + LogicLevel::High => 0.001, + }) + .clamp(0.0, 1.0); + pwm_output_pin.set_duty_cycle(pwm_duty_cycle); + + thread::sleep(Duration::from_millis(1)); + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs index d9cac19b..bb235fd6 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -20,6 +20,8 @@ use slight_http_client::HttpClient; #[cfg(feature = "http-server")] use slight_http_server::{HttpServer, HttpServerInit}; +#[cfg(feature = "gpio")] +use slight_gpio::Gpio; #[cfg(feature = "keyvalue")] use slight_keyvalue::Keyvalue; #[cfg(feature = "messaging")] @@ -213,6 +215,9 @@ fn link_all_caps(builder: &mut Builder, linked_capabilities: &mut HashSet()?; + linked_capabilities.insert("gpio".to_string()); + Ok(()) } @@ -330,6 +335,16 @@ async fn build_store_instance( linked_capabilities.insert("http-client".to_string()); } } + #[cfg(feature = "gpio")] + Resource::Gpio(_) => { + if !linked_capabilities.contains("gpio") { + let gpio = Gpio::new(&c.name().to_string(), capability_store.clone()); + builder + .link_capability::()? + .add_to_builder("gpio".to_string(), gpio); + linked_capabilities.insert("gpio".to_string()); + } + } } } diff --git a/wit/gpio.wit b/wit/gpio.wit new file mode 100644 index 00000000..2dda93ad --- /dev/null +++ b/wit/gpio.wit @@ -0,0 +1,38 @@ +variant gpio-error { + configuration-error(string), + pin-usage-error(string), + hardware-error(string), + unexpected-error(string), +} + +enum logic-level { + low, + high, +} + +resource input-pin { + static get-named: func(name: string) -> expected + + read: func() -> logic-level +} + +resource output-pin { + static get-named: func(name: string) -> expected + + write: func(level: logic-level) -> unit +} + +/// A pin that can output a PWM signal. +resource pwm-output-pin { + /// Acquire a handle to the pin named in the slightfile. + static get-named: func(name: string) -> expected + + /// Configure the pin's duty cycle, which should be and will be clamped between 0.0 and 1.0. + /// This does not enable the pin if it is currently disabled. + set-duty-cycle: func(duty-cycle: float32) -> unit + + /// Enable the output signal. + enable: func() -> unit + /// Disable the output signal. + disable: func() -> unit +}