diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1f6df7..76dc908 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,7 @@ jobs: - name: Verify embedded binaries exist run: | set -euo pipefail - expected=(mini module6 module15 module32 original original-v2 plus revised-mini xl) + expected=(module6 module15 module32 original original-v2 plus xl) missing=0 for bin in "${expected[@]}"; do if [ -f "target/thumbv6m-none-eabi/release/$bin" ]; then @@ -89,14 +89,12 @@ jobs: with: name: firmware-${{ github.sha }} path: | - target/thumbv6m-none-eabi/release/mini target/thumbv6m-none-eabi/release/module6 target/thumbv6m-none-eabi/release/module15 target/thumbv6m-none-eabi/release/module32 target/thumbv6m-none-eabi/release/original target/thumbv6m-none-eabi/release/original-v2 target/thumbv6m-none-eabi/release/plus - target/thumbv6m-none-eabi/release/revised-mini target/thumbv6m-none-eabi/release/xl retention-days: 30 diff --git a/CLAUDE.md b/CLAUDE.md index 063fa27..79d0c9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with th ## Project Overview -ProductionDeck is an open-source RP2040-based Stream Deck–compatible firmware in Rust (Embassy). Multiple **device profiles** (Mini, Classic/Mk.2, XL, Neo, +, modules, etc.) share the same codebase; each profile selects USB PID, layout, and **Legacy (Mini)** vs **Main/Expanded** HID handling. Physical hardware is often a small key matrix + one ST7735 region unless you build a larger layout. +ProductionDeck is an open-source RP2040-based Stream Deck–compatible firmware in Rust (Embassy). Multiple **device profiles** (Classic/Mk.2, XL, Neo, +, modules, etc.) share the same codebase; each profile selects USB PID, layout, and **Legacy (Mini-family)** vs **Main/Expanded** HID handling. Six-key Mini-class hardware is covered only by **`module6`** ([`Device::Module6Keys`]). Physical hardware is often a small key matrix + one ST7735 region unless you build a larger layout. **Current Status**: Alpha - Firmware compiles successfully, ready for hardware testing. @@ -50,7 +50,7 @@ cargo doc --open ## Project Structure ### Core Source Files -- `src/bin/*.rs` - One binary per target device (`mini`, `xl`, `mk2`, `neo`, `plus-xl`, …) +- `src/bin/*.rs` - One binary per target device (`module6`, `xl`, `mk2`, `neo`, `plus-xl`, …) - `src/lib.rs` - Library root (`productiondeck` crate) - `src/config.rs` - Hardware configuration constants and pin assignments - `src/device/mod.rs` - USB PID, layout, and protocol family per `Device` diff --git a/Cargo.lock b/Cargo.lock index daf2e04..dfce46c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,6 +1233,7 @@ dependencies = [ "embassy-time", "embassy-usb", "embedded-graphics", + "embedded-graphics-core", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-bus", diff --git a/Cargo.toml b/Cargo.toml index eed8ee6..a5dc776 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ portable-atomic = { version = "1.0", features = ["critical-section"] } # Display and graphics st7735-lcd = "0.10" embedded-graphics = "0.8" +embedded-graphics-core = "0.4" # USB HID usbd-hid = "0.10" @@ -66,18 +67,6 @@ debug = 2 incremental = false opt-level = "z" -[[bin]] -name = "mini" -path = "src/bin/mini.rs" -test = false -bench = false - -[[bin]] -name = "revised-mini" -path = "src/bin/revised_mini.rs" -test = false -bench = false - [[bin]] name = "original" path = "src/bin/original.rs" diff --git a/README.md b/README.md index fe2c97d..db4b111 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ cargo install elf2uf2-rs flip-link ## Building ```bash -cargo build --release --bin mini +cargo build --release --bin module6 ``` Other devices: `original`, `xl`, `plus`, `module6`, etc. diff --git a/build-devices.sh b/build-devices.sh index e363476..57916e8 100644 --- a/build-devices.sh +++ b/build-devices.sh @@ -7,7 +7,7 @@ echo "=== ProductionDeck Multi-Device Build Script ===" echo # List of devices to build -devices=("mini" "revised-mini" "original" "original-v2" "xl" "plus") +devices=("original" "original-v2" "xl" "plus") echo "Available device targets:" for device in "${devices[@]}"; do @@ -45,7 +45,7 @@ else echo "All builds completed!" echo echo "Usage examples:" - echo " cargo run --release --bin mini # Run Mini firmware" + echo " cargo run --release --bin module6 # Module 6 / Mini-family firmware" echo " cargo run --release --bin xl # Run XL firmware" - echo " ./build-devices.sh mini # Build only Mini firmware" + echo " ./build-devices.sh module6 # Build only Module 6 firmware" fi \ No newline at end of file diff --git a/src/bin/mini.rs b/src/bin/mini.rs deleted file mode 100644 index ff187a7..0000000 --- a/src/bin/mini.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![allow(unreachable_code)] -//! ProductionDeck — StreamDeck Mini (PID `0x0063`), V1 BMP, 3×2 keys. - -#![no_std] -#![no_main] - -use cortex_m_rt::entry; -use defmt_rtt as _; -use panic_halt as _; -use productiondeck::device::Device; -use productiondeck::entry::{run_multicore, MulticoreCore0Layout, MulticoreCore1Buffer}; - -const DEVICE: Device = Device::Mini; - -#[entry] -fn main() -> ! { - run_multicore( - DEVICE, - MulticoreCore0Layout::MiniOrModule6Direct, - MulticoreCore1Buffer::B8192, - ) -} diff --git a/src/bin/module6.rs b/src/bin/module6.rs index 29a4184..b3e8942 100644 --- a/src/bin/module6.rs +++ b/src/bin/module6.rs @@ -16,7 +16,7 @@ const DEVICE: Device = Device::Module6Keys; fn main() -> ! { run_multicore( DEVICE, - MulticoreCore0Layout::MiniOrModule6Direct, + MulticoreCore0Layout::Module6DirectTc00, MulticoreCore1Buffer::B8192, ) } diff --git a/src/bin/revised_mini.rs b/src/bin/revised_mini.rs deleted file mode 100644 index 518a55e..0000000 --- a/src/bin/revised_mini.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! ProductionDeck — Stream Deck Mini 2022 (PID `0x0090`). - -#![no_std] -#![no_main] - -use defmt_rtt as _; -use embassy_executor::Spawner; -use panic_halt as _; -use productiondeck::device::Device; -use productiondeck::entry::run_single_core; - -const DEVICE: Device = Device::RevisedMini; - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - run_single_core(spawner, DEVICE).await; -} diff --git a/src/channels.rs b/src/channels.rs index c3d0d84..b5133c6 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -3,8 +3,9 @@ //! This module defines all the Embassy channels used for communication //! between different tasks in the ProductionDeck application. +use crate::config::{DISPLAY_CHANNEL_CAPACITY, MULTICORE_CHANNEL_SIZE, USB_COMMAND_CHANNEL_SIZE}; use crate::types::{ButtonState, DisplayCommand, UsbCommand}; -use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, ThreadModeRawMutex}; use embassy_sync::channel::Channel; /// Channel for button state communication from button task to USB task @@ -13,8 +14,16 @@ pub static BUTTON_CHANNEL: Channel = Channel /// Channel for USB commands from HID handler to other tasks /// Buffer size: 4 (allows some buffering of commands) -pub static USB_COMMAND_CHANNEL: Channel = Channel::new(); +pub static USB_COMMAND_CHANNEL: Channel = + Channel::new(); /// Channel for display commands to the display task -/// Buffer size: 8 (allows buffering of multiple display operations) -pub static DISPLAY_CHANNEL: Channel = Channel::new(); +pub static DISPLAY_CHANNEL: Channel = + Channel::new(); + +/// Core 0 → Core 1 display commands (multicore builds). Uses [`MULTICORE_CHANNEL_SIZE`] slots. +pub static MULTICORE_IMAGE_CHANNEL: Channel< + CriticalSectionRawMutex, + DisplayCommand, + MULTICORE_CHANNEL_SIZE, +> = Channel::new(); diff --git a/src/config.rs b/src/config.rs index 80ffd06..a370c3c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -203,12 +203,24 @@ pub fn display_total_height() -> usize { // USB Configuration pub const USB_POLL_RATE_MS: u64 = 1; // 1ms USB polling (1000Hz) + pub const IMAGE_BUFFER_SIZE: usize = 1024; // 1KB buffer size // Image processing optimization pub const IMAGE_PROCESSING_BUFFER_SIZE: usize = 8192; // 8KB for image processing pub const DISPLAY_BUFFER_SIZE: usize = 2048; // 2KB for display operations -pub const MULTICORE_CHANNEL_SIZE: usize = 8; // Increased channel size for better throughput + +/// Queue depth for [`crate::channels::MULTICORE_IMAGE_CHANNEL`]. +pub const MULTICORE_CHANNEL_SIZE: usize = 8; + +/// Raw BMP accumulation cap for Module 6 chunked uploads. +pub const MODULE6_BMP_CAP: usize = 1024; + +/// Depth of [`crate::channels::USB_COMMAND_CHANNEL`]. +pub const USB_COMMAND_CHANNEL_SIZE: usize = 4; + +/// Depth of [`crate::channels::DISPLAY_CHANNEL`]. +pub const DISPLAY_CHANNEL_CAPACITY: usize = 8; // =================================================================== // Power Management: Idle Time (Sleep Mode) diff --git a/src/device/mod.rs b/src/device/mod.rs index 46a7f9e..49f358c 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -4,7 +4,7 @@ //! abstracting away device-specific configurations, protocols, and capabilities. //! //! Protocol families (Elgato HID API): -//! - **Legacy / Mini family**: Mini, Mini 2022, Mini Discord, 6-key Module — distinct report layout. +//! - **Mini / 6-key Module**: [`Device::Module6Keys`] only (Mini-family HID); use `--bin module6`. //! - **Main / Expanded family**: Classic, XL, Neo, Plus, Plus XL, 15/32-key Modules — see General Reference. pub mod neo; @@ -134,11 +134,7 @@ pub trait DeviceConfig { #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Device { - Mini = 0, - /// Mini 2022 (Elgato PID 0x0090) - RevisedMini = 1, - /// Mini Discord (0x00B3) - MiniDiscord = 2, + /// Runtime tags `0`…`2` are unused (removed Mini-family firmware SKUs). /// First-gen Stream Deck (0x0060) Original = 3, /// Stream Deck 2019 (0x006D) @@ -170,9 +166,6 @@ impl Device { return None; } match tag { - 0 => Some(Device::Mini), - 1 => Some(Device::RevisedMini), - 2 => Some(Device::MiniDiscord), 3 => Some(Device::Original), 4 => Some(Device::OriginalV2), 5 => Some(Device::Mk2), @@ -191,9 +184,6 @@ impl Device { pub fn pid(&self) -> u16 { match self { - Device::Mini => 0x0063, - Device::RevisedMini => 0x0090, - Device::MiniDiscord => 0x00B3, Device::Original => 0x0060, Device::OriginalV2 => 0x006d, Device::Mk2 => 0x0080, @@ -221,9 +211,7 @@ impl Device { Device::Plus => (2u8, 4u8, 120u16, 120u16, 800u16, 480u16), Device::PlusXl => (4u8, 9u8, 112u16, 112u16, 1280u16, 800u16), Device::Neo => (2u8, 4u8, 96u16, 96u16, 480u16, 320u16), - Device::Mini | Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { - (2u8, 3u8, 80u16, 80u16, 320u16, 240u16) - } + Device::Module6Keys => (2u8, 3u8, 80u16, 80u16, 320u16, 240u16), Device::Original => (3u8, 5u8, 72u16, 72u16, 480u16, 272u16), }; let mut b = [0u8; 16]; @@ -271,9 +259,6 @@ impl Device { impl DeviceConfig for Device { fn device_name(&self) -> &'static str { match self { - Device::Mini => "StreamDeck Mini", - Device::RevisedMini => "StreamDeck Mini 2022", - Device::MiniDiscord => "StreamDeck Mini Discord", Device::Original => "StreamDeck Original", Device::OriginalV2 => "StreamDeck Classic (2019)", Device::Mk2 => "StreamDeck Mk.2", @@ -298,9 +283,7 @@ impl DeviceConfig for Device { fn button_layout(&self) -> ButtonLayout { match self { - Device::Mini | Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { - ButtonLayout::new(3, 2, true) - } + Device::Module6Keys => ButtonLayout::new(3, 2, true), Device::Module15Keys | Device::OriginalV2 | Device::Mk2 | Device::Mk2ScissorKeys => { ButtonLayout::new(5, 3, true) } @@ -313,16 +296,14 @@ impl DeviceConfig for Device { fn display_config(&self) -> DisplayConfig { match self { - Device::Mini | Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { - DisplayConfig { - image_width: 80, - image_height: 80, - format: ImageFormat::Bmp, - needs_rotation: true, - flip_horizontal: false, - flip_vertical: false, - } - } + Device::Module6Keys => DisplayConfig { + image_width: 80, + image_height: 80, + format: ImageFormat::Bmp, + needs_rotation: true, + flip_horizontal: false, + flip_vertical: false, + }, Device::Module15Keys | Device::OriginalV2 | Device::Mk2 | Device::Mk2ScissorKeys => { DisplayConfig { image_width: 72, @@ -378,27 +359,6 @@ impl DeviceConfig for Device { fn usb_config(&self) -> UsbConfig { match self { - Device::Mini => UsbConfig { - vid: 0x0fd9, - pid: 0x0063, - product_name: "Stream Deck Mini", - manufacturer: "Elgato Systems", - protocol: ProtocolVersion::V1, - }, - Device::RevisedMini => UsbConfig { - vid: 0x0fd9, - pid: 0x0090, - product_name: "Stream Deck Mini", - manufacturer: "Elgato Systems", - protocol: ProtocolVersion::V1, - }, - Device::MiniDiscord => UsbConfig { - vid: 0x0fd9, - pid: 0x00B3, - product_name: "Stream Deck Mini", - manufacturer: "Elgato Systems", - protocol: ProtocolVersion::V1, - }, Device::Original => UsbConfig { vid: 0x0fd9, pid: 0x0060, diff --git a/src/entry.rs b/src/entry.rs index 2df68f7..697ae76 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -3,7 +3,7 @@ #![allow(unreachable_code)] use crate::buttons; -use crate::config::{self, MULTICORE_CHANNEL_SIZE}; +use crate::config::{self}; use crate::device::{Device, DeviceConfig}; use crate::hardware; use crate::supervisor::AppSupervisor; @@ -14,8 +14,6 @@ use embassy_executor::{Executor, Spawner}; use embassy_rp::gpio::{Input, Level, Output, Pull}; use embassy_rp::multicore::{spawn_core1, Stack}; use embassy_rp::usb::Driver; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::channel::Channel; use static_cell::StaticCell; // --------------------------------------------------------------------------- @@ -58,13 +56,6 @@ pub async fn run_single_core_quiet(spawner: Spawner, device: Device) { // Multicore (`cortex_m_rt::entry` + dual executors) // --------------------------------------------------------------------------- -/// Cross-core display pipeline (not yet wired from core 0). -pub static MULTICORE_IMAGE_CHANNEL: Channel< - CriticalSectionRawMutex, - DisplayCommand, - MULTICORE_CHANNEL_SIZE, -> = Channel::new(); - static mut CORE1_STACK: Stack<4096> = Stack::new(); static EXECUTOR0: StaticCell = StaticCell::new(); static EXECUTOR1: StaticCell = StaticCell::new(); @@ -72,8 +63,8 @@ static EXECUTOR1: StaticCell = StaticCell::new(); /// GPIO / button wiring for multicore Stream Deck–style builds. #[derive(Clone, Copy)] pub enum MulticoreCore0Layout { - /// Mini / Module 6: USB LED `PIN_20`, heartbeat `PIN_25` + `PIN_21`, six direct inputs. - MiniOrModule6Direct, + /// TC-00 / Iryx wiring for Module 6: GP1,2,3,4,9,10 (`--bin module6`). + Module6DirectTc00, /// Module 15: USB `PIN_20`, status `PIN_25` / `PIN_21`, 5×3 matrix. Module15Matrix, /// Module 32: USB LED `PIN_25`, status `PIN_20` / `PIN_21`, 8×4 matrix. @@ -83,7 +74,7 @@ pub enum MulticoreCore0Layout { /// Core 1 image buffer size for the display stub loop. #[derive(Clone, Copy)] pub enum MulticoreCore1Buffer { - /// 8 KiB (Mini, Module 6, Module 15). + /// 8 KiB (`module6` stub path, Module 15). B8192, /// 16 KiB (Module 32). B16384, @@ -122,7 +113,7 @@ pub fn run_multicore( executor0.run(|spawner| { unwrap!(multicore_core0_supervisor_task(supervisor).map(|t| spawner.spawn(t))); match layout { - MulticoreCore0Layout::MiniOrModule6Direct => { + MulticoreCore0Layout::Module6DirectTc00 => { unwrap!(usb::usb_task_for_device( Driver::new(p.USB, crate::Irqs), Output::new(p.PIN_20, Level::Low), @@ -131,12 +122,12 @@ pub fn run_multicore( .map(|t| spawner.spawn(t))); unwrap!(buttons::button_task_direct({ let mut inputs = heapless::Vec::new(); + let _ = inputs.push(Input::new(p.PIN_1, Pull::Up)); + let _ = inputs.push(Input::new(p.PIN_2, Pull::Up)); + let _ = inputs.push(Input::new(p.PIN_3, Pull::Up)); let _ = inputs.push(Input::new(p.PIN_4, Pull::Up)); - let _ = inputs.push(Input::new(p.PIN_5, Pull::Up)); - let _ = inputs.push(Input::new(p.PIN_6, Pull::Up)); + let _ = inputs.push(Input::new(p.PIN_9, Pull::Up)); let _ = inputs.push(Input::new(p.PIN_10, Pull::Up)); - let _ = inputs.push(Input::new(p.PIN_11, Pull::Up)); - let _ = inputs.push(Input::new(p.PIN_12, Pull::Up)); inputs }) .map(|t| spawner.spawn(t))); @@ -226,7 +217,7 @@ async fn multicore_core1_image_loop(device: Device, buf: &mut [u8]) { core::panic!("Image processing initialization failed"); } } - let receiver = MULTICORE_IMAGE_CHANNEL.receiver(); + let receiver = crate::channels::MULTICORE_IMAGE_CHANNEL.receiver(); loop { match receiver.receive().await { DisplayCommand::DisplayImage { key_id, data } => { diff --git a/src/hardware.rs b/src/hardware.rs index f4a3937..7c2fed5 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -53,18 +53,42 @@ impl HardwareConfig { /// /// This is the canonical row/column assignment for each layout; do not duplicate in `config`. pub fn for_device(device: Device) -> Self { + if device == Device::Neo { + // Eight direct GPIOs (button index 0 → GP1 … button 7 → GP26). + return Self { + device, + button_pins: ButtonPins { + row_pins: &[], + col_pins: &[1, 2, 3, 4, 9, 10, 15, 26][..], + }, + display_pins: DisplayPins { + spi_mosi: 19, + spi_sck: 18, + cs: 8, + dc: 14, + rst: 15, + backlight: 17, + }, + led_pins: LedPins { + status: 25, + usb: 20, + error: 21, + }, + }; + } + let layout = device.button_layout(); // Get pin assignments based on device layout let (row_pins, col_pins) = match (layout.rows, layout.cols) { - (2, 3) => (&[2u8, 3][..], &[4u8, 5, 6][..]), // Mini + (2, 3) => (&[2u8, 3][..], &[4u8, 5, 6][..]), // 2×3 six-key matrix (3, 5) => (&[2u8, 3, 7][..], &[4u8, 5, 6, 10, 11][..]), // Original (4, 8) => (&[2u8, 3, 7, 9][..], &[4u8, 5, 6, 10, 11, 12, 13, 16][..]), // XL (4, 9) => ( &[2u8, 3, 7, 9][..], &[4u8, 5, 6, 10, 11, 12, 13, 16, 22][..], ), // + XL - (2, 4) => (&[2u8, 3][..], &[4u8, 5, 6, 10][..]), // Plus / Neo + (2, 4) => (&[2u8, 3][..], &[4u8, 5, 6, 10][..]), // Plus (Neo: direct pins, see above) _ => core::panic!("no pin mapping for matrix {}×{}", layout.cols, layout.rows), }; @@ -118,16 +142,6 @@ pub async fn init_hardware_tasks_core0( // Spawn USB task spawner.spawn(usb_task_for_device(driver, usb_led, hw_config.device)?); - // For Mini devices, prefer Direct pin mode with 6 dedicated inputs - if matches!( - device, - crate::device::Device::Mini - | crate::device::Device::RevisedMini - | crate::device::Device::MiniDiscord - ) { - crate::config::set_button_input_mode(crate::config::ButtonInputMode::Direct); - } - // Spawn button task with device-specific layout spawn_button_task_with_pins(spawner, row_pins, col_pins, device)?; @@ -174,16 +188,7 @@ async fn init_hardware_tasks_with_config( // Spawn USB task spawner.spawn(usb_task_for_device(driver, usb_led, hw_config.device)?); - // For Mini devices, prefer Direct pin mode with 6 dedicated inputs let device = hw_config.device; - if matches!( - device, - crate::device::Device::Mini - | crate::device::Device::RevisedMini - | crate::device::Device::MiniDiscord - ) { - crate::config::set_button_input_mode(crate::config::ButtonInputMode::Direct); - } // Spawn button task with device-specific layout spawn_button_task_with_pins(spawner, row_pins, col_pins, device)?; @@ -212,6 +217,7 @@ fn create_all_pins_for_device( let Peripherals { FLASH, USB, + PIN_1, PIN_2, PIN_3, PIN_4, @@ -223,11 +229,13 @@ fn create_all_pins_for_device( PIN_11, PIN_12, PIN_13, + PIN_15, PIN_16, PIN_20, PIN_21, PIN_22, PIN_25, + PIN_26, .. } = p; @@ -244,84 +252,81 @@ fn create_all_pins_for_device( let mut row_pins: Vec, 4> = Vec::new(); let mut col_pins: Vec, 32> = Vec::new(); - // If Direct mode is selected for Mini, build 6 direct input pins - if matches!( - crate::config::button_input_mode(), - crate::config::ButtonInputMode::Direct - ) && matches!( - device, - Device::Mini | Device::RevisedMini | Device::MiniDiscord - ) { - // Build six dedicated direct-input pins for Mini to avoid partial-move issues - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + // Neo: eight independent keys on GP1,2,3,4,9,10,15,26 (see [`HardwareConfig::for_device`]). + // Push order matches [`spawn_button_task_with_pins`] direct-mode pop/reassembly (button 0 = GP1). + if device == Device::Neo { + let _ = col_pins.push(Input::new(PIN_26, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_15, Pull::Up)); let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_12, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_9, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_3, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_2, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_1, Pull::Up)); } else { - match (layout.rows, layout.cols) { - (2, 3) => { - // Mini and Revised Mini (2x3 = 6 keys) - let _ = row_pins.push(Output::new(PIN_2, Level::High)); - let _ = row_pins.push(Output::new(PIN_3, Level::High)); - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); - } - (2, 4) => { - // Stream Deck + / Neo (4x2 keys) - let _ = row_pins.push(Output::new(PIN_2, Level::High)); - let _ = row_pins.push(Output::new(PIN_3, Level::High)); - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); - } - (3, 5) => { - // 15 Keys Module (5x3) - let _ = row_pins.push(Output::new(PIN_2, Level::High)); - let _ = row_pins.push(Output::new(PIN_3, Level::High)); - let _ = row_pins.push(Output::new(PIN_7, Level::High)); - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); - } - (4, 8) => { - // 32 Keys Module (8x4) - let _ = row_pins.push(Output::new(PIN_2, Level::High)); - let _ = row_pins.push(Output::new(PIN_3, Level::High)); - let _ = row_pins.push(Output::new(PIN_7, Level::High)); - let _ = row_pins.push(Output::new(PIN_9, Level::High)); - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_12, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_13, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_16, Pull::Up)); - } - (4, 9) => { - // Stream Deck + XL (9x4) - let _ = row_pins.push(Output::new(PIN_2, Level::High)); - let _ = row_pins.push(Output::new(PIN_3, Level::High)); - let _ = row_pins.push(Output::new(PIN_7, Level::High)); - let _ = row_pins.push(Output::new(PIN_9, Level::High)); - let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_12, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_13, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_16, Pull::Up)); - let _ = col_pins.push(Input::new(PIN_22, Pull::Up)); - } - _ => core::panic!("no pin mapping for matrix {}×{}", layout.cols, layout.rows), + // Matrix pin assignments by layout (`module6` uses multicore wiring in `entry`, not here). + match (layout.rows, layout.cols) { + (2, 3) => { + // 2×3 matrix (six keys) + let _ = row_pins.push(Output::new(PIN_2, Level::High)); + let _ = row_pins.push(Output::new(PIN_3, Level::High)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + } + (2, 4) => { + // Stream Deck + (4×2 matrix; Neo uses direct GPIOs above) + let _ = row_pins.push(Output::new(PIN_2, Level::High)); + let _ = row_pins.push(Output::new(PIN_3, Level::High)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); + } + (3, 5) => { + // 15 Keys Module (5x3) + let _ = row_pins.push(Output::new(PIN_2, Level::High)); + let _ = row_pins.push(Output::new(PIN_3, Level::High)); + let _ = row_pins.push(Output::new(PIN_7, Level::High)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); } + (4, 8) => { + // 32 Keys Module (8x4) + let _ = row_pins.push(Output::new(PIN_2, Level::High)); + let _ = row_pins.push(Output::new(PIN_3, Level::High)); + let _ = row_pins.push(Output::new(PIN_7, Level::High)); + let _ = row_pins.push(Output::new(PIN_9, Level::High)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_12, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_13, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_16, Pull::Up)); + } + (4, 9) => { + // Stream Deck + XL (9x4) + let _ = row_pins.push(Output::new(PIN_2, Level::High)); + let _ = row_pins.push(Output::new(PIN_3, Level::High)); + let _ = row_pins.push(Output::new(PIN_7, Level::High)); + let _ = row_pins.push(Output::new(PIN_9, Level::High)); + let _ = col_pins.push(Input::new(PIN_4, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_5, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_10, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_11, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_12, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_13, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_16, Pull::Up)); + let _ = col_pins.push(Input::new(PIN_22, Pull::Up)); + } + _ => core::panic!("no pin mapping for matrix {}×{}", layout.cols, layout.rows), + } } (driver, usb_led, status_led, error_led, row_pins, col_pins) @@ -334,6 +339,16 @@ fn spawn_button_task_with_pins( mut col_pins: Vec, 32>, device: Device, ) -> Result<(), SpawnError> { + if device == Device::Neo { + let mut inputs: heapless::Vec, 32> = heapless::Vec::new(); + while let Some(pin) = col_pins.pop() { + let _ = inputs.push(pin); + } + core::debug_assert_eq!(inputs.len(), 8); + spawner.spawn(button_task_direct(inputs)?); + return Ok(()); + } + match crate::config::button_input_mode() { crate::config::ButtonInputMode::Matrix => { // Extract pins for matrix task based on device layout @@ -419,16 +434,6 @@ fn spawn_button_task_with_pins( while let Some(pin) = col_pins.pop() { let _ = inputs.push(pin); } - // Ensure Mini has exactly 6 inputs if possible - if matches!( - device, - Device::Mini | Device::RevisedMini | Device::MiniDiscord - ) && inputs.len() > 6 - { - while inputs.len() > 6 { - let _ = inputs.pop(); - } - } spawner.spawn(button_task_direct(inputs)?); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 4ae0dce..d995865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! using Embassy async framework on RP2040. //! //! ## Supported Devices -//! - StreamDeck Mini (6 keys, 80x80px) +//! - StreamDeck Module 6 Keys (6 keys, 80×80px BMP / Mini-family HID; `--bin module6`) //! - StreamDeck Original (15 keys, 72x72px) //! - StreamDeck Original V2 (15 keys, 72x72px, JPEG) //! - StreamDeck XL (32 keys, 96x96px, JPEG) diff --git a/src/protocol/v1.rs b/src/protocol/v1.rs index c9ce9f6..f00b54c 100644 --- a/src/protocol/v1.rs +++ b/src/protocol/v1.rs @@ -1,6 +1,6 @@ //! StreamDeck V1 Protocol Handler //! -//! Handles Original, Mini, and Revised Mini devices using BMP format +//! Handles Original (classic V1) devices using BMP format. use super::{ feature_report_clamp, feature_report_zero_prefix, fill_feature_rid_ascii, diff --git a/src/usb.rs b/src/usb.rs index f0270f6..2ca7029 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -21,11 +21,13 @@ use embassy_usb::class::hid::{ use embassy_usb::control::OutResponse; use embassy_usb::{Builder, Config}; +use crate::config::USB_COMMAND_CHANNEL_SIZE; + type UsbCommandSender = embassy_sync::channel::Sender< 'static, embassy_sync::blocking_mutex::raw::ThreadModeRawMutex, UsbCommand, - 4, + USB_COMMAND_CHANNEL_SIZE, >; /// Send assembled output-report payloads to the USB command channel (shared by HID paths). @@ -74,6 +76,17 @@ fn dispatch_output_report_result(result: OutputReportResult, sender: &UsbCommand } } +async fn dispatch_display_command(device: Device, cmd: DisplayCommand) { + if matches!(device, Device::Module6Keys) { + let _ = crate::channels::MULTICORE_IMAGE_CHANNEL + .sender() + .send(cmd) + .await; + } else { + let _ = DISPLAY_CHANNEL.sender().send(cmd).await; + } +} + // =================================================================== // USB Configuration // =================================================================== @@ -103,12 +116,7 @@ fn create_usb_config_for_device(device: Device) -> Config<'static> { struct StreamDeckHidHandler { protocol_handler: ProtocolHandler, - usb_command_sender: embassy_sync::channel::Sender< - 'static, - embassy_sync::blocking_mutex::raw::ThreadModeRawMutex, - UsbCommand, - 4, - >, + usb_command_sender: UsbCommandSender, } impl StreamDeckHidHandler { @@ -308,16 +316,11 @@ async fn usb_task_impl( match receiver.receive().await { UsbCommand::Reset => { info!("Processing reset command"); - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::ClearAll) - .await; + dispatch_display_command(device, DisplayCommand::ClearAll).await; } UsbCommand::SetBrightness(brightness) => { info!("Processing brightness command: {}%", brightness); - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::SetBrightness(brightness)) + dispatch_display_command(device, DisplayCommand::SetBrightness(brightness)) .await; } UsbCommand::ImageData { key_id, data } => { @@ -326,22 +329,15 @@ async fn usb_task_impl( key_id, data.len() ); - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::DisplayImage { key_id, data }) + dispatch_display_command(device, DisplayCommand::DisplayImage { key_id, data }) .await; } UsbCommand::FullScreenImage { data } => { - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::DisplayFullScreen { data }) + dispatch_display_command(device, DisplayCommand::DisplayFullScreen { data }) .await; } UsbCommand::WindowImage { data } => { - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::DisplayWindow { data }) - .await; + dispatch_display_command(device, DisplayCommand::DisplayWindow { data }).await; } UsbCommand::PartialWindowImage { x, @@ -363,16 +359,14 @@ async fn usb_task_impl( debug!("Background {} ({} bytes)", index, data.len()); } UsbCommand::FillLcdColor { r, g, b } => { - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::FillLcd { r, g, b }) - .await; + dispatch_display_command(device, DisplayCommand::FillLcd { r, g, b }).await; } UsbCommand::FillKeyColor { key_index, r, g, b } => { - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::FillKey { key_index, r, g, b }) - .await; + dispatch_display_command( + device, + DisplayCommand::FillKey { key_index, r, g, b }, + ) + .await; } UsbCommand::ShowBackgroundByIndex { index } => { debug!("Show background index {}", index);