use std::sync::Arc;
use tracing::{error, info, warn};
use crate::config::MorningstarConfig;
use crate::songs::Song;
pub struct Notifier {
config: MorningstarConfig,
device: Arc<dyn super::Device>,
}
impl Notifier {
pub fn new(config: MorningstarConfig, device: Arc<dyn super::Device>) -> Notifier {
Notifier { config, device }
}
}
impl crate::player::SongChangeNotifier for Notifier {
fn notify(&self, song: &Song) {
let sysex = build_update_bank_name(&self.config, song.name());
if let Err(e) = self.device.emit_sysex(&sysex) {
error!("Error emitting Morningstar bank name SysEx: {:?}", e);
}
}
}
const MANUFACTURER_ID: [u8; 3] = [0x00, 0x21, 0x24];
pub fn check_ack(raw_event: &[u8]) -> bool {
if raw_event.len() < 10 {
return false;
}
if raw_event[0] != 0xF0 || raw_event[1..4] != MANUFACTURER_ID {
return false;
}
if raw_event[7] != 0x7F {
return false;
}
let ack_code = raw_event[8];
match ack_code {
0x00 => info!("Morningstar SysEx acknowledged (success)"),
0x01 => warn!("Morningstar SysEx rejected: wrong model ID"),
0x02 => warn!("Morningstar SysEx rejected: wrong checksum"),
0x03 => warn!("Morningstar SysEx rejected: wrong payload size"),
code => warn!(code, "Morningstar SysEx rejected: unknown ack code"),
}
true
}
pub fn build_update_bank_name(config: &MorningstarConfig, name: &str) -> Vec<u8> {
let name_len = config.name_length();
let padded = format!("{:<width$}", name, width = name_len);
let padded: String = padded.chars().take(name_len).collect();
let save_flag: u8 = if config.save() { 0x7F } else { 0x00 };
let mut msg: Vec<u8> = Vec::with_capacity(16 + name_len + 2);
msg.push(0xF0); msg.push(0x00); msg.push(0x21); msg.push(0x24); msg.push(config.model_id()); msg.push(0x00); msg.push(0x70); msg.push(0x10); msg.push(0x00); msg.push(save_flag); msg.push(0x00); msg.push(0x00); msg.push(0x00); msg.push(0x00); msg.push(0x00); msg.push(0x00);
for b in padded.as_bytes() {
msg.push(*b);
}
let checksum = msg.iter().fold(0u8, |acc, &b| acc ^ b) & 0x7F;
msg.push(checksum);
msg.push(0xF7);
msg
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::MorningstarModel;
fn make_config(model: MorningstarModel, save: bool) -> MorningstarConfig {
MorningstarConfig::new(model, save)
}
#[test]
fn basic_message_structure() {
let config = make_config(MorningstarModel::MC6Pro, false);
let msg = build_update_bank_name(&config, "Test");
assert_eq!(msg[0], 0xF0);
assert_eq!(&msg[1..4], &[0x00, 0x21, 0x24]);
assert_eq!(msg[4], 0x06); assert_eq!(msg[6], 0x70); assert_eq!(msg[7], 0x10); assert_eq!(msg[8], 0x00); assert_eq!(msg[9], 0x00); assert_eq!(*msg.last().unwrap(), 0xF7);
}
#[test]
fn save_flag_set() {
let config = make_config(MorningstarModel::MC4Pro, true);
let msg = build_update_bank_name(&config, "X");
assert_eq!(msg[9], 0x7F);
}
#[test]
fn name_bytes_padded_with_spaces() {
let config = make_config(MorningstarModel::MC4Pro, false);
let msg = build_update_bank_name(&config, "ABC");
assert_eq!(msg[16], b'A');
assert_eq!(msg[17], b'B');
assert_eq!(msg[18], b'C');
for (i, &byte) in msg.iter().enumerate().take(48).skip(19) {
assert_eq!(byte, b' ', "byte {} should be space padding", i);
}
assert_eq!(msg.len(), 50);
}
#[test]
fn truncation_at_model_limit() {
let config = make_config(MorningstarModel::MC4Pro, false);
let long_name = "A".repeat(50);
let msg = build_update_bank_name(&config, &long_name);
assert_eq!(msg.len(), 50);
for &byte in msg.iter().take(48).skip(16) {
assert_eq!(byte, b'A');
}
}
#[test]
fn mc3_uses_16_char_name() {
let config = make_config(MorningstarModel::MC3, false);
let msg = build_update_bank_name(&config, "Hi");
assert_eq!(msg.len(), 34);
assert_eq!(msg[16], b'H');
assert_eq!(msg[17], b'i');
for &byte in msg.iter().take(32).skip(18) {
assert_eq!(byte, b' ');
}
}
#[test]
fn mc6_uses_24_char_name() {
let config = make_config(MorningstarModel::MC6, false);
let msg = build_update_bank_name(&config, "Test");
assert_eq!(msg.len(), 42);
}
#[test]
fn checksum_is_correct() {
let config = make_config(MorningstarModel::MC4Pro, false);
let msg = build_update_bank_name(&config, "Hi");
let checksum_idx = msg.len() - 2;
let expected = msg[..checksum_idx].iter().fold(0u8, |acc, &b| acc ^ b) & 0x7F;
assert_eq!(msg[checksum_idx], expected);
}
#[test]
fn checksum_masked_to_7_bits() {
let config = make_config(MorningstarModel::MC4Pro, false);
let msg = build_update_bank_name(&config, "~~~~~");
let checksum_idx = msg.len() - 2;
assert!(msg[checksum_idx] <= 0x7F);
}
#[test]
fn all_model_ids() {
let models = vec![
(MorningstarModel::MC3, 0x05),
(MorningstarModel::MC6, 0x03),
(MorningstarModel::MC8, 0x04),
(MorningstarModel::MC6Pro, 0x06),
(MorningstarModel::MC8Pro, 0x08),
(MorningstarModel::MC4Pro, 0x09),
(
MorningstarModel::Custom(crate::config::CustomModel { model_id: 0x0A }),
0x0A,
),
];
for (model, expected_id) in models {
let config = make_config(model, false);
let msg = build_update_bank_name(&config, "X");
assert_eq!(msg[4], expected_id, "Model ID mismatch");
}
}
#[test]
fn empty_name_padded_to_model_length() {
let config = make_config(MorningstarModel::MC4Pro, false);
let msg = build_update_bank_name(&config, "");
assert_eq!(msg.len(), 50);
for &byte in msg.iter().take(48).skip(16) {
assert_eq!(byte, b' ');
}
}
#[test]
fn exactly_32_chars_not_truncated() {
let config = make_config(MorningstarModel::MC4Pro, false);
let name = "A".repeat(32);
let msg = build_update_bank_name(&config, &name);
assert_eq!(msg.len(), 50);
for &byte in msg.iter().take(48).skip(16) {
assert_eq!(byte, b'A');
}
}
#[test]
fn config_deserialization() {
use config::Config;
use config::File;
use config::FileFormat;
let yaml = r#"
model: mc4pro
save: true
"#;
let config: MorningstarConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(config.model_id(), 0x09);
assert!(config.save());
}
#[test]
fn config_deserialization_defaults() {
use config::Config;
use config::File;
use config::FileFormat;
let yaml = r#"
model: mc6pro
"#;
let config: MorningstarConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(config.model_id(), 0x06);
assert!(!config.save());
}
#[test]
fn config_deserialization_custom_model() {
use config::Config;
use config::File;
use config::FileFormat;
let yaml = r#"
model:
custom:
model_id: 15
"#;
let config: MorningstarConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(config.model_id(), 15);
}
mod check_ack_tests {
use super::*;
fn make_ack(ack_code: u8) -> Vec<u8> {
vec![
0xF0, 0x00, 0x21, 0x24, 0x09, 0x00, 0x70, 0x7F, ack_code, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xF7,
]
}
#[test]
fn recognizes_success_ack() {
assert!(check_ack(&make_ack(0x00)));
}
#[test]
fn recognizes_wrong_model_id() {
assert!(check_ack(&make_ack(0x01)));
}
#[test]
fn recognizes_wrong_checksum() {
assert!(check_ack(&make_ack(0x02)));
}
#[test]
fn recognizes_wrong_payload_size() {
assert!(check_ack(&make_ack(0x03)));
}
#[test]
fn ignores_non_sysex() {
assert!(!check_ack(&[0x90, 0x3C, 0x64]));
}
#[test]
fn ignores_wrong_manufacturer() {
assert!(!check_ack(&[
0xF0, 0x00, 0x00, 0x00, 0x09, 0x00, 0x70, 0x7F, 0x00, 0xF7,
]));
}
#[test]
fn ignores_non_ack_sysex() {
assert!(!check_ack(&[
0xF0, 0x00, 0x21, 0x24, 0x09, 0x00, 0x70, 0x10, 0x00, 0xF7,
]));
}
#[test]
fn ignores_too_short() {
assert!(!check_ack(&[0xF0, 0x00, 0x21, 0x24]));
}
}
}