From 3758029a524cc063b7516612302d52c882f3ea5d Mon Sep 17 00:00:00 2001 From: occheung Date: Thu, 17 Sep 2020 17:02:01 +0800 Subject: [PATCH] mqtt_mux: init --- src/mqtt_mux.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/mqtt_mux.rs diff --git a/src/mqtt_mux.rs b/src/mqtt_mux.rs new file mode 100644 index 0000000..95c41ae --- /dev/null +++ b/src/mqtt_mux.rs @@ -0,0 +1,195 @@ +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(()) + } + } +} \ No newline at end of file