diff --git a/Cargo.lock b/Cargo.lock index 6486ef0..7ba0bb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -481,7 +487,7 @@ dependencies = [ "dirs", "futures", "hound", - "jack", + "jack 0.13.3", "kanal", "log", "osc", @@ -650,6 +656,21 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.27" @@ -810,6 +831,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -893,6 +927,31 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1119,6 +1178,12 @@ dependencies = [ "winit", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.29.1" @@ -1615,6 +1680,8 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1816,12 +1883,43 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jack" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" +dependencies = [ + "bitflags 1.3.2", + "jack-sys", + "lazy_static", + "libc", + "log", +] + [[package]] name = "jack" version = "0.13.3" @@ -2001,6 +2099,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2010,6 +2117,18 @@ dependencies = [ "libc", ] +[[package]] +name = "mapper" +version = "0.1.0" +dependencies = [ + "crossterm", + "jack 0.11.4", + "kanal", + "ratatui", + "serde", + "serde_json", +] + [[package]] name = "memchr" version = "2.7.5" @@ -2065,6 +2184,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.4" @@ -2542,7 +2673,7 @@ version = "0.1.0" dependencies = [ "log", "rosc", - "strum", + "strum 0.23.0", "thiserror", ] @@ -2791,6 +2922,26 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2992,6 +3143,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.5" @@ -3023,7 +3195,7 @@ dependencies = [ name = "simulator" version = "0.1.0" dependencies = [ - "jack", + "jack 0.13.3", "kanal", "wmidi", ] @@ -3113,6 +3285,16 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3143,7 +3325,16 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" dependencies = [ - "strum_macros", + "strum_macros 0.23.1", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", ] [[package]] @@ -3159,6 +3350,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", +] + [[package]] name = "syn" version = "1.0.109" @@ -3311,7 +3515,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3436,6 +3640,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.14" diff --git a/Cargo.toml b/Cargo.toml index 66b81e8..276a202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "3" members = [ "audio_engine", - "gui", + "gui", "mapper", "osc", "simulator", "xtask", @@ -23,4 +23,4 @@ rosc = "0.10" thiserror = "1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } -wmidi = "4" \ No newline at end of file +wmidi = "4" diff --git a/firmware/.cargo/config.toml b/firmware/.cargo/config.toml index f59a45d..8b326d2 100644 --- a/firmware/.cargo/config.toml +++ b/firmware/.cargo/config.toml @@ -5,6 +5,7 @@ target = "thumbv6m-none-eabi" runner = "probe-rs run --chip STM32F042C4Tx" rustflags = [ "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", "-C", "link-arg=--nmagic", ] diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 701cf53..655fd08 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -88,15 +88,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "defmt" -version = "0.3.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" -dependencies = [ - "defmt 1.0.1", -] - [[package]] name = "defmt" version = "1.0.1" @@ -131,12 +122,12 @@ dependencies = [ [[package]] name = "defmt-rtt" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6eca0aae8aa2cf8333200ecbd236274697bc0a394765c858b3d9372eb1abcfa" +checksum = "b2cac3b8a5644a9e02b75085ebad3b6deafdbdbdec04bb25086523828aa4dfd1" dependencies = [ "critical-section", - "defmt 0.3.100", + "defmt", ] [[package]] @@ -149,6 +140,23 @@ dependencies = [ "void", ] +[[package]] +name = "embedded-midi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26119337828c4c6a29d9c1661962eeec2a3f884b859d5713a70c6c4143988497" +dependencies = [ + "embedded-hal", + "midi-types", + "nb 1.1.0", +] + +[[package]] +name = "midi-types" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0bbe5256e5c434947d790788426bb65773502784aed7b23408f7e7fb4d8eb5" + [[package]] name = "nb" version = "0.1.3" @@ -268,9 +276,10 @@ dependencies = [ "cortex-m", "cortex-m-rt", "critical-section", - "defmt 0.3.100", + "defmt", "defmt-rtt", "embedded-hal", + "embedded-midi", "nb 1.1.0", "panic-halt", "stm32f0xx-hal", diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml old mode 100644 new mode 100755 index 1bf7026..ab5c3de --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -8,13 +8,18 @@ cortex-m = { version = "0.7", features = ["inline-asm", "critical-section-single cortex-m-rt = "0.7" critical-section = "1" embedded-hal = "0.2" +embedded-midi = "0.1.2" nb = "1" panic-halt = "0.2" stm32f0xx-hal = { version = "0.18", features = ["stm32f042", "rt"] } # For debugging and logging -defmt = "0.3" -defmt-rtt = "0.4" +defmt = "1" +defmt-rtt = "1" + +# Add defmt feature to make sure metadata is included +[package.metadata.defmt] +linker_args = ["-C", "link-arg=-Tdefmt.x"] [profile.release] debug = true # Keep debug info for better debugging diff --git a/firmware/build.rs b/firmware/build.rs index d0c1913..9850ab9 100644 --- a/firmware/build.rs +++ b/firmware/build.rs @@ -13,4 +13,4 @@ fn main() { println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rustc-link-arg=--nmagic"); -} \ No newline at end of file +} diff --git a/firmware/src/main.rs b/firmware/src/main.rs old mode 100644 new mode 100755 index 39fcfbb..cb240cd --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -1,28 +1,48 @@ #![no_std] #![no_main] -use panic_halt as _; use cortex_m_rt::entry; -use stm32f0xx_hal::serial::Error; -use stm32f0xx_hal as hal; use hal::prelude::*; use hal::stm32; -use hal::{ - delay::Delay, - serial::Serial, -}; +use panic_halt as _; +use stm32f0xx_hal as hal; + +use defmt::info; +use defmt_rtt as _; + +use embedded_midi::Channel; +use hal::{delay::Delay, serial::Serial}; + +defmt::timestamp!("{=u32}", { + static mut COUNTER: u32 = 0; + unsafe { + COUNTER = COUNTER.wrapping_add(1); + COUNTER + } +}); struct Bus { - latch_en: stm32f0xx_hal::gpio::gpioa::PA0>, - select_en: stm32f0xx_hal::gpio::gpiob::PB8>, - pb0: stm32f0xx_hal::gpio::gpiob::PB0>, - pb1: stm32f0xx_hal::gpio::gpiob::PB1>, - pb2: stm32f0xx_hal::gpio::gpiob::PB2>, - pb3: stm32f0xx_hal::gpio::gpiob::PB3>, - pb4: stm32f0xx_hal::gpio::gpiob::PB4>, - pb5: stm32f0xx_hal::gpio::gpiob::PB5>, - pb6: stm32f0xx_hal::gpio::gpiob::PB6>, - pb7: stm32f0xx_hal::gpio::gpiob::PB7>, + latch_en: + stm32f0xx_hal::gpio::gpioa::PA0>, + select_en: stm32f0xx_hal::gpio::gpiob::PB8< + stm32f0xx_hal::gpio::Output, + >, + pb0: + stm32f0xx_hal::gpio::gpiob::PB0>, + pb1: + stm32f0xx_hal::gpio::gpiob::PB1>, + pb2: + stm32f0xx_hal::gpio::gpiob::PB2>, + pb3: + stm32f0xx_hal::gpio::gpiob::PB3>, + pb4: + stm32f0xx_hal::gpio::gpiob::PB4>, + pb5: + stm32f0xx_hal::gpio::gpiob::PB5>, + pb6: + stm32f0xx_hal::gpio::gpiob::PB6>, + pb7: + stm32f0xx_hal::gpio::gpiob::PB7>, } #[entry] @@ -30,9 +50,11 @@ fn main() -> ! { // Get device peripherals let mut dp = stm32::Peripherals::take().unwrap(); let cp = cortex_m::Peripherals::take().unwrap(); - + // Configure the clock to 48 MHz - let mut rcc = dp.RCC.configure() + let mut rcc = dp + .RCC + .configure() .hsi48() .enable_crs(dp.CRS) .sysclk(48.mhz()) @@ -41,56 +63,105 @@ fn main() -> ! { // Set up delay provider let mut delay = Delay::new(cp.SYST, &rcc); - + // Configure GPIO let gpioa = dp.GPIOA.split(&mut rcc); let gpiob = dp.GPIOB.split(&mut rcc); - let mut bus = cortex_m::interrupt::free(|cs| { - Bus { - latch_en: gpioa.pa0.into_push_pull_output(cs), - select_en: gpiob.pb8.into_open_drain_output(cs), - pb0: gpiob.pb0.into_push_pull_output(cs), - pb1: gpiob.pb1.into_push_pull_output(cs), - pb2: gpiob.pb2.into_push_pull_output(cs), - pb3: gpiob.pb3.into_push_pull_output(cs), - pb4: gpiob.pb4.into_push_pull_output(cs), - pb5: gpiob.pb5.into_push_pull_output(cs), - pb6: gpiob.pb6.into_push_pull_output(cs), - pb7: gpiob.pb7.into_push_pull_output(cs), - } + let button_1_5 = cortex_m::interrupt::free(|cs| gpiob.pb11.into_floating_input(cs)); + + let button_6_10 = cortex_m::interrupt::free(|cs| gpiob.pb13.into_floating_input(cs)); + + let mut bus = cortex_m::interrupt::free(|cs| Bus { + latch_en: gpioa.pa0.into_push_pull_output(cs), + select_en: gpiob.pb8.into_open_drain_output(cs), + pb0: gpiob.pb0.into_push_pull_output(cs), + pb1: gpiob.pb1.into_push_pull_output(cs), + pb2: gpiob.pb2.into_push_pull_output(cs), + pb3: gpiob.pb3.into_push_pull_output(cs), + pb4: gpiob.pb4.into_push_pull_output(cs), + pb5: gpiob.pb5.into_push_pull_output(cs), + pb6: gpiob.pb6.into_push_pull_output(cs), + pb7: gpiob.pb7.into_push_pull_output(cs), }); - let mut serial = cortex_m::interrupt::free(|cs| { + // Configure serial port + let serial = cortex_m::interrupt::free(|cs| { let tx = gpioa.pa9.into_alternate_af1(cs); let rx = gpioa.pa10.into_alternate_af1(cs); - //dp.USART1.cr1.write(|w| w.uesm().enabled()); Serial::usart1(dp.USART1, (tx, rx), 31_250.bps(), &mut rcc) }); - // Loop + let (_tx, rx) = serial.split(); + let mut midi = embedded_midi::MidiIn::new(rx); + + // Main loop + let mut ic_03: u8 = 0; + let mut ic_10: u8 = 0; + let mut ic_11: u8 = 0; + loop { - use stm32f0xx_hal::serial::Error; - //type Error = nb::Error; - //use embedded_hal::serial::nb::Error; - match serial.read() { - Ok(_) => { + let message = midi.read(); + match message { + Ok(embedded_midi::MidiMessage::ControlChange(channel, control, value)) + if channel == Channel::new(0) => + { bus.led_button_1(&mut delay); + if control == embedded_midi::Control::new(20) { + bus.led_button_2(&mut delay); + ic_03 = value.into(); + } else if control == embedded_midi::Control::new(21) { + bus.led_button_2(&mut delay); + let value: u8 = value.into(); + ic_03 = 128_u8 + value; + } else if control == embedded_midi::Control::new(22) { + bus.led_button_2(&mut delay); + ic_10 = value.into(); + } else if control == embedded_midi::Control::new(23) { + bus.led_button_2(&mut delay); + let value: u8 = value.into(); + ic_10 = 128_u8 + value; + } else if control == embedded_midi::Control::new(24) { + bus.led_button_2(&mut delay); + ic_11 = value.into(); + } else if control == embedded_midi::Control::new(25) { + bus.led_button_2(&mut delay); + let value: u8 = value.into(); + ic_11 = 128_u8 + value; + } + bus.output(03, ic_03, &mut delay); + bus.output(10, ic_10, &mut delay); + bus.output(11, ic_11, &mut delay); } - Err(nb::Error::WouldBlock) => { - bus.led_button_3(&mut delay); + Ok(_) => { + info!("unhandled midi message"); } - Err(nb::Error::Other(Error::Framing)) => { - bus.led_button_2(&mut delay); + Err(nb::Error::WouldBlock) => {} + Err(nb::Error::Other(hal::serial::Error::Framing)) => { + info!("Error::Framing"); } - Err(nb::Error::Other(Error::Noise)) => { - bus.led_button_9(&mut delay); + Err(nb::Error::Other(hal::serial::Error::Noise)) => { + info!("Error::Noise"); + } + Err(nb::Error::Other(hal::serial::Error::Overrun)) => { + info!("Error::Overrun"); + } + Err(nb::Error::Other(hal::serial::Error::Parity)) => { + info!("Error::Parity"); } Err(nb::Error::Other(_)) => { - bus.led_button_10(&mut delay); + info!("Error::Other"); } } - delay.delay_ms(1000u16); + if button_1_5.is_high().unwrap_or(false) { + info!("Button 1-5 pressed"); + delay.delay_ms(500u16); + } + + if button_6_10.is_high().unwrap_or(false) { + info!("Button 6-10 pressed"); + delay.delay_ms(500u16); + } } } @@ -103,11 +174,11 @@ impl Bus { _ => self.select(2, delay), } - delay.delay_ms(10u16); + delay.delay_us(1u16); self.select_en(true); - delay.delay_ms(10u16); + delay.delay_us(1u16); self.write(data); - delay.delay_ms(10u16); + delay.delay_us(1u16); self.select_en(false); } @@ -128,7 +199,7 @@ impl Bus { fn select(&mut self, line: u8, delay: &mut Delay) { self.select_en.set_low().ok(); - delay.delay_ms(10u16); + delay.delay_us(1u16); match line { 0 => { self.pb0.set_low().ok(); @@ -212,11 +283,11 @@ impl Bus { } _ => {} } - delay.delay_ms(10u16); + delay.delay_us(1u16); self.latch_en.set_high().ok(); - delay.delay_ms(10u16); + delay.delay_us(1u16); self.latch_en.set_low().ok(); - delay.delay_ms(10u16); + delay.delay_us(1u16); self.select_en.set_high().ok(); } @@ -249,4 +320,4 @@ impl Bus { self.output(10, 0b0001_0000, delay); self.output(11, 0b0000_0000, delay); } -} \ No newline at end of file +} diff --git a/mapper/Cargo.toml b/mapper/Cargo.toml new file mode 100644 index 0000000..285a9d0 --- /dev/null +++ b/mapper/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mapper" +version = "0.1.0" +edition = "2024" + +[dependencies] +jack = "0.11" +kanal = "0.1" +ratatui = "0.26" +crossterm = "0.27" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/mapper/format.txt b/mapper/format.txt new file mode 100644 index 0000000..48c0863 --- /dev/null +++ b/mapper/format.txt @@ -0,0 +1,26 @@ +switch 1 +switch 2 + +switches +select +number +value 1 +value 2 + +direct select +midi function +midi chan +config + +expression pedal a +expression pedal b + +display 1 + +display 2 + +display 3 + +button leds + +button presses \ No newline at end of file diff --git a/mapper/mapping.json b/mapper/mapping.json new file mode 100644 index 0000000..2e9cc6b --- /dev/null +++ b/mapper/mapping.json @@ -0,0 +1,169 @@ +{ + "notes": [ + { + "description": "00 00001 0010 00 [G] [G] [G] [2] []", + "register_values": { + "IC03": 253, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00000 1100 00 [P] [P] [P] [3] []", + "register_values": { + "IC03": 254, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00010 0000 01 [] [F] [F] [1] []", + "register_values": { + "IC03": 251, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00100 0000 10 [] [E] [E] [10] []", + "register_values": { + "IC03": 247, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "10 01000 0000 00 [] [D] [D] [9] []", + "register_values": { + "IC03": 239, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00000 1100 00 [P] [P] [P] [3] []", + "register_values": { + "IC03": 222, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00001 0010 00 [G] [G] [G] [2] []", + "register_values": { + "IC03": 221, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00010 0000 01 [] [F] [F] [1] []", + "register_values": { + "IC03": 219, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00100 0000 10 [] [E] [E] [10] []", + "register_values": { + "IC03": 215, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "10 01000 0000 00 [] [D] [D] [9] []", + "register_values": { + "IC03": 207, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00000 1100 00 [P] [P] [P] [3] []", + "register_values": { + "IC03": 190, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00001 0010 00 [G] [G] [G] [2] []", + "register_values": { + "IC03": 189, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00010 0000 01 [] [F] [F] [1] []", + "register_values": { + "IC03": 187, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "00 00100 0000 10 [] [E] [E] [10] []", + "register_values": { + "IC03": 183, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "10 01000 0000 00 [] [D] [D] [9] []", + "register_values": { + "IC03": 175, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "01 10000 1101 00 [B, C, E, F, G, P] [A, B, C, P] [A, B, C, P] [3, 4, 5, 6, 7, 8] []", + "register_values": { + "IC03": 158, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "01 10001 0011 00 [B, C, E, F, G] [A, B, C, G] [A, B, C, G] [2, 4, 5, 6, 7, 8] []", + "register_values": { + "IC03": 157, + "IC10": 255, + "IC11": 0 + } + }, + { + "description": "01 10001 0010 00 [B, C, E, F, G] [] [A, B, V, G] [2, 4, 5, 6, 7, 8] []", + "register_values": { + "IC03": 157, + "IC10": 253, + "IC11": 0 + } + }, + { + "description": "00 00000 0000 00 [] [] [] [] [1, 6]", + "register_values": { + "IC03": 255, + "IC10": 255, + "IC11": 4 + } + }, + { + "description": "00 00000 0000 00 [] [] [] [] [2, 7]", + "register_values": { + "IC03": 255, + "IC10": 255, + "IC11": 8 + } + } + ], + "register_values": { + "IC03": 255, + "IC10": 255, + "IC11": 8 + } +} \ No newline at end of file diff --git a/mapper/src/main.rs b/mapper/src/main.rs new file mode 100644 index 0000000..49eb20f --- /dev/null +++ b/mapper/src/main.rs @@ -0,0 +1,959 @@ +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use jack::RawMidi; +use kanal::{Receiver, Sender}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{ + Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap, + }, + Frame, Terminal, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + error::Error, + fs, + io, +}; + +#[derive(Debug)] +enum MidiCommand { + ControlChange { cc: u8, value: u8 }, + Quit, +} + +struct MidiSender { + midi_out: jack::Port, + command_receiver: Receiver, +} + +impl MidiSender { + fn new( + client: &jack::Client, + command_receiver: Receiver, + ) -> Result> { + let midi_out = client + .register_port("fcb1010_mapper", jack::MidiOut::default()) + .map_err(|e| format!("Could not create MIDI output port: {}", e))?; + + Ok(Self { + midi_out, + command_receiver, + }) + } +} + +impl jack::ProcessHandler for MidiSender { + fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { + let mut midi_writer = self.midi_out.writer(ps); + + while let Ok(Some(command)) = self.command_receiver.try_recv() { + match command { + MidiCommand::ControlChange { cc, value } => { + let midi_data = [0xB0, cc, value]; // Control Change on channel 1 + if let Err(e) = midi_writer.write(&RawMidi { + time: 0, + bytes: &midi_data, + }) { + eprintln!("Failed to send MIDI: {}", e); + } + } + MidiCommand::Quit => return jack::Control::Quit, + } + } + + jack::Control::Continue + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct RegisterNote { + description: String, + register_values: Option>, // Store all register values +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct MappingData { + notes: Vec, + register_values: HashMap, +} + +impl Default for MappingData { + fn default() -> Self { + let mut register_values = HashMap::new(); + register_values.insert("IC03".to_string(), 0); + register_values.insert("IC10".to_string(), 0); + register_values.insert("IC11".to_string(), 0); + + Self { + notes: Vec::new(), + register_values, + } + } +} + +#[derive(Debug, PartialEq)] +enum InputMode { + Normal, + EditValue, + AddNote, + SaveFile, + LoadFile, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +enum SelectedRegister { + IC03, + IC10, + IC11, +} + +impl SelectedRegister { + fn as_str(&self) -> &str { + match self { + SelectedRegister::IC03 => "IC03", + SelectedRegister::IC10 => "IC10", + SelectedRegister::IC11 => "IC11", + } + } + + fn cc_pair(&self) -> (u8, u8) { + match self { + SelectedRegister::IC03 => (20, 21), + SelectedRegister::IC10 => (22, 23), + SelectedRegister::IC11 => (24, 25), + } + } +} + +struct App { + mapping_data: MappingData, + selected_register: SelectedRegister, + input_mode: InputMode, + input_buffer: String, + cursor_position: usize, + notes_list_state: ListState, + command_sender: Sender, + status_message: String, + show_help: bool, +} + +impl App { + fn new(command_sender: Sender) -> Self { + Self { + mapping_data: MappingData::default(), + selected_register: SelectedRegister::IC03, + input_mode: InputMode::Normal, + input_buffer: String::new(), + cursor_position: 0, + notes_list_state: ListState::default(), + command_sender, + status_message: "Ready - Press 'h' for help".to_string(), + show_help: false, + } + } + + fn get_current_value(&self) -> u8 { + *self.mapping_data.register_values.get(self.selected_register.as_str()).unwrap_or(&0) + } + + fn set_current_value(&mut self, value: u8) { + self.mapping_data.register_values.insert(self.selected_register.as_str().to_string(), value); + self.send_midi_value(value); + } + + fn send_midi_value(&self, value: u8) { + let (cc_low, cc_high) = self.selected_register.cc_pair(); + + if value <= 127 { + let _ = self.command_sender.send(MidiCommand::ControlChange { + cc: cc_low, + value, + }); + } else { + let _ = self.command_sender.send(MidiCommand::ControlChange { + cc: cc_high, + value: value - 128, + }); + } + } + + fn toggle_bit(&mut self, bit: u8) { + let current = self.get_current_value(); + let mask = 1 << bit; + let new_value = current ^ mask; + self.set_current_value(new_value); + self.status_message = format!("Toggled {}:bit{} ({})", + self.selected_register.as_str(), bit, new_value); + } + + fn add_note(&mut self, description: String) { + // Check if a note already exists for this register state + let current_values = self.mapping_data.register_values.clone(); + + // Find existing note with same register values + if let Some(existing_note) = self.mapping_data.notes.iter_mut().find(|note| { + note.register_values.as_ref() == Some(¤t_values) + }) { + // Update existing note + existing_note.description = description; + existing_note.register_values = Some(current_values); + self.status_message = "Note updated".to_string(); + } else { + // Create new note + let note = RegisterNote { + description, + register_values: Some(current_values), + }; + self.mapping_data.notes.push(note); + self.status_message = "Note added".to_string(); + } + } + + fn save_mapping(&self, filename: &str) -> Result<(), Box> { + let json = serde_json::to_string_pretty(&self.mapping_data)?; + fs::write(filename, json)?; + Ok(()) + } + + fn load_mapping(&mut self, filename: &str) -> Result<(), Box> { + let content = fs::read_to_string(filename)?; + self.mapping_data = serde_json::from_str(&content)?; + + // Send current register values to hardware + for (register, &value) in &self.mapping_data.register_values { + match register.as_str() { + "IC03" => { + self.selected_register = SelectedRegister::IC03; + self.send_midi_value(value); + } + "IC10" => { + self.selected_register = SelectedRegister::IC10; + self.send_midi_value(value); + } + "IC11" => { + self.selected_register = SelectedRegister::IC11; + self.send_midi_value(value); + } + _ => {} + } + } + + Ok(()) + } + + fn handle_input(&mut self, key: KeyCode) -> Result> { + match self.input_mode { + InputMode::Normal => self.handle_normal_input(key), + InputMode::EditValue => self.handle_edit_value_input(key), + InputMode::AddNote => self.handle_add_note_input(key), + InputMode::SaveFile => self.handle_save_file_input(key), + InputMode::LoadFile => self.handle_load_file_input(key), + } + } + + fn handle_normal_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Char('q') => { + let _ = self.command_sender.send(MidiCommand::Quit); + return Ok(true); + } + KeyCode::Char('h') => self.show_help = !self.show_help, + KeyCode::Left => { + self.selected_register = match self.selected_register { + SelectedRegister::IC03 => SelectedRegister::IC11, + SelectedRegister::IC10 => SelectedRegister::IC03, + SelectedRegister::IC11 => SelectedRegister::IC10, + }; + } + KeyCode::Right => { + self.selected_register = match self.selected_register { + SelectedRegister::IC03 => SelectedRegister::IC10, + SelectedRegister::IC10 => SelectedRegister::IC11, + SelectedRegister::IC11 => SelectedRegister::IC03, + }; + } + KeyCode::Char(c @ '0'..='7') => { + if let Some(bit) = c.to_digit(10) { + self.toggle_bit(bit as u8); + } + } + KeyCode::Char('r') => { + self.set_current_value(0); + self.status_message = format!("{} reset to 0", self.selected_register.as_str()); + } + KeyCode::Char('f') => { + self.set_current_value(255); + self.status_message = format!("{} set to 255", self.selected_register.as_str()); + } + KeyCode::Char('v') => { + self.input_mode = InputMode::EditValue; + self.input_buffer = self.get_current_value().to_string(); + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Char('n') => { + // Check if a note already exists for this register state + let current_values = self.mapping_data.register_values.clone(); + + // Find existing note with same register values and pre-load its text + if let Some(existing_note) = self.mapping_data.notes.iter().find(|note| { + note.register_values.as_ref() == Some(¤t_values) + }) { + self.input_buffer = existing_note.description.clone(); + } else { + self.input_buffer.clear(); + } + + self.cursor_position = self.input_buffer.chars().count(); + self.input_mode = InputMode::AddNote; + } + KeyCode::Char('s') => { + self.input_mode = InputMode::SaveFile; + self.input_buffer = "mapping.json".to_string(); + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Char('l') => { + self.input_mode = InputMode::LoadFile; + self.input_buffer = "mapping.json".to_string(); + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Up => { + if let Some(selected) = self.notes_list_state.selected() { + if selected > 0 { + self.notes_list_state.select(Some(selected - 1)); + } + } else if !self.mapping_data.notes.is_empty() { + self.notes_list_state.select(Some(0)); + } + } + KeyCode::Down => { + if let Some(selected) = self.notes_list_state.selected() { + if selected < self.mapping_data.notes.len().saturating_sub(1) { + self.notes_list_state.select(Some(selected + 1)); + } + } else if !self.mapping_data.notes.is_empty() { + self.notes_list_state.select(Some(0)); + } + } + KeyCode::Enter => { + if let Some(selected) = self.notes_list_state.selected() { + if let Some(note) = self.mapping_data.notes.get(selected) { + // Restore register values from note + if let Some(values) = note.register_values.as_ref() { + for (register, &value) in values { + self.mapping_data.register_values.insert(register.clone(), value); + // Send MIDI for each register + match register.as_str() { + "IC03" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC03; + self.send_midi_value(value); + self.selected_register = old_selected; + } + "IC10" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC10; + self.send_midi_value(value); + self.selected_register = old_selected; + } + "IC11" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC11; + self.send_midi_value(value); + self.selected_register = old_selected; + } + _ => {} + } + } + self.status_message = "State restored from note".to_string(); + } + } + } + } + KeyCode::Delete => { + if let Some(selected) = self.notes_list_state.selected() { + if selected < self.mapping_data.notes.len() { + self.mapping_data.notes.remove(selected); + if self.mapping_data.notes.is_empty() { + self.notes_list_state.select(None); + } else if selected >= self.mapping_data.notes.len() { + self.notes_list_state.select(Some(self.mapping_data.notes.len() - 1)); + } + self.status_message = "Note deleted".to_string(); + } + } + } + _ => {} + } + Ok(false) + } + + fn handle_edit_value_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Enter => { + match self.input_buffer.parse::() { + Ok(value) => { + self.set_current_value(value); + self.status_message = format!("{} set to {}", + self.selected_register.as_str(), value); + } + Err(_) => { + self.status_message = "Invalid value (0-255)".to_string(); + } + } + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + KeyCode::Right => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + self.cursor_position += 1; + } + } + KeyCode::Home => { + self.cursor_position = 0; + } + KeyCode::End => { + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Backspace => { + if self.cursor_position > 0 { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position - 1); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position -= 1; + } + } + KeyCode::Delete => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position); + self.input_buffer = chars.into_iter().collect(); + } + } + KeyCode::Char(c) => { + if let Ok(potential_value) = format!("{}{}", &self.input_buffer[..self.cursor_position], c).parse::() { + if potential_value <= 255 { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.insert(self.cursor_position, c); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position += 1; + } + } else if self.input_buffer.len() < 3 { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.insert(self.cursor_position, c); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position += 1; + } + } + _ => {} + } + Ok(false) + } + + fn handle_add_note_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Enter => { + if !self.input_buffer.trim().is_empty() { + self.add_note(self.input_buffer.clone()); + } + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + KeyCode::Right => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + self.cursor_position += 1; + } + } + KeyCode::Home => { + self.cursor_position = 0; + } + KeyCode::End => { + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Backspace => { + if self.cursor_position > 0 { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position - 1); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position -= 1; + } + } + KeyCode::Delete => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position); + self.input_buffer = chars.into_iter().collect(); + } + } + KeyCode::Char(c) => { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.insert(self.cursor_position, c); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position += 1; + } + _ => {} + } + Ok(false) + } + + fn handle_save_file_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Enter => { + match self.save_mapping(&self.input_buffer) { + Ok(_) => self.status_message = format!("Saved to {}", self.input_buffer), + Err(e) => self.status_message = format!("Save failed: {}", e), + } + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + KeyCode::Right => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + self.cursor_position += 1; + } + } + KeyCode::Home => { + self.cursor_position = 0; + } + KeyCode::End => { + self.cursor_position = self.input_buffer.chars().count(); + } + KeyCode::Backspace => { + if self.cursor_position > 0 { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position - 1); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position -= 1; + } + } + KeyCode::Delete => { + let char_count = self.input_buffer.chars().count(); + if self.cursor_position < char_count { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.remove(self.cursor_position); + self.input_buffer = chars.into_iter().collect(); + } + } + KeyCode::Char(c) => { + let mut chars: Vec = self.input_buffer.chars().collect(); + chars.insert(self.cursor_position, c); + self.input_buffer = chars.into_iter().collect(); + self.cursor_position += 1; + } + _ => {} + } + Ok(false) + } + + fn handle_load_file_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Enter => { + let filename = self.input_buffer.clone(); + match self.load_mapping(&filename) { + Ok(_) => self.status_message = format!("Loaded from {}", filename), + Err(e) => self.status_message = format!("Load failed: {}", e), + } + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + self.cursor_position = 0; + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + KeyCode::Right => { + if self.cursor_position < self.input_buffer.len() { + self.cursor_position += 1; + } + } + KeyCode::Home => { + self.cursor_position = 0; + } + KeyCode::End => { + self.cursor_position = self.input_buffer.len(); + } + KeyCode::Backspace => { + if self.cursor_position > 0 { + self.input_buffer.remove(self.cursor_position - 1); + self.cursor_position -= 1; + } + } + KeyCode::Delete => { + if self.cursor_position < self.input_buffer.len() { + self.input_buffer.remove(self.cursor_position); + } + } + KeyCode::Char(c) => { + self.input_buffer.insert(self.cursor_position, c); + self.cursor_position += 1; + } + _ => {} + } + Ok(false) + } +} + +fn ui(f: &mut Frame, app: &mut App) { + if app.show_help { + draw_help(f); + return; + } + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(7), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(f.size()); + + // Title + let title = Paragraph::new("FCB1010 Register Mapper") + .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, chunks[0]); + + // Register values + draw_register_values(f, app, chunks[1]); + + // Notes list + draw_notes_list(f, app, chunks[2]); + + // Status and input + draw_status_bar(f, app, chunks[3]); + + // Input popup if needed + if app.input_mode != InputMode::Normal { + draw_input_popup(f, app); + } +} + +fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) + .split(area); + + let registers = [ + (&SelectedRegister::IC03, "IC03"), + (&SelectedRegister::IC10, "IC10"), + (&SelectedRegister::IC11, "IC11"), + ]; + + for (i, (register, name)) in registers.iter().enumerate() { + let value = *app.mapping_data.register_values.get(*name).unwrap_or(&0); + let is_selected = app.selected_register == **register; + + let mut lines = Vec::new(); + lines.push(Line::from(vec![ + Span::styled(format!("{}: ", name), Style::default().add_modifier(Modifier::BOLD)), + Span::raw(format!("{:08b} = {}", value, value)), + ])); + + // Bit representation + let mut bit_spans = Vec::new(); + for bit in (0..8).rev() { + let is_set = (value >> bit) & 1 == 1; + let style = if is_set { + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + bit_spans.push(Span::styled(format!("[{}]", if is_set { "■" } else { "□" }), style)); + } + lines.push(Line::from(bit_spans)); + + // Bit numbers + let mut bit_num_spans = Vec::new(); + for bit in (0..8).rev() { + bit_num_spans.push(Span::styled(format!(" {} ", bit), Style::default().fg(Color::Yellow))); + } + lines.push(Line::from(bit_num_spans)); + + let border_style = if is_selected { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + + let block = Block::default() + .borders(Borders::ALL) + .style(border_style) + .title(if is_selected { format!("→ {}", name) } else { name.to_string() }); + + let paragraph = Paragraph::new(lines).block(block); + f.render_widget(paragraph, chunks[i]); + } +} + +fn draw_notes_list(f: &mut Frame, app: &mut App, area: Rect) { + let items: Vec = app + .mapping_data + .notes + .iter() + .map(|note| { + let mut content = String::new(); + + // Add register values in hex format first + if let Some(values) = ¬e.register_values { + let ic03 = values.get("IC03").unwrap_or(&0); + let ic10 = values.get("IC10").unwrap_or(&0); + let ic11 = values.get("IC11").unwrap_or(&0); + content.push_str(&format!("[IC03:0x{:02X}, IC10:0x{:02X}, IC11:0x{:02X}]", ic03, ic10, ic11)); + } + + // Add dash separator and note description + content.push_str(" - "); + content.push_str(¬e.description); + + ListItem::new(content) + }) + .collect(); + + let notes_list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Notes (↑↓ to navigate, Enter to restore, Del to remove)")) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .highlight_symbol("→ "); + + f.render_stateful_widget(notes_list, area, &mut app.notes_list_state); +} + +fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { + let status_text = match app.input_mode { + InputMode::Normal => format!("Status: {} | Commands: ←→:select reg, 0-7:toggle bit, v:edit value, n:add note, s:save, l:load, r:reset, f:fill, h:help, q:quit", app.status_message), + InputMode::EditValue => format!("Enter value (0-255): {}", app.input_buffer), + InputMode::AddNote => format!("Enter note: {}", app.input_buffer), + InputMode::SaveFile => format!("Save filename: {}", app.input_buffer), + InputMode::LoadFile => format!("Load filename: {}", app.input_buffer), + }; + + let paragraph = Paragraph::new(status_text) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(paragraph, area); +} + +fn draw_input_popup(f: &mut Frame, app: &App) { + let area = centered_rect(60, 20, f.size()); + f.render_widget(Clear, area); + + let title = match app.input_mode { + InputMode::EditValue => "Edit Value", + InputMode::AddNote => "Add/Edit Note", + InputMode::SaveFile => "Save File", + InputMode::LoadFile => "Load File", + _ => "Input", + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .style(Style::default().fg(Color::Yellow)); + + // Create text with cursor using char-aware operations + let mut spans = Vec::new(); + let chars: Vec = app.input_buffer.chars().collect(); + + if chars.is_empty() { + spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + } else { + if app.cursor_position > 0 { + let before_cursor: String = chars.iter().take(app.cursor_position).collect(); + spans.push(Span::raw(before_cursor)); + } + + if app.cursor_position < chars.len() { + let cursor_char = chars[app.cursor_position]; + spans.push(Span::styled(cursor_char.to_string(), Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + + if app.cursor_position + 1 < chars.len() { + let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect(); + spans.push(Span::raw(after_cursor)); + } + } else { + spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + } + } + + let paragraph = Paragraph::new(Line::from(spans)) + .block(block); + + f.render_widget(paragraph, area); +} + +fn draw_help(f: &mut Frame) { + let help_text = vec![ + "FCB1010 Register Mapper - Help", + "", + "Register Selection:", + " ← → - Select IC03, IC10, IC11", + "", + "Bit Control:", + " 0-7 - Toggle bit 0-7 of selected register", + " v - Edit register value directly (0-255)", + " r - Reset register to 0", + " f - Fill register (set to 255)", + "", + "Notes:", + " n - Add note for current register/bit", + " ↑↓ - Navigate notes list", + " Enter - Restore register values from selected note", + " Del - Delete selected note", + "", + "File Operations:", + " s - Save mapping to file", + " l - Load mapping from file", + "", + "Other:", + " h - Toggle this help", + " q - Quit", + "", + "Press 'h' again to return to main view", + ]; + + let help_paragraph = Paragraph::new( + help_text + .iter() + .map(|&line| Line::from(line)) + .collect::>() + ) + .block(Block::default().title("Help").borders(Borders::ALL)) + .wrap(Wrap { trim: true }); + + f.render_widget(help_paragraph, f.size()); +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, +) -> Result<(), Box> { + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + if app.handle_input(key.code)? { + break; + } + } + } + } + Ok(()) +} + +fn main() -> Result<(), Box> { + // Create JACK client + let (client, status) = jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?; + if !status.is_empty() { + eprintln!("JACK client status: {:?}", status); + } + + // Create command channel + let (command_sender, command_receiver): (Sender, Receiver) = + kanal::bounded(32); + + // Create MIDI sender + let midi_sender = MidiSender::new(&client, command_receiver)?; + + // Activate client + let _active_client = client.activate_async((), midi_sender)?; + + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app and run + let app = App::new(command_sender); + let res = run_app(&mut terminal, app); + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + Ok(()) +} \ No newline at end of file