use log::info; use embedded_hal::blocking::spi::Transfer; use core::convert::TryInto; use crate::ClockSource as UrukulClockSource; use crate::ClockSource::*; use crate::Urukul; use crate::Error; #[derive(Debug)] pub enum MqttCommandType { // Urukul/Control/Clock/Source ClockSource(UrukulClockSource), // Urukul/Control/Clock/Division ClockDivision(u8), // Urukul/Control/ChannelX/Switch Switch(u8, bool), // Urukul/Control/ChannelX/Attenuation Attenuation(u8, f32), // Urukul/Control/ChannelX/SystemClock SystemClock(u8, f64), // Urukul/Control/ChannelX/ProfileY/Frequency SingleToneFrequency(u8, u8, f64), // Urukul/Control/ChannelX/ProfileY/Amplitude SingleToneAmplitude(u8, u8, f64), // Urukul/Control/ChannelX/ProfileY/Phase SingleTonePhase(u8, u8, f64), } use crate::mqtt_mux::MqttCommandType::*; pub struct MqttMux { urukul: Urukul } impl MqttMux where SPI: Transfer { pub fn new(urukul: Urukul) -> Self { MqttMux { urukul } } pub fn handle_command(&mut self, topic: &str, message: &[u8]) -> Result<(), Error> { let command = self.parse(topic, message)?; self.execute(command) } // MQTT command are not case tolerant // If the command differs by case, space or delimiter, it is a wrong command // A starting forward slash ("/") is acceptable, as per MQTT standard // Topic should contain the appropriate command header // Message should provide the parameter fn parse(&mut self, topic: &str, message: &[u8]) -> Result> { let mut assigned_channel = false; let mut assigned_profile = false; let mut channel :u8 = 0; let mut profile :u8 = 0; // Verify that the topic must start with Urukul/Control/ or /Urukul/Control/ let mut header = topic.strip_prefix("/Urukul/Control/") .or_else(|| topic.strip_prefix("Urukul/Control/")) .ok_or(Error::MqttCommandError)?; loop { match header { // The topic has a channel subtopic _ if header.starts_with("Channel") => { // MQTT command should only mention channel once appropriately // Channel must be referred before profile, // as a channel is broader than a profile if assigned_channel || assigned_profile { return Err(Error::MqttCommandError); } // Remove the "Channel" part of the subtopic header = header.strip_prefix("Channel") .ok_or(Error::MqttCommandError)?; // Remove the channel number at the end of the subtopic // But store the channel as a char, so it can be removed easily let numeric_char :char = header.chars() .next() .ok_or(Error::MqttCommandError)?; // Record the channel number channel = numeric_char.to_digit(10) .ok_or(Error::MqttCommandError)? .try_into() .unwrap(); assigned_channel = true; header = header.strip_prefix(numeric_char) .ok_or(Error::MqttCommandError)?; // Remove forward slash ("/") header = header.strip_prefix("/") .ok_or(Error::MqttCommandError)?; }, _ if header.starts_with("Profile") => { // MQTT command should only mention profile once appropriately if assigned_profile { return Err(Error::MqttCommandError); } // Remove the "Profile" part of the subtopic header = header.strip_prefix("Profile") .ok_or(Error::MqttCommandError)?; // Remove the profile number at the end of the subtopic // But store the profile as a char, so it can be removed easily let numeric_char :char = header.chars() .next() .ok_or(Error::MqttCommandError)?; // Record the channel number profile = numeric_char.to_digit(10) .ok_or(Error::MqttCommandError)? .try_into() .unwrap(); assigned_profile = true; header = header.strip_prefix(numeric_char) .ok_or(Error::MqttCommandError)?; // Remove forward slash ("/") header = header.strip_prefix("/") .ok_or(Error::MqttCommandError)?; }, "Clock/Source" => { // Clock/Source refers to the Urukul clock source // It should be common for all channels and profiles if assigned_channel || assigned_profile { return Err(Error::MqttCommandError); } let source_string = core::str::from_utf8(message).unwrap(); return match source_string { _ if source_string.eq_ignore_ascii_case("OSC") => { Ok(ClockSource(OSC)) }, _ if source_string.eq_ignore_ascii_case("SMA") => { Ok(ClockSource(SMA)) }, _ if source_string.eq_ignore_ascii_case("MMCX") => { Ok(ClockSource(MMCX)) }, _ => Err(Error::MqttCommandError), }; } "Clock/Division" => { // Clock/Division refers to the Urukul clock division // It should be common for all channels and profiles if assigned_channel || assigned_profile { return Err(Error::MqttCommandError); } let division = u8::from_str_radix(core::str::from_utf8(message).unwrap(), 10) .map_or_else( |_| Err(Error::MqttCommandError), |div| if (div == 1 || div == 2 || div == 4) { Ok(div) } else { Err(Error::MqttCommandError) })?; return Ok(ClockDivision(division)); } "Switch" => { // Switch is a channel specific topic if !(assigned_channel && !assigned_profile) { return Err(Error::MqttCommandError); } let switch_string = core::str::from_utf8(message).unwrap(); return match switch_string { _ if switch_string.eq_ignore_ascii_case("on") => { Ok(Switch(channel, true)) }, _ if switch_string.eq_ignore_ascii_case("off") => { Ok(Switch(channel, false)) }, _ => Err(Error::MqttCommandError), }; }, // TODO: Cover all commands _ => return Err(Error::MqttCommandError), }; } } // TODO: Implement this fn execute(&mut self, command_type: MqttCommandType) -> Result<(), Error> { info!("{:?}", command_type); match command_type { Switch(channel, status) => self.urukul.set_channel_switch(channel as u32, status), _ => Ok(()) } } }