Mapper tool for decoding charlieplex
This commit is contained in:
parent
9510ba04be
commit
6eb15fbd12
225
Cargo.lock
generated
225
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
wmidi = "4"
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
|
||||
35
firmware/Cargo.lock
generated
35
firmware/Cargo.lock
generated
@ -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",
|
||||
|
||||
9
firmware/Cargo.toml
Normal file → Executable file
9
firmware/Cargo.toml
Normal file → Executable file
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
185
firmware/src/main.rs
Normal file → Executable file
185
firmware/src/main.rs
Normal file → Executable file
@ -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<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
select_en: stm32f0xx_hal::gpio::gpiob::PB8<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::OpenDrain>>,
|
||||
pb0: stm32f0xx_hal::gpio::gpiob::PB0<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb1: stm32f0xx_hal::gpio::gpiob::PB1<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb2: stm32f0xx_hal::gpio::gpiob::PB2<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb3: stm32f0xx_hal::gpio::gpiob::PB3<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb4: stm32f0xx_hal::gpio::gpiob::PB4<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb5: stm32f0xx_hal::gpio::gpiob::PB5<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb6: stm32f0xx_hal::gpio::gpiob::PB6<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb7: stm32f0xx_hal::gpio::gpiob::PB7<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
latch_en:
|
||||
stm32f0xx_hal::gpio::gpioa::PA0<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
select_en: stm32f0xx_hal::gpio::gpiob::PB8<
|
||||
stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::OpenDrain>,
|
||||
>,
|
||||
pb0:
|
||||
stm32f0xx_hal::gpio::gpiob::PB0<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb1:
|
||||
stm32f0xx_hal::gpio::gpiob::PB1<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb2:
|
||||
stm32f0xx_hal::gpio::gpiob::PB2<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb3:
|
||||
stm32f0xx_hal::gpio::gpiob::PB3<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb4:
|
||||
stm32f0xx_hal::gpio::gpiob::PB4<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb5:
|
||||
stm32f0xx_hal::gpio::gpiob::PB5<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb6:
|
||||
stm32f0xx_hal::gpio::gpiob::PB6<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
pb7:
|
||||
stm32f0xx_hal::gpio::gpiob::PB7<stm32f0xx_hal::gpio::Output<stm32f0xx_hal::gpio::PushPull>>,
|
||||
}
|
||||
|
||||
#[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<stm32f0xx_hal::serial::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
mapper/Cargo.toml
Normal file
12
mapper/Cargo.toml
Normal file
@ -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"
|
||||
26
mapper/format.txt
Normal file
26
mapper/format.txt
Normal file
@ -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
|
||||
169
mapper/mapping.json
Normal file
169
mapper/mapping.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
959
mapper/src/main.rs
Normal file
959
mapper/src/main.rs
Normal file
@ -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<jack::MidiOut>,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
}
|
||||
|
||||
impl MidiSender {
|
||||
fn new(
|
||||
client: &jack::Client,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
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<HashMap<String, u8>>, // Store all register values
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct MappingData {
|
||||
notes: Vec<RegisterNote>,
|
||||
register_values: HashMap<String, u8>,
|
||||
}
|
||||
|
||||
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<MidiCommand>,
|
||||
status_message: String,
|
||||
show_help: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(command_sender: Sender<MidiCommand>) -> 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<dyn Error>> {
|
||||
let json = serde_json::to_string_pretty(&self.mapping_data)?;
|
||||
fs::write(filename, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_mapping(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
match key {
|
||||
KeyCode::Enter => {
|
||||
match self.input_buffer.parse::<u8>() {
|
||||
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<char> = 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<char> = 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::<u16>() {
|
||||
if potential_value <= 255 {
|
||||
let mut chars: Vec<char> = 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<char> = 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<bool, Box<dyn Error>> {
|
||||
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<char> = 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<char> = self.input_buffer.chars().collect();
|
||||
chars.remove(self.cursor_position);
|
||||
self.input_buffer = chars.into_iter().collect();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut chars: Vec<char> = 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<bool, Box<dyn Error>> {
|
||||
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<char> = 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<char> = self.input_buffer.chars().collect();
|
||||
chars.remove(self.cursor_position);
|
||||
self.input_buffer = chars.into_iter().collect();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut chars: Vec<char> = 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<bool, Box<dyn Error>> {
|
||||
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<ListItem> = 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<char> = 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::<Vec<_>>()
|
||||
)
|
||||
.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<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
// 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<MidiCommand>, Receiver<MidiCommand>) =
|
||||
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(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user