use heapless::Vec;
const UNIVERSAL_SYSEX: u8 = 0x7E;
const SUB_ID1_MIDI_CI: u8 = 0x0D;
pub const PE_GET_INQUIRY: u8 = 0x34;
pub const PE_GET_REPLY: u8 = 0x35;
pub const PE_SET_INQUIRY: u8 = 0x36;
pub const PE_SET_REPLY: u8 = 0x37;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PeStatus {
Ok = 0x00,
NotFound = 0x01,
FormatError = 0x02,
VersionMismatch = 0x03,
}
impl PeStatus {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0x00 => Some(Self::Ok),
0x01 => Some(Self::NotFound),
0x02 => Some(Self::FormatError),
0x03 => Some(Self::VersionMismatch),
_ => None,
}
}
pub fn is_ok(self) -> bool {
self == Self::Ok
}
}
const CI_VERSION: u8 = 0x02;
pub fn is_ci_message(buf: &[u8]) -> bool {
buf.len() >= 15 && buf[0] == 0xF0 && buf[1] == UNIVERSAL_SYSEX && buf[3] == SUB_ID1_MIDI_CI
}
pub fn is_set_property(buf: &[u8]) -> bool {
is_ci_message(buf) && buf[4] == PE_SET_INQUIRY
}
pub fn request_id(buf: &[u8]) -> u8 {
buf[14]
}
pub fn source_muid(buf: &[u8]) -> [u8; 4] {
[buf[6], buf[7], buf[8], buf[9]]
}
pub struct SetPropertyData<'a> {
pub resource: u8,
pub body: &'a [u8],
}
pub fn extract_set_property(buf: &[u8]) -> Option<SetPropertyData<'_>> {
if !is_set_property(buf) {
return None;
}
if buf.len() < 16 {
return None;
}
let mut pos = 15;
if pos + 2 > buf.len() {
return None;
}
let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
pos += 2;
let resource = if header_len > 0 && pos < buf.len() {
buf[pos]
} else {
0
};
pos += header_len;
if pos + 4 > buf.len() {
return None;
}
pos += 4;
if pos + 2 > buf.len() {
return None;
}
let body_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
pos += 2;
if pos + body_len > buf.len() {
return None;
}
Some(SetPropertyData {
resource,
body: &buf[pos..pos + body_len],
})
}
pub fn extract_body(buf: &[u8]) -> Option<&[u8]> {
extract_set_property(buf).map(|d| d.body)
}
pub fn build_set_reply(
device_muid: [u8; 4],
dest_muid: [u8; 4],
req_id: u8,
status: PeStatus,
) -> Vec<u8, 32> {
let mut msg: Vec<u8, 32> = Vec::new();
let _ = msg.push(0xF0);
let _ = msg.push(UNIVERSAL_SYSEX);
let _ = msg.push(0x7F); let _ = msg.push(SUB_ID1_MIDI_CI);
let _ = msg.push(PE_SET_REPLY);
let _ = msg.push(CI_VERSION);
for &b in &device_muid {
let _ = msg.push(b);
}
for &b in &dest_muid {
let _ = msg.push(b);
}
let _ = msg.push(req_id);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(status as u8);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x00);
let _ = msg.push(0x00);
let _ = msg.push(0xF7);
msg
}
pub fn build_set_inquiry(
source_muid: [u8; 4],
dest_muid: [u8; 4],
req_id: u8,
resource: u8,
body: &[u8],
) -> Vec<u8, 350> {
let mut msg: Vec<u8, 350> = Vec::new();
let _ = msg.push(0xF0);
let _ = msg.push(UNIVERSAL_SYSEX);
let _ = msg.push(0x7F);
let _ = msg.push(SUB_ID1_MIDI_CI);
let _ = msg.push(PE_SET_INQUIRY);
let _ = msg.push(CI_VERSION);
for &b in &source_muid {
let _ = msg.push(b);
}
for &b in &dest_muid {
let _ = msg.push(b);
}
let _ = msg.push(req_id);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(resource);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let mut encoded_body = [0u8; 300];
let enc_len = encode_mcoded7(body, &mut encoded_body);
let _ = msg.push((enc_len & 0x7F) as u8);
let _ = msg.push(((enc_len >> 7) & 0x7F) as u8);
for &b in &encoded_body[..enc_len] {
let _ = msg.push(b);
}
let _ = msg.push(0xF7);
msg
}
pub fn is_get_property(buf: &[u8]) -> bool {
is_ci_message(buf) && buf[4] == PE_GET_INQUIRY
}
pub fn is_get_reply(buf: &[u8]) -> bool {
is_ci_message(buf) && buf[4] == PE_GET_REPLY
}
pub fn extract_reply_status(buf: &[u8]) -> Option<PeStatus> {
if buf.len() < 16 || !is_ci_message(buf) {
return None;
}
let sub_id2 = buf[4];
if sub_id2 != PE_SET_REPLY && sub_id2 != PE_GET_REPLY {
return None;
}
let pos = 15;
if pos + 2 > buf.len() {
return None;
}
let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
if header_len == 0 {
return Some(PeStatus::Ok);
}
if pos + 2 + 1 > buf.len() {
return None;
}
PeStatus::from_byte(buf[pos + 2])
}
pub fn extract_get_body(buf: &[u8]) -> Option<&[u8]> {
if !is_get_reply(buf) || buf.len() < 16 {
return None;
}
let mut pos = 15;
if pos + 2 > buf.len() {
return None;
}
let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
pos += 2 + header_len;
if pos + 4 > buf.len() {
return None;
}
pos += 4;
if pos + 2 > buf.len() {
return None;
}
let body_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
pos += 2;
if pos + body_len > buf.len() {
return None;
}
Some(&buf[pos..pos + body_len])
}
pub fn extract_get_resource(buf: &[u8]) -> Option<u8> {
if !is_get_property(buf) || buf.len() < 16 {
return None;
}
let pos = 15;
if pos + 2 > buf.len() {
return None;
}
let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
if header_len > 0 && pos + 2 < buf.len() {
Some(buf[pos + 2])
} else {
Some(0)
}
}
pub fn build_get_inquiry(
source_muid: [u8; 4],
dest_muid: [u8; 4],
req_id: u8,
resource: u8,
) -> Vec<u8, 32> {
let mut msg: Vec<u8, 32> = Vec::new();
let _ = msg.push(0xF0);
let _ = msg.push(UNIVERSAL_SYSEX);
let _ = msg.push(0x7F);
let _ = msg.push(SUB_ID1_MIDI_CI);
let _ = msg.push(PE_GET_INQUIRY);
let _ = msg.push(CI_VERSION);
for &b in &source_muid {
let _ = msg.push(b);
}
for &b in &dest_muid {
let _ = msg.push(b);
}
let _ = msg.push(req_id);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(resource);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x00);
let _ = msg.push(0x00);
let _ = msg.push(0xF7);
msg
}
pub fn build_get_reply(
device_muid: [u8; 4],
dest_muid: [u8; 4],
req_id: u8,
resource: u8,
status: PeStatus,
body: &[u8],
) -> Vec<u8, 350> {
let mut msg: Vec<u8, 350> = Vec::new();
let _ = msg.push(0xF0);
let _ = msg.push(UNIVERSAL_SYSEX);
let _ = msg.push(0x7F);
let _ = msg.push(SUB_ID1_MIDI_CI);
let _ = msg.push(PE_GET_REPLY);
let _ = msg.push(CI_VERSION);
for &b in &device_muid {
let _ = msg.push(b);
}
for &b in &dest_muid {
let _ = msg.push(b);
}
let _ = msg.push(req_id);
let _ = msg.push(0x02);
let _ = msg.push(0x00);
let _ = msg.push(status as u8);
let _ = msg.push(resource);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let mut encoded_body = [0u8; 300];
let enc_len = encode_mcoded7(body, &mut encoded_body);
let _ = msg.push((enc_len & 0x7F) as u8);
let _ = msg.push(((enc_len >> 7) & 0x7F) as u8);
for &b in &encoded_body[..enc_len] {
let _ = msg.push(b);
}
let _ = msg.push(0xF7);
msg
}
pub fn encode_mcoded7(data: &[u8], out: &mut [u8]) -> usize {
let mut pos = 0;
for chunk in data.chunks(7) {
let mut high_bits: u8 = 0;
for (i, &b) in chunk.iter().enumerate() {
if b & 0x80 != 0 {
high_bits |= 1 << i;
}
}
out[pos] = high_bits;
pos += 1;
for &b in chunk {
out[pos] = b & 0x7F;
pos += 1;
}
}
pos
}
pub fn decode_mcoded7(data: &[u8], out: &mut [u8]) -> usize {
let mut pos = 0;
let mut i = 0;
while i < data.len() {
let high_bits = data[i];
i += 1;
for bit in 0..7 {
if i >= data.len() {
break;
}
out[pos] = data[i] | (if high_bits & (1 << bit) != 0 { 0x80 } else { 0 });
pos += 1;
i += 1;
}
}
pos
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_set_property() {
let payload = b"hello";
let msg = build_set_inquiry(
[0x10, 0x20, 0x30, 0x40],
[0x01, 0x02, 0x03, 0x04],
0x07,
3,
payload,
);
assert!(is_ci_message(&msg));
assert!(is_set_property(&msg));
assert_eq!(request_id(&msg), 0x07);
assert_eq!(source_muid(&msg), [0x10, 0x20, 0x30, 0x40]);
let data = extract_set_property(&msg).unwrap();
assert_eq!(data.resource, 3);
let mut decoded = [0u8; 32];
let dec_len = decode_mcoded7(data.body, &mut decoded);
assert_eq!(&decoded[..dec_len], b"hello");
}
#[test]
fn not_ci_for_opendeck() {
let buf = [0xF0, 0x00, 0x53, 0x43, 0x00, 0x00, 0x01, 0xF7];
assert!(!is_ci_message(&buf));
}
#[test]
fn reply_structure() {
let reply = build_set_reply(
[0x01, 0x02, 0x03, 0x04],
[0x10, 0x20, 0x30, 0x40],
0x07,
PeStatus::Ok,
);
assert_eq!(reply[0], 0xF0);
assert_eq!(reply[4], PE_SET_REPLY);
assert_eq!(reply[14], 0x07);
assert_eq!(reply[15], 0x01); assert_eq!(reply[17], 0x00); assert_eq!(*reply.last().unwrap(), 0xF7);
assert_eq!(extract_reply_status(&reply), Some(PeStatus::Ok));
}
#[test]
fn reply_status_not_found() {
let reply = build_set_reply(
[0x01, 0x02, 0x03, 0x04],
[0x10, 0x20, 0x30, 0x40],
0x01,
PeStatus::NotFound,
);
assert_eq!(extract_reply_status(&reply), Some(PeStatus::NotFound));
}
#[test]
fn get_reply_status_extracted() {
let reply = build_get_reply(
[0x01, 0x02, 0x03, 0x04],
[0x10, 0x20, 0x30, 0x40],
0x01,
5,
PeStatus::NotFound,
&[],
);
assert_eq!(extract_reply_status(&reply), Some(PeStatus::NotFound));
}
#[test]
fn extract_body_with_header() {
let mut msg: Vec<u8, 128> = Vec::new();
let _ = msg.push(0xF0);
let _ = msg.push(UNIVERSAL_SYSEX);
let _ = msg.push(0x7F);
let _ = msg.push(SUB_ID1_MIDI_CI);
let _ = msg.push(PE_SET_INQUIRY);
let _ = msg.push(CI_VERSION);
for &b in &[0x10, 0x20, 0x30, 0x40, 0x01, 0x02, 0x03, 0x04] {
let _ = msg.push(b);
}
let _ = msg.push(0x01); let _ = msg.push(0x03);
let _ = msg.push(0x00);
for &b in b"abc" {
let _ = msg.push(b);
}
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x01);
let _ = msg.push(0x00);
let _ = msg.push(0x04);
let _ = msg.push(0x00);
for &b in b"data" {
let _ = msg.push(b);
}
let _ = msg.push(0xF7);
assert_eq!(extract_body(&msg).unwrap(), b"data");
}
#[test]
fn get_property_roundtrip() {
let inquiry =
build_get_inquiry([0x10, 0x20, 0x30, 0x40], [0x01, 0x02, 0x03, 0x04], 0x05, 7);
assert!(is_get_property(&inquiry));
assert!(!is_set_property(&inquiry));
assert_eq!(extract_get_resource(&inquiry), Some(7));
assert_eq!(request_id(&inquiry), 0x05);
}
#[test]
fn get_reply_body_extraction() {
let body = b"preset data here";
let reply = build_get_reply(
[0x01, 0x02, 0x03, 0x04],
[0x10, 0x20, 0x30, 0x40],
0x03,
2,
PeStatus::Ok,
body,
);
assert!(is_get_reply(&reply));
let raw = extract_get_body(&reply).unwrap();
let mut decoded = [0u8; 32];
let dec_len = decode_mcoded7(raw, &mut decoded);
assert_eq!(&decoded[..dec_len], body);
}
#[test]
fn mcoded7_roundtrip_short() {
let data = [0x80, 0xFF, 0x00, 0x7F];
let mut encoded = [0u8; 256];
let enc_len = encode_mcoded7(&data, &mut encoded);
let mut decoded = [0u8; 256];
let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
assert_eq!(&decoded[..dec_len], &data);
}
#[test]
fn mcoded7_roundtrip_exact_7() {
let data = [0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86];
let mut encoded = [0u8; 256];
let enc_len = encode_mcoded7(&data, &mut encoded);
assert_eq!(enc_len, 8); let mut decoded = [0u8; 256];
let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
assert_eq!(&decoded[..dec_len], &data);
}
#[test]
fn mcoded7_roundtrip_14_bytes() {
let data = [0xFF; 14];
let mut encoded = [0u8; 256];
let enc_len = encode_mcoded7(&data, &mut encoded);
assert_eq!(enc_len, 16); let mut decoded = [0u8; 256];
let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
assert_eq!(&decoded[..dec_len], &data);
}
#[test]
fn mcoded7_all_7bit_safe_is_passthrough_with_prefix() {
let data = [0x01, 0x7F, 0x00, 0x55];
let mut encoded = [0u8; 256];
let enc_len = encode_mcoded7(&data, &mut encoded);
assert_eq!(encoded[0], 0x00);
assert_eq!(&encoded[1..5], &data);
assert_eq!(enc_len, 5);
}
#[test]
fn mcoded7_custom_rgb_color() {
let data = [0xFF, 0x80, 0x00];
let mut encoded = [0u8; 256];
let enc_len = encode_mcoded7(&data, &mut encoded);
let mut decoded = [0u8; 256];
let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
assert_eq!(&decoded[..dec_len], &data);
}
}