forked from M-Labs/humpback-dds
mqtt_mux: init
This commit is contained in:
parent
54e91c49e0
commit
3758029a52
195
src/mqtt_mux.rs
Normal file
195
src/mqtt_mux.rs
Normal file
@ -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<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),
|
||||
|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<E>> {
|
||||
info!("{:?}", command_type);
|
||||
match command_type {
|
||||
Switch(channel, status) => self.urukul.set_channel_switch(channel as u32, status),
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user