1use heapless::Vec;
21
22const UNIVERSAL_SYSEX: u8 = 0x7E;
23const SUB_ID1_MIDI_CI: u8 = 0x0D;
24
25pub const PE_GET_INQUIRY: u8 = 0x34;
27pub const PE_GET_REPLY: u8 = 0x35;
29pub const PE_SET_INQUIRY: u8 = 0x36;
31pub const PE_SET_REPLY: u8 = 0x37;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[repr(u8)]
37pub enum PeStatus {
38 Ok = 0x00,
40 NotFound = 0x01,
42 FormatError = 0x02,
44 VersionMismatch = 0x03,
46}
47
48impl PeStatus {
49 pub fn from_byte(b: u8) -> Option<Self> {
50 match b {
51 0x00 => Some(Self::Ok),
52 0x01 => Some(Self::NotFound),
53 0x02 => Some(Self::FormatError),
54 0x03 => Some(Self::VersionMismatch),
55 _ => None,
56 }
57 }
58
59 pub fn is_ok(self) -> bool {
60 self == Self::Ok
61 }
62}
63
64const CI_VERSION: u8 = 0x02;
65
66pub fn is_ci_message(buf: &[u8]) -> bool {
68 buf.len() >= 15 && buf[0] == 0xF0 && buf[1] == UNIVERSAL_SYSEX && buf[3] == SUB_ID1_MIDI_CI
69}
70
71pub fn is_set_property(buf: &[u8]) -> bool {
73 is_ci_message(buf) && buf[4] == PE_SET_INQUIRY
74}
75
76pub fn request_id(buf: &[u8]) -> u8 {
78 buf[14]
79}
80
81pub fn source_muid(buf: &[u8]) -> [u8; 4] {
83 [buf[6], buf[7], buf[8], buf[9]]
84}
85
86pub struct SetPropertyData<'a> {
88 pub resource: u8,
90 pub body: &'a [u8],
92}
93
94pub fn extract_set_property(buf: &[u8]) -> Option<SetPropertyData<'_>> {
96 if !is_set_property(buf) {
97 return None;
98 }
99 if buf.len() < 16 {
100 return None;
101 }
102
103 let mut pos = 15;
104
105 if pos + 2 > buf.len() {
107 return None;
108 }
109 let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
110 pos += 2;
111
112 let resource = if header_len > 0 && pos < buf.len() {
114 buf[pos]
115 } else {
116 0
117 };
118 pos += header_len;
119
120 if pos + 4 > buf.len() {
122 return None;
123 }
124 pos += 4;
125
126 if pos + 2 > buf.len() {
128 return None;
129 }
130 let body_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
131 pos += 2;
132
133 if pos + body_len > buf.len() {
134 return None;
135 }
136 Some(SetPropertyData {
137 resource,
138 body: &buf[pos..pos + body_len],
139 })
140}
141
142pub fn extract_body(buf: &[u8]) -> Option<&[u8]> {
144 extract_set_property(buf).map(|d| d.body)
145}
146
147pub fn build_set_reply(
149 device_muid: [u8; 4],
150 dest_muid: [u8; 4],
151 req_id: u8,
152 status: PeStatus,
153) -> Vec<u8, 32> {
154 let mut msg: Vec<u8, 32> = Vec::new();
155 let _ = msg.push(0xF0);
156 let _ = msg.push(UNIVERSAL_SYSEX);
157 let _ = msg.push(0x7F); let _ = msg.push(SUB_ID1_MIDI_CI);
159 let _ = msg.push(PE_SET_REPLY);
160 let _ = msg.push(CI_VERSION);
161 for &b in &device_muid {
162 let _ = msg.push(b);
163 }
164 for &b in &dest_muid {
165 let _ = msg.push(b);
166 }
167 let _ = msg.push(req_id);
168 let _ = msg.push(0x01);
170 let _ = msg.push(0x00);
171 let _ = msg.push(status as u8);
172 let _ = msg.push(0x01);
174 let _ = msg.push(0x00);
175 let _ = msg.push(0x01);
176 let _ = msg.push(0x00);
177 let _ = msg.push(0x00);
179 let _ = msg.push(0x00);
180 let _ = msg.push(0xF7);
181 msg
182}
183
184pub fn build_set_inquiry(
188 source_muid: [u8; 4],
189 dest_muid: [u8; 4],
190 req_id: u8,
191 resource: u8,
192 body: &[u8],
193) -> Vec<u8, 350> {
194 let mut msg: Vec<u8, 350> = Vec::new();
195 let _ = msg.push(0xF0);
196 let _ = msg.push(UNIVERSAL_SYSEX);
197 let _ = msg.push(0x7F);
198 let _ = msg.push(SUB_ID1_MIDI_CI);
199 let _ = msg.push(PE_SET_INQUIRY);
200 let _ = msg.push(CI_VERSION);
201 for &b in &source_muid {
202 let _ = msg.push(b);
203 }
204 for &b in &dest_muid {
205 let _ = msg.push(b);
206 }
207 let _ = msg.push(req_id);
208 let _ = msg.push(0x01);
210 let _ = msg.push(0x00);
211 let _ = msg.push(resource);
212 let _ = msg.push(0x01);
214 let _ = msg.push(0x00);
215 let _ = msg.push(0x01);
216 let _ = msg.push(0x00);
217 let mut encoded_body = [0u8; 300];
219 let enc_len = encode_mcoded7(body, &mut encoded_body);
220 let _ = msg.push((enc_len & 0x7F) as u8);
221 let _ = msg.push(((enc_len >> 7) & 0x7F) as u8);
222 for &b in &encoded_body[..enc_len] {
223 let _ = msg.push(b);
224 }
225 let _ = msg.push(0xF7);
226 msg
227}
228
229pub fn is_get_property(buf: &[u8]) -> bool {
231 is_ci_message(buf) && buf[4] == PE_GET_INQUIRY
232}
233
234pub fn is_get_reply(buf: &[u8]) -> bool {
236 is_ci_message(buf) && buf[4] == PE_GET_REPLY
237}
238
239pub fn extract_reply_status(buf: &[u8]) -> Option<PeStatus> {
242 if buf.len() < 16 || !is_ci_message(buf) {
243 return None;
244 }
245 let sub_id2 = buf[4];
246 if sub_id2 != PE_SET_REPLY && sub_id2 != PE_GET_REPLY {
247 return None;
248 }
249 let pos = 15;
250 if pos + 2 > buf.len() {
251 return None;
252 }
253 let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
254 if header_len == 0 {
255 return Some(PeStatus::Ok);
257 }
258 if pos + 2 + 1 > buf.len() {
259 return None;
260 }
261 PeStatus::from_byte(buf[pos + 2])
262}
263
264pub fn extract_get_body(buf: &[u8]) -> Option<&[u8]> {
266 if !is_get_reply(buf) || buf.len() < 16 {
267 return None;
268 }
269 let mut pos = 15;
270 if pos + 2 > buf.len() {
271 return None;
272 }
273 let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
274 pos += 2 + header_len;
275 if pos + 4 > buf.len() {
277 return None;
278 }
279 pos += 4;
280 if pos + 2 > buf.len() {
282 return None;
283 }
284 let body_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
285 pos += 2;
286 if pos + body_len > buf.len() {
287 return None;
288 }
289 Some(&buf[pos..pos + body_len])
290}
291
292pub fn extract_get_resource(buf: &[u8]) -> Option<u8> {
294 if !is_get_property(buf) || buf.len() < 16 {
295 return None;
296 }
297 let pos = 15;
298 if pos + 2 > buf.len() {
299 return None;
300 }
301 let header_len = (buf[pos] as usize) | ((buf[pos + 1] as usize) << 7);
302 if header_len > 0 && pos + 2 < buf.len() {
303 Some(buf[pos + 2])
304 } else {
305 Some(0)
306 }
307}
308
309pub fn build_get_inquiry(
311 source_muid: [u8; 4],
312 dest_muid: [u8; 4],
313 req_id: u8,
314 resource: u8,
315) -> Vec<u8, 32> {
316 let mut msg: Vec<u8, 32> = Vec::new();
317 let _ = msg.push(0xF0);
318 let _ = msg.push(UNIVERSAL_SYSEX);
319 let _ = msg.push(0x7F);
320 let _ = msg.push(SUB_ID1_MIDI_CI);
321 let _ = msg.push(PE_GET_INQUIRY);
322 let _ = msg.push(CI_VERSION);
323 for &b in &source_muid {
324 let _ = msg.push(b);
325 }
326 for &b in &dest_muid {
327 let _ = msg.push(b);
328 }
329 let _ = msg.push(req_id);
330 let _ = msg.push(0x01);
332 let _ = msg.push(0x00);
333 let _ = msg.push(resource);
334 let _ = msg.push(0x01);
336 let _ = msg.push(0x00);
337 let _ = msg.push(0x01);
338 let _ = msg.push(0x00);
339 let _ = msg.push(0x00);
341 let _ = msg.push(0x00);
342 let _ = msg.push(0xF7);
343 msg
344}
345
346pub fn build_get_reply(
348 device_muid: [u8; 4],
349 dest_muid: [u8; 4],
350 req_id: u8,
351 resource: u8,
352 status: PeStatus,
353 body: &[u8],
354) -> Vec<u8, 350> {
355 let mut msg: Vec<u8, 350> = Vec::new();
356 let _ = msg.push(0xF0);
357 let _ = msg.push(UNIVERSAL_SYSEX);
358 let _ = msg.push(0x7F);
359 let _ = msg.push(SUB_ID1_MIDI_CI);
360 let _ = msg.push(PE_GET_REPLY);
361 let _ = msg.push(CI_VERSION);
362 for &b in &device_muid {
363 let _ = msg.push(b);
364 }
365 for &b in &dest_muid {
366 let _ = msg.push(b);
367 }
368 let _ = msg.push(req_id);
369 let _ = msg.push(0x02);
371 let _ = msg.push(0x00);
372 let _ = msg.push(status as u8);
373 let _ = msg.push(resource);
374 let _ = msg.push(0x01);
376 let _ = msg.push(0x00);
377 let _ = msg.push(0x01);
378 let _ = msg.push(0x00);
379 let mut encoded_body = [0u8; 300];
381 let enc_len = encode_mcoded7(body, &mut encoded_body);
382 let _ = msg.push((enc_len & 0x7F) as u8);
383 let _ = msg.push(((enc_len >> 7) & 0x7F) as u8);
384 for &b in &encoded_body[..enc_len] {
385 let _ = msg.push(b);
386 }
387 let _ = msg.push(0xF7);
388 msg
389}
390
391pub fn encode_mcoded7(data: &[u8], out: &mut [u8]) -> usize {
395 let mut pos = 0;
396 for chunk in data.chunks(7) {
397 let mut high_bits: u8 = 0;
398 for (i, &b) in chunk.iter().enumerate() {
399 if b & 0x80 != 0 {
400 high_bits |= 1 << i;
401 }
402 }
403 out[pos] = high_bits;
404 pos += 1;
405 for &b in chunk {
406 out[pos] = b & 0x7F;
407 pos += 1;
408 }
409 }
410 pos
411}
412
413pub fn decode_mcoded7(data: &[u8], out: &mut [u8]) -> usize {
416 let mut pos = 0;
417 let mut i = 0;
418 while i < data.len() {
419 let high_bits = data[i];
420 i += 1;
421 for bit in 0..7 {
422 if i >= data.len() {
423 break;
424 }
425 out[pos] = data[i] | (if high_bits & (1 << bit) != 0 { 0x80 } else { 0 });
426 pos += 1;
427 i += 1;
428 }
429 }
430 pos
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn roundtrip_set_property() {
439 let payload = b"hello";
440 let msg = build_set_inquiry(
441 [0x10, 0x20, 0x30, 0x40],
442 [0x01, 0x02, 0x03, 0x04],
443 0x07,
444 3,
445 payload,
446 );
447
448 assert!(is_ci_message(&msg));
449 assert!(is_set_property(&msg));
450 assert_eq!(request_id(&msg), 0x07);
451 assert_eq!(source_muid(&msg), [0x10, 0x20, 0x30, 0x40]);
452
453 let data = extract_set_property(&msg).unwrap();
454 assert_eq!(data.resource, 3);
455 let mut decoded = [0u8; 32];
456 let dec_len = decode_mcoded7(data.body, &mut decoded);
457 assert_eq!(&decoded[..dec_len], b"hello");
458 }
459
460 #[test]
461 fn not_ci_for_opendeck() {
462 let buf = [0xF0, 0x00, 0x53, 0x43, 0x00, 0x00, 0x01, 0xF7];
463 assert!(!is_ci_message(&buf));
464 }
465
466 #[test]
467 fn reply_structure() {
468 let reply = build_set_reply(
469 [0x01, 0x02, 0x03, 0x04],
470 [0x10, 0x20, 0x30, 0x40],
471 0x07,
472 PeStatus::Ok,
473 );
474 assert_eq!(reply[0], 0xF0);
475 assert_eq!(reply[4], PE_SET_REPLY);
476 assert_eq!(reply[14], 0x07);
477 assert_eq!(reply[15], 0x01); assert_eq!(reply[17], 0x00); assert_eq!(*reply.last().unwrap(), 0xF7);
481 assert_eq!(extract_reply_status(&reply), Some(PeStatus::Ok));
482 }
483
484 #[test]
485 fn reply_status_not_found() {
486 let reply = build_set_reply(
487 [0x01, 0x02, 0x03, 0x04],
488 [0x10, 0x20, 0x30, 0x40],
489 0x01,
490 PeStatus::NotFound,
491 );
492 assert_eq!(extract_reply_status(&reply), Some(PeStatus::NotFound));
493 }
494
495 #[test]
496 fn get_reply_status_extracted() {
497 let reply = build_get_reply(
498 [0x01, 0x02, 0x03, 0x04],
499 [0x10, 0x20, 0x30, 0x40],
500 0x01,
501 5,
502 PeStatus::NotFound,
503 &[],
504 );
505 assert_eq!(extract_reply_status(&reply), Some(PeStatus::NotFound));
506 }
507
508 #[test]
509 fn extract_body_with_header() {
510 let mut msg: Vec<u8, 128> = Vec::new();
511 let _ = msg.push(0xF0);
512 let _ = msg.push(UNIVERSAL_SYSEX);
513 let _ = msg.push(0x7F);
514 let _ = msg.push(SUB_ID1_MIDI_CI);
515 let _ = msg.push(PE_SET_INQUIRY);
516 let _ = msg.push(CI_VERSION);
517 for &b in &[0x10, 0x20, 0x30, 0x40, 0x01, 0x02, 0x03, 0x04] {
518 let _ = msg.push(b);
519 }
520 let _ = msg.push(0x01); let _ = msg.push(0x03);
523 let _ = msg.push(0x00);
524 for &b in b"abc" {
525 let _ = msg.push(b);
526 }
527 let _ = msg.push(0x01);
529 let _ = msg.push(0x00);
530 let _ = msg.push(0x01);
531 let _ = msg.push(0x00);
532 let _ = msg.push(0x04);
534 let _ = msg.push(0x00);
535 for &b in b"data" {
536 let _ = msg.push(b);
537 }
538 let _ = msg.push(0xF7);
539
540 assert_eq!(extract_body(&msg).unwrap(), b"data");
541 }
542
543 #[test]
544 fn get_property_roundtrip() {
545 let inquiry =
546 build_get_inquiry([0x10, 0x20, 0x30, 0x40], [0x01, 0x02, 0x03, 0x04], 0x05, 7);
547 assert!(is_get_property(&inquiry));
548 assert!(!is_set_property(&inquiry));
549 assert_eq!(extract_get_resource(&inquiry), Some(7));
550 assert_eq!(request_id(&inquiry), 0x05);
551 }
552
553 #[test]
554 fn get_reply_body_extraction() {
555 let body = b"preset data here";
556 let reply = build_get_reply(
557 [0x01, 0x02, 0x03, 0x04],
558 [0x10, 0x20, 0x30, 0x40],
559 0x03,
560 2,
561 PeStatus::Ok,
562 body,
563 );
564 assert!(is_get_reply(&reply));
565 let raw = extract_get_body(&reply).unwrap();
566 let mut decoded = [0u8; 32];
567 let dec_len = decode_mcoded7(raw, &mut decoded);
568 assert_eq!(&decoded[..dec_len], body);
569 }
570
571 #[test]
572 fn mcoded7_roundtrip_short() {
573 let data = [0x80, 0xFF, 0x00, 0x7F];
574 let mut encoded = [0u8; 256];
575 let enc_len = encode_mcoded7(&data, &mut encoded);
576 let mut decoded = [0u8; 256];
577 let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
578 assert_eq!(&decoded[..dec_len], &data);
579 }
580
581 #[test]
582 fn mcoded7_roundtrip_exact_7() {
583 let data = [0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86];
584 let mut encoded = [0u8; 256];
585 let enc_len = encode_mcoded7(&data, &mut encoded);
586 assert_eq!(enc_len, 8); let mut decoded = [0u8; 256];
588 let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
589 assert_eq!(&decoded[..dec_len], &data);
590 }
591
592 #[test]
593 fn mcoded7_roundtrip_14_bytes() {
594 let data = [0xFF; 14];
595 let mut encoded = [0u8; 256];
596 let enc_len = encode_mcoded7(&data, &mut encoded);
597 assert_eq!(enc_len, 16); let mut decoded = [0u8; 256];
599 let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
600 assert_eq!(&decoded[..dec_len], &data);
601 }
602
603 #[test]
604 fn mcoded7_all_7bit_safe_is_passthrough_with_prefix() {
605 let data = [0x01, 0x7F, 0x00, 0x55];
606 let mut encoded = [0u8; 256];
607 let enc_len = encode_mcoded7(&data, &mut encoded);
608 assert_eq!(encoded[0], 0x00);
610 assert_eq!(&encoded[1..5], &data);
611 assert_eq!(enc_len, 5);
612 }
613
614 #[test]
615 fn mcoded7_custom_rgb_color() {
616 let data = [0xFF, 0x80, 0x00];
618 let mut encoded = [0u8; 256];
619 let enc_len = encode_mcoded7(&data, &mut encoded);
620 let mut decoded = [0u8; 256];
621 let dec_len = decode_mcoded7(&encoded[..enc_len], &mut decoded);
622 assert_eq!(&decoded[..dec_len], &data);
623 }
624}