humpback-dds/src/mqtt_mux.rs

197 lines
8.3 KiB
Rust
Raw Normal View History

2020-09-17 17:02:01 +08:00
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<SPI> {
urukul: Urukul<SPI>
}
impl<SPI, E> MqttMux<SPI> where SPI: Transfer<u8, Error = E> {
pub fn new(urukul: Urukul<SPI>) -> Self {
MqttMux {
urukul
}
}
pub fn handle_command(&mut self, topic: &str, message: &[u8]) -> Result<(), Error<E>> {
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<MqttCommandType, Error<E>> {
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),
2020-09-18 12:24:27 +08:00
|div| if div == 1 || div == 2 || div == 4 {
2020-09-17 17:02:01 +08:00
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
2020-09-18 12:24:27 +08:00
// Only need to sort the command enum
// Obviously. This is what a MUX does
2020-09-17 17:02:01 +08:00
fn execute(&mut self, command_type: MqttCommandType) -> Result<(), Error<E>> {
info!("{:?}", command_type);
match command_type {
Switch(channel, status) => self.urukul.set_channel_switch(channel as u32, status),
_ => Ok(())
}
}
}