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(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|