From 1d2b0ae2732f410f2f62f59e5355169f1e250e70 Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 4 May 2026 09:35:40 +0900 Subject: [PATCH 1/4] feat(module6): TC-00 ST7789 display path and remove Mini firmware - Add optional ST7789 on SPI1 for module6 (--features display), BMP routing, and tighter channel/buffer sizing when display is enabled. - Extend Module 6 HID handling (chunk assembly, unit info) and USB dispatch. - Remove the standalone Mini binary, Device::Mini, and MiniOrModule6Direct multicore layout; runtime tag 0 is reserved/unused. - Update CI artifact list, build-devices.sh, README, and CLAUDE.md. Co-authored-by: Cursor --- .github/workflows/build.yml | 3 +- CLAUDE.md | 2 +- Cargo.lock | 53 ++++++++- Cargo.toml | 12 +-- README.md | 2 +- build-devices.sh | 6 +- src/bin/mini.rs | 22 ---- src/bin/module6.rs | 2 +- src/channels.rs | 20 +++- src/config.rs | 35 +++++- src/device/mod.rs | 21 +--- src/display_module6_st7789.rs | 195 ++++++++++++++++++++++++++++++++++ src/display_spi_dma.rs | 85 +++++++++++++++ src/entry.rs | 138 ++++++++++++++++++++---- src/hardware.rs | 10 +- src/lib.rs | 16 ++- src/protocol/module_6.rs | 146 +++++++++++++++++-------- src/usb.rs | 65 ++++++------ 18 files changed, 671 insertions(+), 162 deletions(-) delete mode 100644 src/bin/mini.rs create mode 100644 src/display_module6_st7789.rs create mode 100644 src/display_spi_dma.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1f6df7..99f7a43 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 revised-mini xl) missing=0 for bin in "${expected[@]}"; do if [ -f "target/thumbv6m-none-eabi/release/$bin" ]; then @@ -89,7 +89,6 @@ 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 diff --git a/CLAUDE.md b/CLAUDE.md index 063fa27..9f6ac9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`, `revised-mini`, `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..c8189c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.23.2" @@ -363,6 +369,24 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "display-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" + +[[package]] +name = "display-interface-spi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9ec30048b1955da2038fcc3c017f419ab21bb0001879d16c0a3749dc6b7a" +dependencies = [ + "byte-slice-cast", + "display-interface", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "document-features" version = "0.2.11" @@ -639,6 +663,16 @@ dependencies = [ "embedded-hal 1.0.0", ] +[[package]] +name = "embedded-hal-bus" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", +] + [[package]] name = "embedded-hal-bus" version = "0.3.0" @@ -990,6 +1024,19 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" +[[package]] +name = "mipidsi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e2bbd372d8ae9ccd0fc6eb4d91742b971ed8149968bbc623f025506989bd30" +dependencies = [ + "display-interface", + "embedded-graphics-core", + "embedded-hal 1.0.0", + "heapless 0.8.0", + "nb 1.1.0", +] + [[package]] name = "nb" version = "0.1.3" @@ -1226,6 +1273,7 @@ dependencies = [ "defmt", "defmt-rtt", "defmt-test", + "display-interface-spi", "embassy-executor", "embassy-futures", "embassy-rp", @@ -1233,11 +1281,14 @@ dependencies = [ "embassy-time", "embassy-usb", "embedded-graphics", + "embedded-graphics-core", "embedded-hal 1.0.0", "embedded-hal-async", - "embedded-hal-bus", + "embedded-hal-bus 0.1.0", + "embedded-hal-bus 0.3.0", "fixed", "heapless 0.9.2", + "mipidsi", "nb 1.1.0", "panic-halt", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index eed8ee6..6e328d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,11 +28,15 @@ critical-section = "1.0" embedded-hal = "1.0" embedded-hal-async = "1.0" embedded-hal-bus = { version = "0.3", features = ["async"] } +embedded-hal-bus-sync = { package = "embedded-hal-bus", version = "0.1", optional = true } portable-atomic = { version = "1.0", features = ["critical-section"] } # Display and graphics st7735-lcd = "0.10" embedded-graphics = "0.8" +embedded-graphics-core = "0.4" +display-interface-spi = { version = "0.5", optional = true } +mipidsi = { version = "0.8", default-features = false, optional = true, features = ["batch"] } # USB HID usbd-hid = "0.10" @@ -66,12 +70,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" @@ -140,3 +138,5 @@ bench = false [features] default = [] +# TC-00 ST7789 + larger Module 6 BMP buffers (use only with `--bin module6 --features display`) +display = ["dep:mipidsi", "dep:display-interface-spi", "dep:embedded-hal-bus-sync"] 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..3de2423 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=("revised-mini" "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 revised-mini # Run Mini 2022 firmware" echo " cargo run --release --bin xl # Run XL firmware" - echo " ./build-devices.sh mini # Build only Mini firmware" + echo " ./build-devices.sh revised-mini # Build only Mini 2022 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/channels.rs b/src/channels.rs index c3d0d84..97fb953 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,19 @@ 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< + ThreadModeRawMutex, + DisplayCommand, + DISPLAY_CHANNEL_CAPACITY, +> = 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..31a7c98 100644 --- a/src/config.rs +++ b/src/config.rs @@ -203,12 +203,43 @@ 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 + +/// BMP / USB image payload capacity (`display` enables full Module 6 key BMP assembly). +#[cfg(feature = "display")] +pub const IMAGE_BUFFER_SIZE: usize = 20480; +#[cfg(not(feature = "display"))] +pub const IMAGE_BUFFER_SIZE: usize = 1024; // 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`] (large payloads when `display`). +#[cfg(feature = "display")] +pub const MULTICORE_CHANNEL_SIZE: usize = 1; +#[cfg(not(feature = "display"))] +pub const MULTICORE_CHANNEL_SIZE: usize = 8; + +/// Raw BMP accumulation cap for Module 6 chunked uploads (≥ full 80×80 24 bpp BMP). +#[cfg(feature = "display")] +pub const MODULE6_BMP_CAP: usize = 20480; +#[cfg(not(feature = "display"))] +pub const MODULE6_BMP_CAP: usize = 1024; + +/// Depth of [`crate::channels::USB_COMMAND_CHANNEL`] (large `UsbCommand` payloads when `display`). +#[cfg(feature = "display")] +pub const USB_COMMAND_CHANNEL_SIZE: usize = 1; +#[cfg(not(feature = "display"))] +pub const USB_COMMAND_CHANNEL_SIZE: usize = 4; + +/// Depth of [`crate::channels::DISPLAY_CHANNEL`] (RAM-heavy when `IMAGE_BUFFER_SIZE` is large). +#[cfg(feature = "display")] +pub const DISPLAY_CHANNEL_CAPACITY: usize = 1; +#[cfg(not(feature = "display"))] +pub const DISPLAY_CHANNEL_CAPACITY: usize = 8; + +/// TC-00 / Iryx ST7789 SPI clock (matches vmix-pico-controller TC00). +pub const MODULE6_SPI_FREQUENCY: u32 = 50_000_000; // =================================================================== // Power Management: Idle Time (Sleep Mode) diff --git a/src/device/mod.rs b/src/device/mod.rs index 46a7f9e..ff62cdf 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. +//! - **Legacy / Mini family**: Mini 2022, Mini Discord, 6-key Module — distinct report layout. //! - **Main / Expanded family**: Classic, XL, Neo, Plus, Plus XL, 15/32-key Modules — see General Reference. pub mod neo; @@ -134,8 +134,7 @@ pub trait DeviceConfig { #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Device { - Mini = 0, - /// Mini 2022 (Elgato PID 0x0090) + /// Mini 2022 (Elgato PID 0x0090). Runtime tag `0` was the removed Mini firmware and is unused. RevisedMini = 1, /// Mini Discord (0x00B3) MiniDiscord = 2, @@ -170,7 +169,6 @@ impl Device { return None; } match tag { - 0 => Some(Device::Mini), 1 => Some(Device::RevisedMini), 2 => Some(Device::MiniDiscord), 3 => Some(Device::Original), @@ -191,7 +189,6 @@ impl Device { pub fn pid(&self) -> u16 { match self { - Device::Mini => 0x0063, Device::RevisedMini => 0x0090, Device::MiniDiscord => 0x00B3, Device::Original => 0x0060, @@ -221,7 +218,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 => { + Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { (2u8, 3u8, 80u16, 80u16, 320u16, 240u16) } Device::Original => (3u8, 5u8, 72u16, 72u16, 480u16, 272u16), @@ -271,7 +268,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", @@ -298,7 +294,7 @@ impl DeviceConfig for Device { fn button_layout(&self) -> ButtonLayout { match self { - Device::Mini | Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { + Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { ButtonLayout::new(3, 2, true) } Device::Module15Keys | Device::OriginalV2 | Device::Mk2 | Device::Mk2ScissorKeys => { @@ -313,7 +309,7 @@ impl DeviceConfig for Device { fn display_config(&self) -> DisplayConfig { match self { - Device::Mini | Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { + Device::RevisedMini | Device::MiniDiscord | Device::Module6Keys => { DisplayConfig { image_width: 80, image_height: 80, @@ -378,13 +374,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, diff --git a/src/display_module6_st7789.rs b/src/display_module6_st7789.rs new file mode 100644 index 0000000..a51d261 --- /dev/null +++ b/src/display_module6_st7789.rs @@ -0,0 +1,195 @@ +//! Core 1 ST7789 pipeline for Module 6 + TC-00 wiring (`--features display`). + +use crate::channels::MULTICORE_IMAGE_CHANNEL; +use crate::device::{Device, DeviceConfig}; +use crate::display_spi_dma::DisplaySpiBus; +use crate::protocol::image::rotate_270; +use crate::types::DisplayCommand; +use defmt::{info, warn}; +use display_interface_spi::SPIInterface; +use embassy_rp::gpio::Output; +use embassy_rp::peripherals::SPI1; +use embassy_rp::spi::{Async, Spi}; +use embassy_time::Delay; +use embedded_graphics::primitives::{Primitive, PrimitiveStyleBuilder}; +use embedded_graphics_core::geometry::{Point, Size}; +use embedded_graphics_core::pixelcolor::{Rgb565, Rgb888, RgbColor}; +use embedded_graphics_core::prelude::*; +use embedded_graphics_core::primitives::Rectangle; +use embedded_hal_bus_sync::spi::ExclusiveDevice; +use mipidsi::models::ST7789; +use mipidsi::options::{ColorInversion, ColorOrder, Orientation}; +use mipidsi::Builder; + +type Module6Display = mipidsi::Display< + SPIInterface< + ExclusiveDevice, Output<'static>, Delay>, + Output<'static>, + >, + ST7789, + Output<'static>, +>; + +fn bmp_rgb888_80(buf: &[u8], dst: &mut [u8; 80 * 80 * 3]) -> Result<(), ()> { + if buf.len() < 54 { + return Err(()); + } + if buf[0] != b'B' || buf[1] != b'M' { + return Err(()); + } + let offset = u32::from_le_bytes(buf[10..14].try_into().map_err(|_| ())?) as usize; + let w = i32::from_le_bytes(buf[18..22].try_into().map_err(|_| ())?).unsigned_abs(); + let h_raw = i32::from_le_bytes(buf[22..26].try_into().map_err(|_| ())?); + let h = h_raw.unsigned_abs(); + let bpp = u16::from_le_bytes(buf[28..30].try_into().map_err(|_| ())?) as usize; + if w != 80 || h != 80 || bpp != 24 { + return Err(()); + } + let row_stride = ((w as usize * bpp / 8) + 3) & !3; + let top_down = h_raw < 0; + for row in 0..80usize { + let src_row = if top_down { row } else { 79 - row }; + let src_off = offset + src_row * row_stride; + if src_off + 240 > buf.len() { + return Err(()); + } + let dst_off = row * 240; + for col in 0..80usize { + let s = src_off + col * 3; + let d = dst_off + col * 3; + let b = buf[s]; + let g = buf[s + 1]; + let r = buf[s + 2]; + dst[d] = r; + dst[d + 1] = g; + dst[d + 2] = b; + } + } + Ok(()) +} + +fn key_origin_px(key_id: u8) -> (i32, i32) { + let k = (key_id as usize).min(5); + let col = k % 3; + let row = k / 3; + ((col * 80) as i32, (row * 80) as i32) +} + +fn fill_rect(display: &mut Module6Display, x: i32, y: i32, w: u32, h: u32, c: Rgb565) { + let rect = Rectangle::new(Point::new(x, y), Size::new(w, h)).into_styled( + PrimitiveStyleBuilder::new().fill_color(c).build(), + ); + let _ = rect.draw(display); +} + +fn draw_px_grid(display: &mut Module6Display, ox: i32, oy: i32, px: &[u8]) { + let it = (0..80i32).flat_map(|yy| { + (0..80i32).filter_map(move |xx| { + let i = ((yy as usize) * 80 + (xx as usize)) * 3; + if i + 2 >= px.len() { + return None; + } + let r = px[i]; + let g = px[i + 1]; + let b = px[i + 2]; + let c = Rgb565::from(Rgb888::new(r, g, b)); + Some(Pixel(Point::new(ox + xx, oy + yy), c)) + }) + }); + let _ = display.draw_iter(it); +} + +fn draw_key_bmp(display: &mut Module6Display, device: Device, key_id: u8, bmp: &[u8]) { + let dc = device.display_config(); + let mut rgb = [0u8; 80 * 80 * 3]; + if bmp_rgb888_80(bmp, &mut rgb).is_err() { + warn!("module6 display: invalid BMP for key {}", key_id); + return; + } + + let (ox, oy) = key_origin_px(key_id); + if dc.needs_rotation { + let rotated = rotate_270(&rgb, 80, 80); + draw_px_grid(display, ox, oy, rotated.as_slice()); + } else { + draw_px_grid(display, ox, oy, rgb.as_slice()); + } +} + +fn handle_display_cmd( + display: &mut Module6Display, + device: Device, + backlight: &mut Output<'static>, + cmd: DisplayCommand, +) { + match cmd { + DisplayCommand::ClearAll => { + for kid in 0u8..6 { + let (x, y) = key_origin_px(kid); + fill_rect(display, x, y, 80, 80, Rgb565::BLACK); + } + } + DisplayCommand::Clear(key_id) => { + let (x, y) = key_origin_px(key_id); + fill_rect(display, x, y, 80, 80, Rgb565::BLACK); + } + DisplayCommand::SetBrightness(pct) => { + info!("backlight {}%", pct); + if pct > 4 { + backlight.set_high(); + } else { + backlight.set_low(); + } + } + DisplayCommand::DisplayImage { key_id, data } => { + draw_key_bmp(display, device, key_id, data.as_slice()); + } + DisplayCommand::FillLcd { r, g, b } => { + let c = Rgb565::from(Rgb888::new(r, g, b)); + fill_rect(display, 0, 0, 240, 320, c); + } + DisplayCommand::FillKey { + key_index, + r, + g, + b, + } => { + let c = Rgb565::from(Rgb888::new(r, g, b)); + let (x, y) = key_origin_px(key_index); + fill_rect(display, x, y, 80, 80, c); + } + DisplayCommand::DisplayFullScreen { .. } | DisplayCommand::DisplayWindow { .. } => {} + } +} + +#[embassy_executor::task] +pub async fn module6_st7789_core1_task( + device: Device, + spi: Spi<'static, SPI1, Async>, + cs: Output<'static>, + dc: Output<'static>, + rst: Output<'static>, + mut backlight: Output<'static>, +) { + backlight.set_high(); + + let spi_bus = DisplaySpiBus(spi); + let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay); + let di = SPIInterface::new(spi_dev, dc); + let mut display: Module6Display = Builder::new(ST7789, di) + .display_size(240, 320) + .orientation(Orientation::new()) + .reset_pin(rst) + .color_order(ColorOrder::Rgb) + .invert_colors(ColorInversion::Inverted) + .init(&mut Delay) + .expect("ST7789 init"); + + display.clear(Rgb565::BLACK).expect("clear"); + + let recv = MULTICORE_IMAGE_CHANNEL.receiver(); + loop { + let cmd = recv.receive().await; + handle_display_cmd(&mut display, device, &mut backlight, cmd); + } +} diff --git a/src/display_spi_dma.rs b/src/display_spi_dma.rs new file mode 100644 index 0000000..82f03f4 --- /dev/null +++ b/src/display_spi_dma.rs @@ -0,0 +1,85 @@ +//! SPI bus wrapper so [`embedded_hal::spi::SpiBus`] writes use RP2040 DMA for bulk TX (TC-00 ST7789). + +const MIN_DMA_TX_LEN: usize = 64; + +use core::future::Future; +use core::pin::pin; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + +use cortex_m::asm::wfe; +use embassy_rp::spi::{Async, Instance, Spi}; +use embedded_hal::spi::SpiBus; + +unsafe fn waker_clone(p: *const ()) -> RawWaker { + RawWaker::new(p, &FLAG_WAKER_VTABLE) +} +unsafe fn waker_wake(p: *const ()) { + (*(p as *const AtomicBool)).store(true, Ordering::Release); +} +unsafe fn waker_wake_by_ref(p: *const ()) { + waker_wake(p); +} +unsafe fn waker_drop(_: *const ()) {} + +static FLAG_WAKER_VTABLE: RawWakerVTable = + RawWakerVTable::new(waker_clone, waker_wake, waker_wake_by_ref, waker_drop); + +fn waker_flag(flag: &AtomicBool) -> Waker { + unsafe { Waker::from_raw(RawWaker::new(flag as *const _ as *const (), &FLAG_WAKER_VTABLE)) } +} + +fn block_on_dma_write( + spi: &mut Spi<'_, T, Async>, + words: &[u8], +) -> Result<(), embassy_rp::spi::Error> { + let done = AtomicBool::new(false); + let waker = waker_flag(&done); + let mut cx = Context::from_waker(&waker); + let mut fut = pin!(spi.write(words)); + loop { + match fut.as_mut().poll(&mut cx) { + Poll::Ready(r) => return r, + Poll::Pending => loop { + if done.load(Ordering::Acquire) { + done.store(false, Ordering::Release); + break; + } + wfe(); + }, + } + } +} + +/// SPI bus for ST7789: TX uses DMA for longer bursts. +pub struct DisplaySpiBus<'d, T: Instance>(pub Spi<'d, T, Async>); + +impl embedded_hal::spi::ErrorType for DisplaySpiBus<'_, T> { + type Error = embassy_rp::spi::Error; +} + +impl SpiBus for DisplaySpiBus<'_, T> { + fn flush(&mut self) -> Result<(), Self::Error> { + self.0.flush() + } + + fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.0.blocking_read(words) + } + + fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { + if words.len() < MIN_DMA_TX_LEN { + self.0.blocking_write(words) + } else { + block_on_dma_write(&mut self.0, words) + } + } + + fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { + self.0.blocking_transfer(read, write) + } + + fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.0.blocking_transfer_in_place(words) + } +} diff --git a/src/entry.rs b/src/entry.rs index 2df68f7..de2d0a5 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. @@ -89,12 +80,123 @@ pub enum MulticoreCore1Buffer { B16384, } +/// TC-00 ST7789 on Core 1 + Module 6 buttons (`--features display`). +#[cfg(feature = "display")] +fn run_multicore_module6_tc00_display(device: Device, _core1_buf: MulticoreCore1Buffer) -> ! { + let embassy_rp::Peripherals { + FLASH, + CORE1, + USB, + SPI1, + DMA_CH2, + DMA_CH3, + PIN_1, + PIN_2, + PIN_3, + PIN_4, + PIN_9, + PIN_10, + PIN_11, + PIN_12, + PIN_13, + PIN_14, + PIN_20, + PIN_21, + PIN_22, + PIN_25, + PIN_27, + PIN_28, + .. + } = embassy_rp::init(Default::default()); + + config::init_runtime_device(device); + config::init_usb_serial_from_flash(FLASH); + + let supervisor = AppSupervisor::new_for_device(device); + supervisor.print_startup_banner(); + + spawn_core1( + CORE1, + unsafe { &mut *core::ptr::addr_of_mut!(CORE1_STACK) }, + move || { + let executor1 = EXECUTOR1.init(Executor::new()); + let mut spi_cfg = embassy_rp::spi::Config::default(); + spi_cfg.frequency = crate::config::MODULE6_SPI_FREQUENCY; + let spi = embassy_rp::spi::Spi::new( + SPI1, + PIN_14, + PIN_11, + PIN_12, + DMA_CH2, + DMA_CH3, + crate::DisplayDmaIrqs, + spi_cfg, + ); + let cs = Output::new(PIN_13, Level::High); + let dc = Output::new(PIN_22, Level::Low); + let rst = Output::new(PIN_27, Level::High); + let bl = Output::new(PIN_28, Level::High); + + executor1.run(|spawner| { + unwrap!( + crate::display_module6_st7789::module6_st7789_core1_task( + device, spi, cs, dc, rst, bl, + ) + .map(|t| spawner.spawn(t)) + ); + }); + }, + ); + + let executor0 = EXECUTOR0.init(Executor::new()); + executor0.run(|spawner| { + unwrap!(multicore_core0_supervisor_task(supervisor).map(|t| spawner.spawn(t))); + unwrap!( + usb::usb_task_for_device( + Driver::new(USB, crate::Irqs), + Output::new(PIN_20, Level::Low), + device, + ) + .map(|t| spawner.spawn(t)) + ); + unwrap!( + buttons::button_task_direct({ + let mut inputs = heapless::Vec::new(); + let _ = inputs.push(Input::new(PIN_1, Pull::Up)); + let _ = inputs.push(Input::new(PIN_2, Pull::Up)); + let _ = inputs.push(Input::new(PIN_3, Pull::Up)); + let _ = inputs.push(Input::new(PIN_4, Pull::Up)); + let _ = inputs.push(Input::new(PIN_9, Pull::Up)); + let _ = inputs.push(Input::new(PIN_10, Pull::Up)); + inputs + }) + .map(|t| spawner.spawn(t)) + ); + unwrap!( + hardware::status_task( + Output::new(PIN_25, Level::Low), + Output::new(PIN_21, Level::Low), + ) + .map(|t| spawner.spawn(t)) + ); + }); + + loop { + cortex_m::asm::wfe(); + } +} + /// Multicore bring-up: core 1 runs display stub; core 0 runs USB, buttons, supervisor. pub fn run_multicore( device: Device, layout: MulticoreCore0Layout, core1_buf: MulticoreCore1Buffer, ) -> ! { + #[cfg(feature = "display")] + if matches!(layout, MulticoreCore0Layout::Module6DirectTc00) { + run_multicore_module6_tc00_display(device, core1_buf); + } + let p = embassy_rp::init(Default::default()); config::init_runtime_device(device); config::init_usb_serial_from_flash(p.FLASH); @@ -122,7 +224,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 +233,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 +328,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..7e2e44c 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -121,8 +121,7 @@ pub async fn init_hardware_tasks_core0( // 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::RevisedMini | crate::device::Device::MiniDiscord ) { crate::config::set_button_input_mode(crate::config::ButtonInputMode::Direct); @@ -178,8 +177,7 @@ async fn init_hardware_tasks_with_config( let device = hw_config.device; if matches!( device, - crate::device::Device::Mini - | crate::device::Device::RevisedMini + crate::device::Device::RevisedMini | crate::device::Device::MiniDiscord ) { crate::config::set_button_input_mode(crate::config::ButtonInputMode::Direct); @@ -250,7 +248,7 @@ fn create_all_pins_for_device( crate::config::ButtonInputMode::Direct ) && matches!( device, - Device::Mini | Device::RevisedMini | Device::MiniDiscord + 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)); @@ -422,7 +420,7 @@ fn spawn_button_task_with_pins( // Ensure Mini has exactly 6 inputs if possible if matches!( device, - Device::Mini | Device::RevisedMini | Device::MiniDiscord + Device::RevisedMini | Device::MiniDiscord ) && inputs.len() > 6 { while inputs.len() > 6 { diff --git a/src/lib.rs b/src/lib.rs index 4ae0dce..eb085b4 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 Mini 2022 / Mini Discord / Module 6 Keys (6 keys, 80×80px BMP family) //! - StreamDeck Original (15 keys, 72x72px) //! - StreamDeck Original V2 (15 keys, 72x72px, JPEG) //! - StreamDeck XL (32 keys, 96x96px, JPEG) @@ -21,6 +21,15 @@ use embassy_rp::usb::InterruptHandler; use embassy_rp::{bind_interrupts, peripherals}; +#[cfg(feature = "display")] +bind_interrupts!(pub struct DisplayDmaIrqs { + DMA_IRQ_0 => embassy_rp::dma::InterruptHandler, + embassy_rp::dma::InterruptHandler, + embassy_rp::dma::InterruptHandler, + embassy_rp::dma::InterruptHandler, + embassy_rp::dma::InterruptHandler; +}); + // Export all modules for use by device-specific binaries pub mod buttons; pub mod channels; @@ -34,6 +43,11 @@ pub mod supervisor; pub mod types; pub mod usb; +#[cfg(feature = "display")] +pub mod display_spi_dma; +#[cfg(feature = "display")] +pub mod display_module6_st7789; + // USB interrupt binding - shared by all binaries bind_interrupts!(pub struct Irqs { USBCTRL_IRQ => InterruptHandler; diff --git a/src/protocol/module_6.rs b/src/protocol/module_6.rs index 4b5842a..3875b31 100644 --- a/src/protocol/module_6.rs +++ b/src/protocol/module_6.rs @@ -1,22 +1,52 @@ -//! StreamDeck Module HID Protocol Handler (6keys) +//! StreamDeck Module HID Protocol Handler (6 keys) //! -//! Implements the unified `ProtocolHandlerTrait` for the Elgato Stream Deck -//! Modules per public HID API docs. Image upload parsing is stubbed until we -//! confirm exact chunk layout from PCAPs. +//! Legacy Mini-family protocol per Elgato docs: +//! https://docs.elgato.com/streamdeck/hid/mini use super::{ feature_report_clamp, feature_report_zero_prefix, fill_feature_rid_ascii, map_buttons_grid, ButtonMapping, OutputReportResult, ProtocolHandlerTrait, }; +use crate::config::MODULE6_BMP_CAP; use crate::device::ProtocolVersion; use crate::protocol::module::{FirmwareType, ModuleGetCommand, ModuleSetCommand}; +use heapless::Vec; #[derive(Debug)] -pub struct Module6KeysHandler {} +pub struct Module6KeysHandler { + image_buffer: Vec, + receiving: bool, + expected_key: u8, +} impl Module6KeysHandler { pub fn new() -> Self { - Self {} + Self { + image_buffer: Vec::new(), + receiving: false, + expected_key: 0, + } + } + + fn reset_rx(&mut self) { + self.image_buffer.clear(); + self.receiving = false; + self.expected_key = 0; + } + + /// BMP file size from `bfSize` once `BM` magic is present. + fn bmp_total_bytes(buf: &[u8]) -> Option { + if buf.len() < 6 { + return None; + } + if buf[0] != b'B' || buf[1] != b'M' { + return None; + } + let bf_size = u32::from_le_bytes(buf[2..6].try_into().ok()?) as usize; + if bf_size > MODULE6_BMP_CAP || bf_size < 54 { + return None; + } + Some(bf_size) } } @@ -25,12 +55,11 @@ impl Default for Module6KeysHandler { Self::new() } } + impl Module6KeysHandler { fn parse_module_set_command(&self, report_id: u8, data: &[u8]) -> Option { match report_id { 0x05 => { - // Payload excludes Report ID. Per spec: - // [Command=0x55, 0xAA, 0xD1, 0x01, Brightness] if data.len() >= 5 && data[0] == 0x55 && data[1] == 0xAA @@ -43,12 +72,9 @@ impl Module6KeysHandler { } } 0x0B => { - // Payload excludes Report ID. - // Commands at data[0] if !data.is_empty() { match data[0] { 0x63 => { - // data[1]: 0x00 Show Logo, 0x02 Update Boot Logo if data.len() >= 2 { match data[1] { 0x00 => Some(ModuleSetCommand::ShowLogo), @@ -63,7 +89,6 @@ impl Module6KeysHandler { } } 0xA2 => { - // data[1..=4]: i32 seconds (LE) if data.len() >= 5 { let secs = i32::from_le_bytes([data[1], data[2], data[3], data[4]]); Some(ModuleSetCommand::SetIdleTime { seconds: secs }) @@ -88,7 +113,7 @@ impl Module6KeysHandler { 0xA2 => Some(ModuleGetCommand::GetFirmwareVersion(FirmwareType::AP1)), 0x03 => Some(ModuleGetCommand::GetUnitSerialNumber), 0xA3 => Some(ModuleGetCommand::GetIdleTime), - 0x08 => Some(ModuleGetCommand::GetUnitInformation), // Module 6 compatibility + 0x08 => Some(ModuleGetCommand::GetUnitInformation), _ => None, } } @@ -114,26 +139,55 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn parse_output_report(&mut self, data: &[u8]) -> OutputReportResult { - let report_id = data[0]; - let command = data[1]; + // Upload Data to Image Memory Bank — Report ID 0x02, Command 0x01. + // Layout: [0]=RID 0x02, [1]=Cmd 0x01, [2]=chunk idx, [3]=0x00, [4]=show flag, + // [5]=key idx, [6..0x10]=reserved, [0x10..]=payload. + if data.len() < 17 { + return OutputReportResult::Unhandled; + } + if data[0] != 0x02 || data[1] != 0x01 { + return OutputReportResult::Unhandled; + } - match report_id { - // https://docs.elgato.com/streamdeck/hid/module-6#upload-data-to-image-memory-bank - 0x02 => { - if command == 0x01 { - let _chunk_index = data[2]; - let _reserved = data[3]; - let _show_image_flag = data[4]; - let _key_index = data[5]; - let _reserved = &data[6..0x10]; - let _chunk_data = &data[0x10..]; - - OutputReportResult::Unhandled - } else { - OutputReportResult::Unhandled - } - } - _ => OutputReportResult::Unhandled, + let chunk_idx = data[2]; + let key_idx = data[5]; + let chunk_payload = data.get(0x10..).unwrap_or(&[]); + + if chunk_idx == 0 { + self.reset_rx(); + self.receiving = true; + self.expected_key = key_idx; + } else if !self.receiving || key_idx != self.expected_key { + self.reset_rx(); + return OutputReportResult::Unhandled; + } + + if self.image_buffer.extend_from_slice(chunk_payload).is_err() { + self.reset_rx(); + return OutputReportResult::Unhandled; + } + + let Some(total) = Self::bmp_total_bytes(&self.image_buffer) else { + return OutputReportResult::Unhandled; + }; + + if self.image_buffer.len() < total { + return OutputReportResult::Unhandled; + } + + let mut image = Vec::new(); + let slice = self.image_buffer.as_slice().get(..total).unwrap_or(&[]); + if image.extend_from_slice(slice).is_err() { + self.reset_rx(); + return OutputReportResult::Unhandled; + } + + let completed_key = self.expected_key; + self.reset_rx(); + + OutputReportResult::KeyImageComplete { + key_id: completed_key, + image, } } @@ -148,8 +202,6 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn hid_descriptor(&self) -> &'static [u8] { - // Minimal descriptor covering Input(0x01), Output(0x02), Feature(0x03/0x04/0x05/0x07/0x08/0x0B/0xA0/0xA1/0xA2/0xA3) - // This can be fine-tuned to match exact real devices if needed. const DESC: &[u8] = &[ 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) @@ -162,7 +214,7 @@ impl ProtocolHandlerTrait for Module6KeysHandler { 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8) - 0x95, 0x3F, // Report Count (63) -> total 64 bytes incl. Report ID + 0x95, 0x3F, // Report Count (63) 0x81, 0x02, // Input (Data,Var,Abs) // Output report 0x02 (image/data chunks) 0x85, 0x02, // Report ID 0x02 @@ -184,7 +236,7 @@ impl ProtocolHandlerTrait for Module6KeysHandler { 0x85, 0xA1, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0x85, 0xA2, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0x85, 0xA3, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, - 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0xC0, // End Collection + 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0xC0, ]; DESC } @@ -194,23 +246,19 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn format_button_report(&self, buttons: &ButtonMapping, report: &mut [u8]) -> usize { - // 64 bytes total per packet: Report ID (1) + 63 data bytes const MAX_USB_SIZE: usize = 64; if report.len() < MAX_USB_SIZE { return 0; } - // Set Report ID report[0] = 0x01; - // Map up to 63 data bytes; Module 6 needs first 6 let button_count = core::cmp::min(6, buttons.mapped_buttons.len()); for i in 0..button_count { report[1 + i] = if buttons.mapped_buttons[i] { 1 } else { 0 }; } - // Zero out remaining bytes in the USB packet report .iter_mut() .take(MAX_USB_SIZE) @@ -221,10 +269,7 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn handle_feature_report(&mut self, report_id: u8, data: &[u8]) -> Option { - if let Some(cmd) = self.parse_module_set_command(report_id, data) { - return Some(cmd); - } - None + self.parse_module_set_command(report_id, data) } fn get_feature_report(&mut self, report_id: u8, buf: &mut [u8]) -> Option { @@ -260,7 +305,18 @@ impl Module6KeysHandler { buf[5] = le[3]; Some(cap) } - _ => None, + ModuleGetCommand::GetUnitInformation => { + let tail = crate::device::Device::Module6Keys.unit_information_tail(); + let cap = feature_report_clamp(total_len, buf.len()); + if cap < 5 + tail.len() { + return None; + } + feature_report_zero_prefix(buf, cap); + buf[0] = 0x08; + buf[1..5].copy_from_slice(&[0u8; 4]); + buf[5..5 + tail.len()].copy_from_slice(&tail); + Some(cap) + } } } else { None diff --git a/src/usb.rs b/src/usb.rs index f0270f6..439a296 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,18 @@ async fn usb_task_impl( key_id, data.len() ); - let _ = DISPLAY_CHANNEL - .sender() - .send(DisplayCommand::DisplayImage { key_id, data }) - .await; + 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 +362,16 @@ 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); From 37140a9126cc273423ae06f1e9a73e2452268270 Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 4 May 2026 09:40:22 +0900 Subject: [PATCH 2/4] refactor: drop revised-mini binary and Mini Discord SKUs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove revised_mini firmware target; Mini-family hosts use module6 only. - Delete Device::RevisedMini and Device::MiniDiscord (runtime tags 0–2 unused). - Simplify hardware pin init (no Mini direct-path overrides). - Refresh build-devices.sh, CI artifacts list, docs, and lib crate docs. - Apply rustfmt; satisfy clippy (module_6 BMP size range check). Co-authored-by: Cursor --- .github/workflows/build.yml | 3 +- CLAUDE.md | 4 +- Cargo.toml | 6 -- build-devices.sh | 6 +- src/bin/revised_mini.rs | 17 ---- src/channels.rs | 7 +- src/device/mod.rs | 53 +++-------- src/display_module6_st7789.rs | 12 +-- src/display_spi_dma.rs | 7 +- src/entry.rs | 62 ++++++------- src/hardware.rs | 167 +++++++++++++--------------------- src/lib.rs | 6 +- src/protocol/module_6.rs | 2 +- src/protocol/v1.rs | 2 +- src/usb.rs | 17 ++-- 15 files changed, 129 insertions(+), 242 deletions(-) delete mode 100644 src/bin/revised_mini.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99f7a43..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=(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 @@ -95,7 +95,6 @@ jobs: 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 9f6ac9c..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 (`module6`, `revised-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.toml b/Cargo.toml index 6e328d2..ae44325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,12 +70,6 @@ debug = 2 incremental = false opt-level = "z" -[[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/build-devices.sh b/build-devices.sh index 3de2423..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=("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 revised-mini # Run Mini 2022 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 revised-mini # Build only Mini 2022 firmware" + echo " ./build-devices.sh module6 # Build only Module 6 firmware" fi \ No newline at end of file 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 97fb953..b5133c6 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -18,11 +18,8 @@ pub static USB_COMMAND_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< diff --git a/src/device/mod.rs b/src/device/mod.rs index ff62cdf..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 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,10 +134,7 @@ pub trait DeviceConfig { #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Device { - /// Mini 2022 (Elgato PID 0x0090). Runtime tag `0` was the removed Mini firmware and is unused. - 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) @@ -169,8 +166,6 @@ impl Device { return None; } match tag { - 1 => Some(Device::RevisedMini), - 2 => Some(Device::MiniDiscord), 3 => Some(Device::Original), 4 => Some(Device::OriginalV2), 5 => Some(Device::Mk2), @@ -189,8 +184,6 @@ impl Device { pub fn pid(&self) -> u16 { match self { - Device::RevisedMini => 0x0090, - Device::MiniDiscord => 0x00B3, Device::Original => 0x0060, Device::OriginalV2 => 0x006d, Device::Mk2 => 0x0080, @@ -218,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::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]; @@ -268,8 +259,6 @@ impl Device { impl DeviceConfig for Device { fn device_name(&self) -> &'static str { match self { - Device::RevisedMini => "StreamDeck Mini 2022", - Device::MiniDiscord => "StreamDeck Mini Discord", Device::Original => "StreamDeck Original", Device::OriginalV2 => "StreamDeck Classic (2019)", Device::Mk2 => "StreamDeck Mk.2", @@ -294,9 +283,7 @@ impl DeviceConfig for Device { fn button_layout(&self) -> ButtonLayout { match self { - 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) } @@ -309,16 +296,14 @@ impl DeviceConfig for Device { fn display_config(&self) -> DisplayConfig { match self { - 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, @@ -374,20 +359,6 @@ impl DeviceConfig for Device { fn usb_config(&self) -> UsbConfig { match self { - 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/display_module6_st7789.rs b/src/display_module6_st7789.rs index a51d261..62775c0 100644 --- a/src/display_module6_st7789.rs +++ b/src/display_module6_st7789.rs @@ -76,9 +76,8 @@ fn key_origin_px(key_id: u8) -> (i32, i32) { } fn fill_rect(display: &mut Module6Display, x: i32, y: i32, w: u32, h: u32, c: Rgb565) { - let rect = Rectangle::new(Point::new(x, y), Size::new(w, h)).into_styled( - PrimitiveStyleBuilder::new().fill_color(c).build(), - ); + let rect = Rectangle::new(Point::new(x, y), Size::new(w, h)) + .into_styled(PrimitiveStyleBuilder::new().fill_color(c).build()); let _ = rect.draw(display); } @@ -148,12 +147,7 @@ fn handle_display_cmd( let c = Rgb565::from(Rgb888::new(r, g, b)); fill_rect(display, 0, 0, 240, 320, c); } - DisplayCommand::FillKey { - key_index, - r, - g, - b, - } => { + DisplayCommand::FillKey { key_index, r, g, b } => { let c = Rgb565::from(Rgb888::new(r, g, b)); let (x, y) = key_origin_px(key_index); fill_rect(display, x, y, 80, 80, c); diff --git a/src/display_spi_dma.rs b/src/display_spi_dma.rs index 82f03f4..07dcaf1 100644 --- a/src/display_spi_dma.rs +++ b/src/display_spi_dma.rs @@ -26,7 +26,12 @@ static FLAG_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(waker_clone, waker_wake, waker_wake_by_ref, waker_drop); fn waker_flag(flag: &AtomicBool) -> Waker { - unsafe { Waker::from_raw(RawWaker::new(flag as *const _ as *const (), &FLAG_WAKER_VTABLE)) } + unsafe { + Waker::from_raw(RawWaker::new( + flag as *const _ as *const (), + &FLAG_WAKER_VTABLE, + )) + } } fn block_on_dma_write( diff --git a/src/entry.rs b/src/entry.rs index de2d0a5..1dd3498 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -74,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, @@ -138,12 +138,10 @@ fn run_multicore_module6_tc00_display(device: Device, _core1_buf: MulticoreCore1 let bl = Output::new(PIN_28, Level::High); executor1.run(|spawner| { - unwrap!( - crate::display_module6_st7789::module6_st7789_core1_task( - device, spi, cs, dc, rst, bl, - ) - .map(|t| spawner.spawn(t)) - ); + unwrap!(crate::display_module6_st7789::module6_st7789_core1_task( + device, spi, cs, dc, rst, bl, + ) + .map(|t| spawner.spawn(t))); }); }, ); @@ -151,34 +149,28 @@ fn run_multicore_module6_tc00_display(device: Device, _core1_buf: MulticoreCore1 let executor0 = EXECUTOR0.init(Executor::new()); executor0.run(|spawner| { unwrap!(multicore_core0_supervisor_task(supervisor).map(|t| spawner.spawn(t))); - unwrap!( - usb::usb_task_for_device( - Driver::new(USB, crate::Irqs), - Output::new(PIN_20, Level::Low), - device, - ) - .map(|t| spawner.spawn(t)) - ); - unwrap!( - buttons::button_task_direct({ - let mut inputs = heapless::Vec::new(); - let _ = inputs.push(Input::new(PIN_1, Pull::Up)); - let _ = inputs.push(Input::new(PIN_2, Pull::Up)); - let _ = inputs.push(Input::new(PIN_3, Pull::Up)); - let _ = inputs.push(Input::new(PIN_4, Pull::Up)); - let _ = inputs.push(Input::new(PIN_9, Pull::Up)); - let _ = inputs.push(Input::new(PIN_10, Pull::Up)); - inputs - }) - .map(|t| spawner.spawn(t)) - ); - unwrap!( - hardware::status_task( - Output::new(PIN_25, Level::Low), - Output::new(PIN_21, Level::Low), - ) - .map(|t| spawner.spawn(t)) - ); + unwrap!(usb::usb_task_for_device( + Driver::new(USB, crate::Irqs), + Output::new(PIN_20, Level::Low), + device, + ) + .map(|t| spawner.spawn(t))); + unwrap!(buttons::button_task_direct({ + let mut inputs = heapless::Vec::new(); + let _ = inputs.push(Input::new(PIN_1, Pull::Up)); + let _ = inputs.push(Input::new(PIN_2, Pull::Up)); + let _ = inputs.push(Input::new(PIN_3, Pull::Up)); + let _ = inputs.push(Input::new(PIN_4, Pull::Up)); + let _ = inputs.push(Input::new(PIN_9, Pull::Up)); + let _ = inputs.push(Input::new(PIN_10, Pull::Up)); + inputs + }) + .map(|t| spawner.spawn(t))); + unwrap!(hardware::status_task( + Output::new(PIN_25, Level::Low), + Output::new(PIN_21, Level::Low), + ) + .map(|t| spawner.spawn(t))); }); loop { diff --git a/src/hardware.rs b/src/hardware.rs index 7e2e44c..be58161 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -57,7 +57,7 @@ impl HardwareConfig { // 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) => ( @@ -118,15 +118,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::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)?; @@ -173,15 +164,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::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)?; @@ -242,84 +225,68 @@ 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::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)); - 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)); - } 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 + / 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), } (driver, usb_led, status_led, error_led, row_pins, col_pins) @@ -417,16 +384,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::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 eb085b4..29d6b0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! using Embassy async framework on RP2040. //! //! ## Supported Devices -//! - StreamDeck Mini 2022 / Mini Discord / Module 6 Keys (6 keys, 80×80px BMP family) +//! - 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) @@ -43,10 +43,10 @@ pub mod supervisor; pub mod types; pub mod usb; -#[cfg(feature = "display")] -pub mod display_spi_dma; #[cfg(feature = "display")] pub mod display_module6_st7789; +#[cfg(feature = "display")] +pub mod display_spi_dma; // USB interrupt binding - shared by all binaries bind_interrupts!(pub struct Irqs { diff --git a/src/protocol/module_6.rs b/src/protocol/module_6.rs index 3875b31..7163edf 100644 --- a/src/protocol/module_6.rs +++ b/src/protocol/module_6.rs @@ -43,7 +43,7 @@ impl Module6KeysHandler { return None; } let bf_size = u32::from_le_bytes(buf[2..6].try_into().ok()?) as usize; - if bf_size > MODULE6_BMP_CAP || bf_size < 54 { + if !(54..=MODULE6_BMP_CAP).contains(&bf_size) { return None; } Some(bf_size) 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 439a296..2ca7029 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -329,11 +329,8 @@ async fn usb_task_impl( key_id, data.len() ); - dispatch_display_command( - device, - DisplayCommand::DisplayImage { key_id, data }, - ) - .await; + dispatch_display_command(device, DisplayCommand::DisplayImage { key_id, data }) + .await; } UsbCommand::FullScreenImage { data } => { dispatch_display_command(device, DisplayCommand::DisplayFullScreen { data }) @@ -365,12 +362,10 @@ async fn usb_task_impl( dispatch_display_command(device, DisplayCommand::FillLcd { r, g, b }).await; } UsbCommand::FillKeyColor { key_index, r, g, b } => { - dispatch_display_command(device, DisplayCommand::FillKey { - key_index, - r, - g, - b, - }) + dispatch_display_command( + device, + DisplayCommand::FillKey { key_index, r, g, b }, + ) .await; } UsbCommand::ShowBackgroundByIndex { index } => { From 5ea21e021e76b464ef60760242675bd605f8afa7 Mon Sep 17 00:00:00 2001 From: "Misei." Date: Mon, 4 May 2026 10:04:51 +0900 Subject: [PATCH 3/4] chore: remove Module 6 ST7789 display feature and related code - Drop mipidsi/display SPI optional deps, display Cargo feature, and ST7789 task entry path. - Remove display_module6_st7789 and display_spi_dma modules; restore module_6 protocol stub. - Reset config channel/buffer sizes to non-display defaults. --- Cargo.lock | 52 +--------- Cargo.toml | 5 - src/config.rs | 29 +----- src/display_module6_st7789.rs | 189 ---------------------------------- src/display_spi_dma.rs | 90 ---------------- src/entry.rs | 103 ------------------ src/lib.rs | 14 --- src/protocol/module_6.rs | 146 ++++++++------------------ 8 files changed, 51 insertions(+), 577 deletions(-) delete mode 100644 src/display_module6_st7789.rs delete mode 100644 src/display_spi_dma.rs diff --git a/Cargo.lock b/Cargo.lock index c8189c9..dfce46c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,12 +107,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - [[package]] name = "bytemuck" version = "1.23.2" @@ -369,24 +363,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "display-interface" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" - -[[package]] -name = "display-interface-spi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9ec30048b1955da2038fcc3c017f419ab21bb0001879d16c0a3749dc6b7a" -dependencies = [ - "byte-slice-cast", - "display-interface", - "embedded-hal 1.0.0", - "embedded-hal-async", -] - [[package]] name = "document-features" version = "0.2.11" @@ -663,16 +639,6 @@ dependencies = [ "embedded-hal 1.0.0", ] -[[package]] -name = "embedded-hal-bus" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886" -dependencies = [ - "critical-section", - "embedded-hal 1.0.0", -] - [[package]] name = "embedded-hal-bus" version = "0.3.0" @@ -1024,19 +990,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" -[[package]] -name = "mipidsi" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e2bbd372d8ae9ccd0fc6eb4d91742b971ed8149968bbc623f025506989bd30" -dependencies = [ - "display-interface", - "embedded-graphics-core", - "embedded-hal 1.0.0", - "heapless 0.8.0", - "nb 1.1.0", -] - [[package]] name = "nb" version = "0.1.3" @@ -1273,7 +1226,6 @@ dependencies = [ "defmt", "defmt-rtt", "defmt-test", - "display-interface-spi", "embassy-executor", "embassy-futures", "embassy-rp", @@ -1284,11 +1236,9 @@ dependencies = [ "embedded-graphics-core", "embedded-hal 1.0.0", "embedded-hal-async", - "embedded-hal-bus 0.1.0", - "embedded-hal-bus 0.3.0", + "embedded-hal-bus", "fixed", "heapless 0.9.2", - "mipidsi", "nb 1.1.0", "panic-halt", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index ae44325..a5dc776 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,15 +28,12 @@ critical-section = "1.0" embedded-hal = "1.0" embedded-hal-async = "1.0" embedded-hal-bus = { version = "0.3", features = ["async"] } -embedded-hal-bus-sync = { package = "embedded-hal-bus", version = "0.1", optional = true } portable-atomic = { version = "1.0", features = ["critical-section"] } # Display and graphics st7735-lcd = "0.10" embedded-graphics = "0.8" embedded-graphics-core = "0.4" -display-interface-spi = { version = "0.5", optional = true } -mipidsi = { version = "0.8", default-features = false, optional = true, features = ["batch"] } # USB HID usbd-hid = "0.10" @@ -132,5 +129,3 @@ bench = false [features] default = [] -# TC-00 ST7789 + larger Module 6 BMP buffers (use only with `--bin module6 --features display`) -display = ["dep:mipidsi", "dep:display-interface-spi", "dep:embedded-hal-bus-sync"] diff --git a/src/config.rs b/src/config.rs index 31a7c98..a370c3c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -204,43 +204,24 @@ pub fn display_total_height() -> usize { // USB Configuration pub const USB_POLL_RATE_MS: u64 = 1; // 1ms USB polling (1000Hz) -/// BMP / USB image payload capacity (`display` enables full Module 6 key BMP assembly). -#[cfg(feature = "display")] -pub const IMAGE_BUFFER_SIZE: usize = 20480; -#[cfg(not(feature = "display"))] -pub const IMAGE_BUFFER_SIZE: usize = 1024; +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 -/// Queue depth for [`crate::channels::MULTICORE_IMAGE_CHANNEL`] (large payloads when `display`). -#[cfg(feature = "display")] -pub const MULTICORE_CHANNEL_SIZE: usize = 1; -#[cfg(not(feature = "display"))] +/// Queue depth for [`crate::channels::MULTICORE_IMAGE_CHANNEL`]. pub const MULTICORE_CHANNEL_SIZE: usize = 8; -/// Raw BMP accumulation cap for Module 6 chunked uploads (≥ full 80×80 24 bpp BMP). -#[cfg(feature = "display")] -pub const MODULE6_BMP_CAP: usize = 20480; -#[cfg(not(feature = "display"))] +/// Raw BMP accumulation cap for Module 6 chunked uploads. pub const MODULE6_BMP_CAP: usize = 1024; -/// Depth of [`crate::channels::USB_COMMAND_CHANNEL`] (large `UsbCommand` payloads when `display`). -#[cfg(feature = "display")] -pub const USB_COMMAND_CHANNEL_SIZE: usize = 1; -#[cfg(not(feature = "display"))] +/// Depth of [`crate::channels::USB_COMMAND_CHANNEL`]. pub const USB_COMMAND_CHANNEL_SIZE: usize = 4; -/// Depth of [`crate::channels::DISPLAY_CHANNEL`] (RAM-heavy when `IMAGE_BUFFER_SIZE` is large). -#[cfg(feature = "display")] -pub const DISPLAY_CHANNEL_CAPACITY: usize = 1; -#[cfg(not(feature = "display"))] +/// Depth of [`crate::channels::DISPLAY_CHANNEL`]. pub const DISPLAY_CHANNEL_CAPACITY: usize = 8; -/// TC-00 / Iryx ST7789 SPI clock (matches vmix-pico-controller TC00). -pub const MODULE6_SPI_FREQUENCY: u32 = 50_000_000; - // =================================================================== // Power Management: Idle Time (Sleep Mode) // =================================================================== diff --git a/src/display_module6_st7789.rs b/src/display_module6_st7789.rs deleted file mode 100644 index 62775c0..0000000 --- a/src/display_module6_st7789.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Core 1 ST7789 pipeline for Module 6 + TC-00 wiring (`--features display`). - -use crate::channels::MULTICORE_IMAGE_CHANNEL; -use crate::device::{Device, DeviceConfig}; -use crate::display_spi_dma::DisplaySpiBus; -use crate::protocol::image::rotate_270; -use crate::types::DisplayCommand; -use defmt::{info, warn}; -use display_interface_spi::SPIInterface; -use embassy_rp::gpio::Output; -use embassy_rp::peripherals::SPI1; -use embassy_rp::spi::{Async, Spi}; -use embassy_time::Delay; -use embedded_graphics::primitives::{Primitive, PrimitiveStyleBuilder}; -use embedded_graphics_core::geometry::{Point, Size}; -use embedded_graphics_core::pixelcolor::{Rgb565, Rgb888, RgbColor}; -use embedded_graphics_core::prelude::*; -use embedded_graphics_core::primitives::Rectangle; -use embedded_hal_bus_sync::spi::ExclusiveDevice; -use mipidsi::models::ST7789; -use mipidsi::options::{ColorInversion, ColorOrder, Orientation}; -use mipidsi::Builder; - -type Module6Display = mipidsi::Display< - SPIInterface< - ExclusiveDevice, Output<'static>, Delay>, - Output<'static>, - >, - ST7789, - Output<'static>, ->; - -fn bmp_rgb888_80(buf: &[u8], dst: &mut [u8; 80 * 80 * 3]) -> Result<(), ()> { - if buf.len() < 54 { - return Err(()); - } - if buf[0] != b'B' || buf[1] != b'M' { - return Err(()); - } - let offset = u32::from_le_bytes(buf[10..14].try_into().map_err(|_| ())?) as usize; - let w = i32::from_le_bytes(buf[18..22].try_into().map_err(|_| ())?).unsigned_abs(); - let h_raw = i32::from_le_bytes(buf[22..26].try_into().map_err(|_| ())?); - let h = h_raw.unsigned_abs(); - let bpp = u16::from_le_bytes(buf[28..30].try_into().map_err(|_| ())?) as usize; - if w != 80 || h != 80 || bpp != 24 { - return Err(()); - } - let row_stride = ((w as usize * bpp / 8) + 3) & !3; - let top_down = h_raw < 0; - for row in 0..80usize { - let src_row = if top_down { row } else { 79 - row }; - let src_off = offset + src_row * row_stride; - if src_off + 240 > buf.len() { - return Err(()); - } - let dst_off = row * 240; - for col in 0..80usize { - let s = src_off + col * 3; - let d = dst_off + col * 3; - let b = buf[s]; - let g = buf[s + 1]; - let r = buf[s + 2]; - dst[d] = r; - dst[d + 1] = g; - dst[d + 2] = b; - } - } - Ok(()) -} - -fn key_origin_px(key_id: u8) -> (i32, i32) { - let k = (key_id as usize).min(5); - let col = k % 3; - let row = k / 3; - ((col * 80) as i32, (row * 80) as i32) -} - -fn fill_rect(display: &mut Module6Display, x: i32, y: i32, w: u32, h: u32, c: Rgb565) { - let rect = Rectangle::new(Point::new(x, y), Size::new(w, h)) - .into_styled(PrimitiveStyleBuilder::new().fill_color(c).build()); - let _ = rect.draw(display); -} - -fn draw_px_grid(display: &mut Module6Display, ox: i32, oy: i32, px: &[u8]) { - let it = (0..80i32).flat_map(|yy| { - (0..80i32).filter_map(move |xx| { - let i = ((yy as usize) * 80 + (xx as usize)) * 3; - if i + 2 >= px.len() { - return None; - } - let r = px[i]; - let g = px[i + 1]; - let b = px[i + 2]; - let c = Rgb565::from(Rgb888::new(r, g, b)); - Some(Pixel(Point::new(ox + xx, oy + yy), c)) - }) - }); - let _ = display.draw_iter(it); -} - -fn draw_key_bmp(display: &mut Module6Display, device: Device, key_id: u8, bmp: &[u8]) { - let dc = device.display_config(); - let mut rgb = [0u8; 80 * 80 * 3]; - if bmp_rgb888_80(bmp, &mut rgb).is_err() { - warn!("module6 display: invalid BMP for key {}", key_id); - return; - } - - let (ox, oy) = key_origin_px(key_id); - if dc.needs_rotation { - let rotated = rotate_270(&rgb, 80, 80); - draw_px_grid(display, ox, oy, rotated.as_slice()); - } else { - draw_px_grid(display, ox, oy, rgb.as_slice()); - } -} - -fn handle_display_cmd( - display: &mut Module6Display, - device: Device, - backlight: &mut Output<'static>, - cmd: DisplayCommand, -) { - match cmd { - DisplayCommand::ClearAll => { - for kid in 0u8..6 { - let (x, y) = key_origin_px(kid); - fill_rect(display, x, y, 80, 80, Rgb565::BLACK); - } - } - DisplayCommand::Clear(key_id) => { - let (x, y) = key_origin_px(key_id); - fill_rect(display, x, y, 80, 80, Rgb565::BLACK); - } - DisplayCommand::SetBrightness(pct) => { - info!("backlight {}%", pct); - if pct > 4 { - backlight.set_high(); - } else { - backlight.set_low(); - } - } - DisplayCommand::DisplayImage { key_id, data } => { - draw_key_bmp(display, device, key_id, data.as_slice()); - } - DisplayCommand::FillLcd { r, g, b } => { - let c = Rgb565::from(Rgb888::new(r, g, b)); - fill_rect(display, 0, 0, 240, 320, c); - } - DisplayCommand::FillKey { key_index, r, g, b } => { - let c = Rgb565::from(Rgb888::new(r, g, b)); - let (x, y) = key_origin_px(key_index); - fill_rect(display, x, y, 80, 80, c); - } - DisplayCommand::DisplayFullScreen { .. } | DisplayCommand::DisplayWindow { .. } => {} - } -} - -#[embassy_executor::task] -pub async fn module6_st7789_core1_task( - device: Device, - spi: Spi<'static, SPI1, Async>, - cs: Output<'static>, - dc: Output<'static>, - rst: Output<'static>, - mut backlight: Output<'static>, -) { - backlight.set_high(); - - let spi_bus = DisplaySpiBus(spi); - let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay); - let di = SPIInterface::new(spi_dev, dc); - let mut display: Module6Display = Builder::new(ST7789, di) - .display_size(240, 320) - .orientation(Orientation::new()) - .reset_pin(rst) - .color_order(ColorOrder::Rgb) - .invert_colors(ColorInversion::Inverted) - .init(&mut Delay) - .expect("ST7789 init"); - - display.clear(Rgb565::BLACK).expect("clear"); - - let recv = MULTICORE_IMAGE_CHANNEL.receiver(); - loop { - let cmd = recv.receive().await; - handle_display_cmd(&mut display, device, &mut backlight, cmd); - } -} diff --git a/src/display_spi_dma.rs b/src/display_spi_dma.rs deleted file mode 100644 index 07dcaf1..0000000 --- a/src/display_spi_dma.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! SPI bus wrapper so [`embedded_hal::spi::SpiBus`] writes use RP2040 DMA for bulk TX (TC-00 ST7789). - -const MIN_DMA_TX_LEN: usize = 64; - -use core::future::Future; -use core::pin::pin; -use core::sync::atomic::{AtomicBool, Ordering}; -use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - -use cortex_m::asm::wfe; -use embassy_rp::spi::{Async, Instance, Spi}; -use embedded_hal::spi::SpiBus; - -unsafe fn waker_clone(p: *const ()) -> RawWaker { - RawWaker::new(p, &FLAG_WAKER_VTABLE) -} -unsafe fn waker_wake(p: *const ()) { - (*(p as *const AtomicBool)).store(true, Ordering::Release); -} -unsafe fn waker_wake_by_ref(p: *const ()) { - waker_wake(p); -} -unsafe fn waker_drop(_: *const ()) {} - -static FLAG_WAKER_VTABLE: RawWakerVTable = - RawWakerVTable::new(waker_clone, waker_wake, waker_wake_by_ref, waker_drop); - -fn waker_flag(flag: &AtomicBool) -> Waker { - unsafe { - Waker::from_raw(RawWaker::new( - flag as *const _ as *const (), - &FLAG_WAKER_VTABLE, - )) - } -} - -fn block_on_dma_write( - spi: &mut Spi<'_, T, Async>, - words: &[u8], -) -> Result<(), embassy_rp::spi::Error> { - let done = AtomicBool::new(false); - let waker = waker_flag(&done); - let mut cx = Context::from_waker(&waker); - let mut fut = pin!(spi.write(words)); - loop { - match fut.as_mut().poll(&mut cx) { - Poll::Ready(r) => return r, - Poll::Pending => loop { - if done.load(Ordering::Acquire) { - done.store(false, Ordering::Release); - break; - } - wfe(); - }, - } - } -} - -/// SPI bus for ST7789: TX uses DMA for longer bursts. -pub struct DisplaySpiBus<'d, T: Instance>(pub Spi<'d, T, Async>); - -impl embedded_hal::spi::ErrorType for DisplaySpiBus<'_, T> { - type Error = embassy_rp::spi::Error; -} - -impl SpiBus for DisplaySpiBus<'_, T> { - fn flush(&mut self) -> Result<(), Self::Error> { - self.0.flush() - } - - fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { - self.0.blocking_read(words) - } - - fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { - if words.len() < MIN_DMA_TX_LEN { - self.0.blocking_write(words) - } else { - block_on_dma_write(&mut self.0, words) - } - } - - fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { - self.0.blocking_transfer(read, write) - } - - fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { - self.0.blocking_transfer_in_place(words) - } -} diff --git a/src/entry.rs b/src/entry.rs index 1dd3498..697ae76 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -80,115 +80,12 @@ pub enum MulticoreCore1Buffer { B16384, } -/// TC-00 ST7789 on Core 1 + Module 6 buttons (`--features display`). -#[cfg(feature = "display")] -fn run_multicore_module6_tc00_display(device: Device, _core1_buf: MulticoreCore1Buffer) -> ! { - let embassy_rp::Peripherals { - FLASH, - CORE1, - USB, - SPI1, - DMA_CH2, - DMA_CH3, - PIN_1, - PIN_2, - PIN_3, - PIN_4, - PIN_9, - PIN_10, - PIN_11, - PIN_12, - PIN_13, - PIN_14, - PIN_20, - PIN_21, - PIN_22, - PIN_25, - PIN_27, - PIN_28, - .. - } = embassy_rp::init(Default::default()); - - config::init_runtime_device(device); - config::init_usb_serial_from_flash(FLASH); - - let supervisor = AppSupervisor::new_for_device(device); - supervisor.print_startup_banner(); - - spawn_core1( - CORE1, - unsafe { &mut *core::ptr::addr_of_mut!(CORE1_STACK) }, - move || { - let executor1 = EXECUTOR1.init(Executor::new()); - let mut spi_cfg = embassy_rp::spi::Config::default(); - spi_cfg.frequency = crate::config::MODULE6_SPI_FREQUENCY; - let spi = embassy_rp::spi::Spi::new( - SPI1, - PIN_14, - PIN_11, - PIN_12, - DMA_CH2, - DMA_CH3, - crate::DisplayDmaIrqs, - spi_cfg, - ); - let cs = Output::new(PIN_13, Level::High); - let dc = Output::new(PIN_22, Level::Low); - let rst = Output::new(PIN_27, Level::High); - let bl = Output::new(PIN_28, Level::High); - - executor1.run(|spawner| { - unwrap!(crate::display_module6_st7789::module6_st7789_core1_task( - device, spi, cs, dc, rst, bl, - ) - .map(|t| spawner.spawn(t))); - }); - }, - ); - - let executor0 = EXECUTOR0.init(Executor::new()); - executor0.run(|spawner| { - unwrap!(multicore_core0_supervisor_task(supervisor).map(|t| spawner.spawn(t))); - unwrap!(usb::usb_task_for_device( - Driver::new(USB, crate::Irqs), - Output::new(PIN_20, Level::Low), - device, - ) - .map(|t| spawner.spawn(t))); - unwrap!(buttons::button_task_direct({ - let mut inputs = heapless::Vec::new(); - let _ = inputs.push(Input::new(PIN_1, Pull::Up)); - let _ = inputs.push(Input::new(PIN_2, Pull::Up)); - let _ = inputs.push(Input::new(PIN_3, Pull::Up)); - let _ = inputs.push(Input::new(PIN_4, Pull::Up)); - let _ = inputs.push(Input::new(PIN_9, Pull::Up)); - let _ = inputs.push(Input::new(PIN_10, Pull::Up)); - inputs - }) - .map(|t| spawner.spawn(t))); - unwrap!(hardware::status_task( - Output::new(PIN_25, Level::Low), - Output::new(PIN_21, Level::Low), - ) - .map(|t| spawner.spawn(t))); - }); - - loop { - cortex_m::asm::wfe(); - } -} - /// Multicore bring-up: core 1 runs display stub; core 0 runs USB, buttons, supervisor. pub fn run_multicore( device: Device, layout: MulticoreCore0Layout, core1_buf: MulticoreCore1Buffer, ) -> ! { - #[cfg(feature = "display")] - if matches!(layout, MulticoreCore0Layout::Module6DirectTc00) { - run_multicore_module6_tc00_display(device, core1_buf); - } - let p = embassy_rp::init(Default::default()); config::init_runtime_device(device); config::init_usb_serial_from_flash(p.FLASH); diff --git a/src/lib.rs b/src/lib.rs index 29d6b0c..d995865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,15 +21,6 @@ use embassy_rp::usb::InterruptHandler; use embassy_rp::{bind_interrupts, peripherals}; -#[cfg(feature = "display")] -bind_interrupts!(pub struct DisplayDmaIrqs { - DMA_IRQ_0 => embassy_rp::dma::InterruptHandler, - embassy_rp::dma::InterruptHandler, - embassy_rp::dma::InterruptHandler, - embassy_rp::dma::InterruptHandler, - embassy_rp::dma::InterruptHandler; -}); - // Export all modules for use by device-specific binaries pub mod buttons; pub mod channels; @@ -43,11 +34,6 @@ pub mod supervisor; pub mod types; pub mod usb; -#[cfg(feature = "display")] -pub mod display_module6_st7789; -#[cfg(feature = "display")] -pub mod display_spi_dma; - // USB interrupt binding - shared by all binaries bind_interrupts!(pub struct Irqs { USBCTRL_IRQ => InterruptHandler; diff --git a/src/protocol/module_6.rs b/src/protocol/module_6.rs index 7163edf..4b5842a 100644 --- a/src/protocol/module_6.rs +++ b/src/protocol/module_6.rs @@ -1,52 +1,22 @@ -//! StreamDeck Module HID Protocol Handler (6 keys) +//! StreamDeck Module HID Protocol Handler (6keys) //! -//! Legacy Mini-family protocol per Elgato docs: -//! https://docs.elgato.com/streamdeck/hid/mini +//! Implements the unified `ProtocolHandlerTrait` for the Elgato Stream Deck +//! Modules per public HID API docs. Image upload parsing is stubbed until we +//! confirm exact chunk layout from PCAPs. use super::{ feature_report_clamp, feature_report_zero_prefix, fill_feature_rid_ascii, map_buttons_grid, ButtonMapping, OutputReportResult, ProtocolHandlerTrait, }; -use crate::config::MODULE6_BMP_CAP; use crate::device::ProtocolVersion; use crate::protocol::module::{FirmwareType, ModuleGetCommand, ModuleSetCommand}; -use heapless::Vec; #[derive(Debug)] -pub struct Module6KeysHandler { - image_buffer: Vec, - receiving: bool, - expected_key: u8, -} +pub struct Module6KeysHandler {} impl Module6KeysHandler { pub fn new() -> Self { - Self { - image_buffer: Vec::new(), - receiving: false, - expected_key: 0, - } - } - - fn reset_rx(&mut self) { - self.image_buffer.clear(); - self.receiving = false; - self.expected_key = 0; - } - - /// BMP file size from `bfSize` once `BM` magic is present. - fn bmp_total_bytes(buf: &[u8]) -> Option { - if buf.len() < 6 { - return None; - } - if buf[0] != b'B' || buf[1] != b'M' { - return None; - } - let bf_size = u32::from_le_bytes(buf[2..6].try_into().ok()?) as usize; - if !(54..=MODULE6_BMP_CAP).contains(&bf_size) { - return None; - } - Some(bf_size) + Self {} } } @@ -55,11 +25,12 @@ impl Default for Module6KeysHandler { Self::new() } } - impl Module6KeysHandler { fn parse_module_set_command(&self, report_id: u8, data: &[u8]) -> Option { match report_id { 0x05 => { + // Payload excludes Report ID. Per spec: + // [Command=0x55, 0xAA, 0xD1, 0x01, Brightness] if data.len() >= 5 && data[0] == 0x55 && data[1] == 0xAA @@ -72,9 +43,12 @@ impl Module6KeysHandler { } } 0x0B => { + // Payload excludes Report ID. + // Commands at data[0] if !data.is_empty() { match data[0] { 0x63 => { + // data[1]: 0x00 Show Logo, 0x02 Update Boot Logo if data.len() >= 2 { match data[1] { 0x00 => Some(ModuleSetCommand::ShowLogo), @@ -89,6 +63,7 @@ impl Module6KeysHandler { } } 0xA2 => { + // data[1..=4]: i32 seconds (LE) if data.len() >= 5 { let secs = i32::from_le_bytes([data[1], data[2], data[3], data[4]]); Some(ModuleSetCommand::SetIdleTime { seconds: secs }) @@ -113,7 +88,7 @@ impl Module6KeysHandler { 0xA2 => Some(ModuleGetCommand::GetFirmwareVersion(FirmwareType::AP1)), 0x03 => Some(ModuleGetCommand::GetUnitSerialNumber), 0xA3 => Some(ModuleGetCommand::GetIdleTime), - 0x08 => Some(ModuleGetCommand::GetUnitInformation), + 0x08 => Some(ModuleGetCommand::GetUnitInformation), // Module 6 compatibility _ => None, } } @@ -139,55 +114,26 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn parse_output_report(&mut self, data: &[u8]) -> OutputReportResult { - // Upload Data to Image Memory Bank — Report ID 0x02, Command 0x01. - // Layout: [0]=RID 0x02, [1]=Cmd 0x01, [2]=chunk idx, [3]=0x00, [4]=show flag, - // [5]=key idx, [6..0x10]=reserved, [0x10..]=payload. - if data.len() < 17 { - return OutputReportResult::Unhandled; - } - if data[0] != 0x02 || data[1] != 0x01 { - return OutputReportResult::Unhandled; - } - - let chunk_idx = data[2]; - let key_idx = data[5]; - let chunk_payload = data.get(0x10..).unwrap_or(&[]); - - if chunk_idx == 0 { - self.reset_rx(); - self.receiving = true; - self.expected_key = key_idx; - } else if !self.receiving || key_idx != self.expected_key { - self.reset_rx(); - return OutputReportResult::Unhandled; - } - - if self.image_buffer.extend_from_slice(chunk_payload).is_err() { - self.reset_rx(); - return OutputReportResult::Unhandled; - } + let report_id = data[0]; + let command = data[1]; - let Some(total) = Self::bmp_total_bytes(&self.image_buffer) else { - return OutputReportResult::Unhandled; - }; - - if self.image_buffer.len() < total { - return OutputReportResult::Unhandled; - } - - let mut image = Vec::new(); - let slice = self.image_buffer.as_slice().get(..total).unwrap_or(&[]); - if image.extend_from_slice(slice).is_err() { - self.reset_rx(); - return OutputReportResult::Unhandled; - } - - let completed_key = self.expected_key; - self.reset_rx(); - - OutputReportResult::KeyImageComplete { - key_id: completed_key, - image, + match report_id { + // https://docs.elgato.com/streamdeck/hid/module-6#upload-data-to-image-memory-bank + 0x02 => { + if command == 0x01 { + let _chunk_index = data[2]; + let _reserved = data[3]; + let _show_image_flag = data[4]; + let _key_index = data[5]; + let _reserved = &data[6..0x10]; + let _chunk_data = &data[0x10..]; + + OutputReportResult::Unhandled + } else { + OutputReportResult::Unhandled + } + } + _ => OutputReportResult::Unhandled, } } @@ -202,6 +148,8 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn hid_descriptor(&self) -> &'static [u8] { + // Minimal descriptor covering Input(0x01), Output(0x02), Feature(0x03/0x04/0x05/0x07/0x08/0x0B/0xA0/0xA1/0xA2/0xA3) + // This can be fine-tuned to match exact real devices if needed. const DESC: &[u8] = &[ 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) @@ -214,7 +162,7 @@ impl ProtocolHandlerTrait for Module6KeysHandler { 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8) - 0x95, 0x3F, // Report Count (63) + 0x95, 0x3F, // Report Count (63) -> total 64 bytes incl. Report ID 0x81, 0x02, // Input (Data,Var,Abs) // Output report 0x02 (image/data chunks) 0x85, 0x02, // Report ID 0x02 @@ -236,7 +184,7 @@ impl ProtocolHandlerTrait for Module6KeysHandler { 0x85, 0xA1, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0x85, 0xA2, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0x85, 0xA3, 0x0A, 0x00, 0xFF, 0x15, 0x00, 0x26, 0xFF, 0x00, - 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0xC0, + 0x75, 0x08, 0x95, 0x10, 0xB1, 0x04, 0xC0, // End Collection ]; DESC } @@ -246,19 +194,23 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn format_button_report(&self, buttons: &ButtonMapping, report: &mut [u8]) -> usize { + // 64 bytes total per packet: Report ID (1) + 63 data bytes const MAX_USB_SIZE: usize = 64; if report.len() < MAX_USB_SIZE { return 0; } + // Set Report ID report[0] = 0x01; + // Map up to 63 data bytes; Module 6 needs first 6 let button_count = core::cmp::min(6, buttons.mapped_buttons.len()); for i in 0..button_count { report[1 + i] = if buttons.mapped_buttons[i] { 1 } else { 0 }; } + // Zero out remaining bytes in the USB packet report .iter_mut() .take(MAX_USB_SIZE) @@ -269,7 +221,10 @@ impl ProtocolHandlerTrait for Module6KeysHandler { } fn handle_feature_report(&mut self, report_id: u8, data: &[u8]) -> Option { - self.parse_module_set_command(report_id, data) + if let Some(cmd) = self.parse_module_set_command(report_id, data) { + return Some(cmd); + } + None } fn get_feature_report(&mut self, report_id: u8, buf: &mut [u8]) -> Option { @@ -305,18 +260,7 @@ impl Module6KeysHandler { buf[5] = le[3]; Some(cap) } - ModuleGetCommand::GetUnitInformation => { - let tail = crate::device::Device::Module6Keys.unit_information_tail(); - let cap = feature_report_clamp(total_len, buf.len()); - if cap < 5 + tail.len() { - return None; - } - feature_report_zero_prefix(buf, cap); - buf[0] = 0x08; - buf[1..5].copy_from_slice(&[0u8; 4]); - buf[5..5 + tail.len()].copy_from_slice(&tail); - Some(cap) - } + _ => None, } } else { None From a82970ba63d38ca832bab7ccd6087178a760865e Mon Sep 17 00:00:00 2001 From: "Misei." Date: Mon, 4 May 2026 10:08:42 +0900 Subject: [PATCH 4/4] feat(neo): wire eight keys to direct GPIO (GP1,2,3,4,9,10,15,26) --- src/hardware.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/hardware.rs b/src/hardware.rs index be58161..7c2fed5 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -53,6 +53,30 @@ 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 @@ -64,7 +88,7 @@ impl HardwareConfig { &[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), }; @@ -193,6 +217,7 @@ fn create_all_pins_for_device( let Peripherals { FLASH, USB, + PIN_1, PIN_2, PIN_3, PIN_4, @@ -204,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; @@ -225,6 +252,18 @@ fn create_all_pins_for_device( let mut row_pins: Vec, 4> = Vec::new(); let mut col_pins: Vec, 32> = Vec::new(); + // 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_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 { // Matrix pin assignments by layout (`module6` uses multicore wiring in `entry`, not here). match (layout.rows, layout.cols) { (2, 3) => { @@ -236,7 +275,7 @@ fn create_all_pins_for_device( let _ = col_pins.push(Input::new(PIN_6, Pull::Up)); } (2, 4) => { - // Stream Deck + / Neo (4x2 keys) + // 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)); @@ -288,6 +327,7 @@ fn create_all_pins_for_device( } _ => core::panic!("no pin mapping for matrix {}×{}", layout.cols, layout.rows), } + } (driver, usb_led, status_led, error_led, row_pins, col_pins) } @@ -299,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