715 lines
27 KiB
Rust
715 lines
27 KiB
Rust
//! TODO:
|
|
//! * GUI event dispatch.
|
|
//! * Async interrupt handling of keyboard input. Reduces LCD glitching.
|
|
//! * Attempt to reduce the size of the framebuffer to 240x960 by changing the front/back porch of
|
|
//! the LCD driver. Reduces LCD glitching.
|
|
//! * Bounce buffers to get rid of glitching completely.
|
|
//! https://esp32.com/viewtopic.php?t=28230
|
|
//! https://esp32.com/viewtopic.php?f=12&t=26793&start=20#p95677
|
|
#![no_std]
|
|
#![no_main]
|
|
#![feature(allocator_api)]
|
|
#![feature(macro_metavar_expr)]
|
|
#![feature(c_variadic)]
|
|
#![feature(c_size_t)]
|
|
|
|
extern crate alloc;
|
|
|
|
use core::alloc::Layout;
|
|
use core::cell::RefCell;
|
|
use core::slice;
|
|
|
|
use alloc::boxed::Box;
|
|
use alloc::vec;
|
|
use cfg_if::cfg_if;
|
|
use embassy_executor::Spawner;
|
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
use embassy_sync::channel::Channel;
|
|
use embassy_sync::signal::Signal;
|
|
use embassy_time::{Duration, Instant};
|
|
use esp_alloc::{HeapRegion, MemoryCapability};
|
|
use esp_hal::Blocking;
|
|
use esp_hal::clock::CpuClock;
|
|
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
|
|
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
|
use esp_hal::i2c::master::{I2c, I2cAddress};
|
|
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
|
use esp_hal::lcd_cam::LcdCam;
|
|
use esp_hal::lcd_cam::lcd::dpi::Dpi;
|
|
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
|
|
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
|
|
use esp_hal::system::Stack;
|
|
use esp_hal::timer::timg::TimerGroup;
|
|
use esp_rtos::embassy::Executor;
|
|
use esp_storage::FlashStorage;
|
|
use log::{LevelFilter, error, info, warn};
|
|
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
|
|
use rmk::config::{BehaviorConfig, PositionalConfig, RmkConfig, StorageConfig, VialConfig};
|
|
use rmk::controller::{Controller, EventController};
|
|
use rmk::debounce::default_debouncer::DefaultDebouncer;
|
|
use rmk::descriptor::KeyboardReport;
|
|
use rmk::event::ControllerEvent;
|
|
use rmk::hid::Report;
|
|
use rmk::input_device::Runnable;
|
|
use rmk::join_all;
|
|
use rmk::keyboard::Keyboard;
|
|
use rmk::storage::async_flash_wrapper;
|
|
use rmk::types::action::{Action, KeyAction};
|
|
use rmk::types::keycode::KeyCode;
|
|
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
|
|
use slint::ComponentHandle;
|
|
use slint::platform::software_renderer::Rgb565Pixel;
|
|
use static_cell::StaticCell;
|
|
use ui::AppWindow;
|
|
use xkbcommon::xkb::{self, KeyDirection};
|
|
use {esp_alloc as _, esp_backtrace as _};
|
|
|
|
use crate::matrix::IoeMatrix;
|
|
use crate::peripherals::st7701s::St7701s;
|
|
use crate::ui::backend::{FramebufferPtr, SlintBackend};
|
|
use crate::vial::{CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID};
|
|
|
|
mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];
|
|
|
|
mod ffi;
|
|
mod keymap;
|
|
mod logging;
|
|
mod matrix;
|
|
mod peripherals;
|
|
mod ui;
|
|
mod vial;
|
|
|
|
#[cfg(feature = "alt-log")]
|
|
mod console;
|
|
|
|
// This creates a default app-descriptor required by the esp-idf bootloader.
|
|
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
|
esp_bootloader_esp_idf::esp_app_desc!();
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "trace")] {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Trace;
|
|
} else if #[cfg(feature = "debug")] {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Debug;
|
|
} else if #[cfg(feature = "info")] {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Info;
|
|
} else if #[cfg(feature = "warn")] {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Warn;
|
|
} else if #[cfg(feature = "error")] {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Error;
|
|
} else {
|
|
const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Off;
|
|
}
|
|
}
|
|
|
|
// const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS
|
|
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
|
|
|
|
pub static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();
|
|
|
|
/// Used to signal that MCU is ready to submit the framebuffer to the LCD.
|
|
static SIGNAL_LCD_SUBMIT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
|
|
|
/// Used to signal that the MCU is ready to render the GUI.
|
|
static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
|
|
|
#[esp_rtos::main]
|
|
async fn main(_spawner: Spawner) {
|
|
#[cfg(feature = "usb-log")]
|
|
{
|
|
esp_println::logger::init_logger(LOG_LEVEL_FILTER);
|
|
info!("Logger initialized!");
|
|
}
|
|
|
|
#[cfg(feature = "rtt-log")]
|
|
rtt_target::rtt_init_log!(LOG_LEVEL_FILTER);
|
|
|
|
let config = esp_hal::Config::default()
|
|
.with_cpu_clock(CpuClock::max())
|
|
.with_psram(PsramConfig {
|
|
size: PsramSize::AutoDetect,
|
|
core_clock: Some(SpiTimingConfigCoreClock::SpiTimingConfigCoreClock80m),
|
|
flash_frequency: FlashFreq::default(),
|
|
ram_frequency: SpiRamFreq::Freq80m,
|
|
});
|
|
let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
|
|
info!("System initialized!");
|
|
|
|
#[cfg(feature = "alt-log")]
|
|
let alt_uart_rx_task = {
|
|
use esp_hal::uart::Uart;
|
|
|
|
let (uart_rx, uart_tx) = Uart::new(peripherals.UART2, Default::default())
|
|
.unwrap()
|
|
.with_tx(peripherals.GPIO12)
|
|
.with_rx(peripherals.GPIO5)
|
|
.split();
|
|
logging::setup_alternative_logging(uart_tx, LOG_LEVEL_FILTER);
|
|
info!("Logger initialized!");
|
|
console::run_console(uart_rx.into_async())
|
|
};
|
|
|
|
#[cfg(not(feature = "alt-log"))]
|
|
let alt_uart_rx_task = async {};
|
|
|
|
// Use the internal DRAM as the heap.
|
|
// TODO: Can we combine the regular link section with dram2?
|
|
// esp_alloc::heap_allocator!(size: 80 * 1024);
|
|
// esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 72 * 1024);
|
|
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024);
|
|
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
|
|
|
|
// Initialize the PSRAM allocator.
|
|
{
|
|
let (psram_offset, psram_size) = esp_hal::psram::psram_raw_parts(&peripherals.PSRAM);
|
|
unsafe {
|
|
PSRAM_ALLOCATOR.add_region(HeapRegion::new(
|
|
psram_offset,
|
|
psram_size,
|
|
MemoryCapability::External.into(),
|
|
));
|
|
}
|
|
}
|
|
|
|
info!("PSRAM allocator initialized!");
|
|
|
|
// let mut io = Io::new(peripherals.IO_MUX);
|
|
// io.set_interrupt_handler(interrupt_handler);
|
|
|
|
// info!("IO Mux initialized!");
|
|
|
|
// Enable pull-up on GPIO0 to prevent booting into download mode.
|
|
let gpio0 = Output::new(
|
|
peripherals.GPIO0,
|
|
Level::High,
|
|
OutputConfig::default().with_pull(Pull::Up),
|
|
);
|
|
|
|
// Enable LDO2
|
|
let _ = Output::new(peripherals.GPIO17, Level::High, OutputConfig::default());
|
|
|
|
// Enable antenna
|
|
let _ = Output::new(peripherals.GPIO11, Level::Low, OutputConfig::default());
|
|
|
|
// TODO: Use PWM to control the pwm_pin.
|
|
let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1));
|
|
let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default());
|
|
|
|
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
|
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
|
esp_rtos::start(
|
|
timg0.timer0, /*, software_interrupt.software_interrupt0 */
|
|
);
|
|
|
|
info!("ESP-RTOS started!");
|
|
|
|
#[cfg(feature = "ble")]
|
|
let mut host_resources = rmk::HostResources::new();
|
|
#[cfg(feature = "ble")]
|
|
let stack = {
|
|
// Enable the TRNG source, so `Trng` can be constructed.
|
|
use bt_hci::controller::ExternalController;
|
|
use esp_hal::rng::TrngSource;
|
|
use esp_radio::{Controller as RadioController, ble::controller::BleConnector};
|
|
|
|
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
|
|
let mut rng = esp_hal::rng::Trng::try_new().unwrap();
|
|
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
|
|
let radio = RADIO.init(esp_radio::init().unwrap());
|
|
let bluetooth = peripherals.BT;
|
|
let connector = BleConnector::new(radio, bluetooth, Default::default()).unwrap();
|
|
let controller: ExternalController<_, 20> = ExternalController::new(connector);
|
|
let central_addr = [0x18, 0xe2, 0x21, 0x80, 0xc0, 0xc7];
|
|
let ble_stack =
|
|
rmk::ble::build_ble_stack(controller, central_addr, &mut rng, &mut host_resources)
|
|
.await;
|
|
|
|
info!("BLE stack for RMK built!");
|
|
|
|
ble_stack
|
|
};
|
|
|
|
// Initialize USB
|
|
#[cfg(not(feature = "no-usb"))]
|
|
let usb_driver = {
|
|
use core::ptr::addr_of_mut;
|
|
use esp_hal::otg_fs::Usb;
|
|
use esp_hal::otg_fs::asynch::{Config, Driver};
|
|
|
|
static mut EP_MEMORY: [u8; 1024] = [0; 1024];
|
|
let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
|
|
// Create the driver, from the HAL.
|
|
let config = Config::default();
|
|
let driver = Driver::new(usb, unsafe { &mut *addr_of_mut!(EP_MEMORY) }, config);
|
|
|
|
info!("USB driver for RMK built!");
|
|
|
|
driver
|
|
};
|
|
|
|
// Initialize the flash
|
|
let flash = FlashStorage::new(peripherals.FLASH)
|
|
// Flash memory may not be written to while another core is executing from it.
|
|
// By default, `FlashStorage` is configured to abort the operation and log an error message.
|
|
// However, it can also be configured to auto-park the other core, such that writing to
|
|
// flash succeeds.
|
|
// Alternatively, XiP from PSRAM could be used along with the `multicore_ignore` strategy,
|
|
// to avoid having to park the other core, which could result in better performance.
|
|
// Invalid configuration would then present itself as freezing/UB.
|
|
.multicore_auto_park();
|
|
let flash = async_flash_wrapper(flash);
|
|
|
|
info!("Flash memory configured!");
|
|
|
|
let sck = Output::new(peripherals.GPIO36, Level::High, OutputConfig::default());
|
|
let mosi = Flex::new(peripherals.GPIO35);
|
|
let cs = Output::new(peripherals.GPIO6, Level::High, OutputConfig::default());
|
|
|
|
let lcd = LcdCam::new(peripherals.LCD_CAM).lcd;
|
|
let unconfigured_dpi = Dpi::new(lcd, peripherals.DMA_CH2, Default::default())
|
|
.unwrap()
|
|
.with_de(peripherals.GPIO37)
|
|
.with_pclk(peripherals.GPIO34)
|
|
.with_hsync(peripherals.GPIO44)
|
|
.with_vsync(peripherals.GPIO43)
|
|
// Blue
|
|
.with_data0(peripherals.GPIO38)
|
|
.with_data1(peripherals.GPIO39)
|
|
.with_data2(peripherals.GPIO40)
|
|
.with_data3(peripherals.GPIO41)
|
|
.with_data4(peripherals.GPIO42)
|
|
// Green
|
|
.with_data7(peripherals.GPIO13)
|
|
.with_data8(peripherals.GPIO14)
|
|
.with_data9(peripherals.GPIO15)
|
|
.with_data10(peripherals.GPIO16)
|
|
// Red
|
|
.with_data11(gpio0)
|
|
.with_data12(peripherals.GPIO1)
|
|
.with_data13(peripherals.GPIO2)
|
|
.with_data14(peripherals.GPIO3)
|
|
.with_data15(peripherals.GPIO4);
|
|
|
|
#[cfg(not(feature = "alt-log"))]
|
|
let unconfigured_dpi = unconfigured_dpi
|
|
// Green
|
|
.with_data5(peripherals.GPIO5)
|
|
.with_data6(peripherals.GPIO12);
|
|
|
|
let st7701s = St7701s::new(sck, mosi, cs, unconfigured_dpi).await;
|
|
|
|
info!("ST7701S-based LCD display initialized!");
|
|
|
|
// RMK config
|
|
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
|
|
let storage_config = StorageConfig {
|
|
start_addr: 0x3f0000,
|
|
num_sectors: 16,
|
|
..Default::default()
|
|
};
|
|
let rmk_config = RmkConfig {
|
|
vial_config,
|
|
storage_config,
|
|
..Default::default()
|
|
};
|
|
|
|
// Initialze keyboard stuffs
|
|
// Initialize the storage and keymap
|
|
let mut default_keymap = keymap::get_default_keymap();
|
|
let mut behavior_config = BehaviorConfig::default();
|
|
let mut per_key_config = PositionalConfig::default();
|
|
let (keymap, mut storage) = initialize_keymap_and_storage(
|
|
&mut default_keymap,
|
|
flash,
|
|
&storage_config,
|
|
&mut behavior_config,
|
|
&mut per_key_config,
|
|
)
|
|
.await;
|
|
|
|
info!("Initialized keymap and storage for RMK!");
|
|
|
|
// Initialize the matrix and keyboard
|
|
const I2C_ADDR_MATRIX_LEFT: I2cAddress = I2cAddress::SevenBit(0b0100000);
|
|
const I2C_ADDR_MATRIX_RIGHT: I2cAddress = I2cAddress::SevenBit(0b0100001);
|
|
|
|
let i2c = I2c::new(peripherals.I2C0, Default::default())
|
|
.unwrap()
|
|
.with_sda(peripherals.GPIO8)
|
|
.with_scl(peripherals.GPIO9);
|
|
|
|
let matrix_interrupt_low = Input::new(peripherals.GPIO7, InputConfig::default());
|
|
|
|
let mut matrix = IoeMatrix::new(
|
|
matrix_interrupt_low,
|
|
i2c.into_async(),
|
|
DefaultDebouncer::new(),
|
|
[I2C_ADDR_MATRIX_LEFT, I2C_ADDR_MATRIX_RIGHT],
|
|
)
|
|
.await;
|
|
let mut keyboard = Keyboard::new(&keymap); // Initialize the light controller
|
|
|
|
info!("Keyboard initialized!");
|
|
|
|
static FRAMEBUFFER: StaticCell<Framebuffer> = StaticCell::new();
|
|
let framebuffer = FRAMEBUFFER.init(Framebuffer::new(
|
|
360 + /* TODO: Figure out why more bytes are needed: */ 8,
|
|
960,
|
|
));
|
|
|
|
info!("Framebuffer created!");
|
|
|
|
// let window_size = [framebuffer.width, framebuffer.height];
|
|
let window_size = [framebuffer.height, framebuffer.width];
|
|
let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);
|
|
|
|
static SECOND_CORE_STACK: StaticCell<Stack<{ 8192 * 2 }>> = StaticCell::new();
|
|
let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
|
|
esp_rtos::start_second_core(
|
|
peripherals.CPU_CTRL,
|
|
software_interrupt.software_interrupt0,
|
|
software_interrupt.software_interrupt1,
|
|
second_core_stack,
|
|
move || {
|
|
// static EXECUTOR: StaticCell<InterruptExecutor<2>> = StaticCell::new();
|
|
// let exec = EXECUTOR.init(InterruptExecutor::new(
|
|
// software_interrupt.software_interrupt2,
|
|
// ));
|
|
// let spawner = exec.start(Priority::Priority3);
|
|
// spawner.must_spawn(run_renderer_task());
|
|
static EXECUTOR: StaticCell<Executor> = StaticCell::new();
|
|
let executor: &mut Executor = EXECUTOR.init(Executor::new());
|
|
executor.run(|spawner| {
|
|
let slint_backend = SlintBackend {
|
|
// peripherals: RefCell::new(Some(peripherals)),
|
|
window_size,
|
|
window: RefCell::new(None),
|
|
framebuffer: framebuffer_ptr,
|
|
};
|
|
spawner.must_spawn(run_renderer_task(slint_backend));
|
|
});
|
|
},
|
|
);
|
|
|
|
info!("Second core started!");
|
|
|
|
let hid_report_proxy_task = {
|
|
static KEYBOARD_REPORT_PROXY: Channel<CriticalSectionRawMutex, Report, 16> = Channel::new();
|
|
|
|
{
|
|
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
|
|
}
|
|
|
|
const KEYMAP_STRING: &str = include_str!("../keymaps/cz.xkb");
|
|
let keymap_string_buffer = unsafe {
|
|
let allocation = PSRAM_ALLOCATOR.alloc_caps(
|
|
MemoryCapability::External.into(),
|
|
Layout::from_size_align(KEYMAP_STRING.len(), 32).unwrap(),
|
|
);
|
|
let slice = str::from_utf8_unchecked_mut(slice::from_raw_parts_mut(
|
|
allocation,
|
|
KEYMAP_STRING.len(),
|
|
));
|
|
|
|
slice
|
|
.as_bytes_mut()
|
|
.copy_from_slice(KEYMAP_STRING.as_bytes());
|
|
|
|
Box::from_raw_in(slice as *mut str, &PSRAM_ALLOCATOR)
|
|
};
|
|
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
|
info!("Parsing XKB keymap...");
|
|
let instant_start = Instant::now();
|
|
let keymap = xkb::Keymap::new_from_string(
|
|
&context,
|
|
keymap_string_buffer,
|
|
xkb::KEYMAP_FORMAT_TEXT_V1,
|
|
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
|
)
|
|
.unwrap();
|
|
let duration = Instant::now().duration_since(instant_start);
|
|
info!(
|
|
"XKB keymap parsed successfully! Took {seconds}.{millis:03} seconds.",
|
|
seconds = duration.as_secs(),
|
|
millis = duration.as_millis() % 1_000
|
|
);
|
|
let mut state = xkb::State::new(&keymap);
|
|
let mut previous_state = KeyboardReport::default();
|
|
|
|
async move {
|
|
loop {
|
|
let report = KEYBOARD_REPORT_PROXY.receive().await;
|
|
|
|
if let Report::KeyboardReport(report) = &report {
|
|
// TODO: Process modifiers
|
|
|
|
for (keycode_old, &keycode_new) in
|
|
core::iter::zip(&mut previous_state.keycodes, &report.keycodes)
|
|
{
|
|
fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode {
|
|
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27
|
|
const UNK: u8 = 240;
|
|
#[rustfmt::skip]
|
|
const HID_KEYBOARD: [u8; 256] = [
|
|
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
|
|
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
|
|
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
|
|
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
|
|
65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,
|
|
105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
|
|
72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,
|
|
191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,
|
|
115, 114, UNK, UNK, UNK, 121, UNK, 89, 93, 124, 92, 94, 95, UNK, UNK, UNK,
|
|
122, 123, 90, 91, 85, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK,
|
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
UNK, UNK, UNK, UNK, UNK, UNK, 179, 180, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115, 114, 113,
|
|
150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140, UNK, UNK, UNK, UNK,
|
|
];
|
|
// https://cgit.freedesktop.org/xorg/driver/xf86-input-evdev/tree/src/evdev.c#n73
|
|
const MIN_KEYCODE: u8 = 8;
|
|
|
|
// TODO: The combination of these two operations should be precomputed
|
|
// in a const expr into a single look-up table.
|
|
xkb::Keycode::new(
|
|
(HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32,
|
|
)
|
|
}
|
|
|
|
if *keycode_old == 0 && keycode_new == 0 {
|
|
continue;
|
|
}
|
|
|
|
if keycode_new == 0 || ((*keycode_old != 0) && *keycode_old != keycode_new)
|
|
{
|
|
warn!("Release: 0x{:02x} ({})", *keycode_old, *keycode_old);
|
|
state.update_key(into_xkb_keycode(*keycode_old), KeyDirection::Up);
|
|
}
|
|
|
|
if *keycode_old == 0 || ((keycode_new != 0) && *keycode_old != keycode_new)
|
|
{
|
|
let keycode_new_xkb = into_xkb_keycode(keycode_new);
|
|
let string = state.key_get_utf8(keycode_new_xkb);
|
|
|
|
warn!("Pressed: 0x{:02x} ({})", keycode_new, keycode_new);
|
|
warn!("Print: {string}");
|
|
|
|
state.update_key(keycode_new_xkb, KeyDirection::Down);
|
|
}
|
|
|
|
// state.update_key(keycode, direction);
|
|
*keycode_old = keycode_new;
|
|
}
|
|
}
|
|
|
|
rmk::channel::KEYBOARD_REPORT_RECEIVER.send(report).await;
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut user_controller = UserController::new();
|
|
|
|
info!("Awaiting on all tasks...");
|
|
|
|
// TODO: Probably want to select! instead and re-try.
|
|
join_all![
|
|
// We currently send the framebuffer data using the main core, which does not seem to slow
|
|
// down the rest of the tasks too much.
|
|
run_lcd(st7701s, framebuffer),
|
|
run_devices! (
|
|
(matrix) => rmk::channel::EVENT_CHANNEL,
|
|
),
|
|
keyboard.run(), // Keyboard is special
|
|
run_rmk(
|
|
&keymap,
|
|
#[cfg(not(feature = "no-usb"))]
|
|
usb_driver,
|
|
#[cfg(feature = "ble")]
|
|
&stack,
|
|
&mut storage,
|
|
rmk_config,
|
|
),
|
|
hid_report_proxy_task,
|
|
user_controller.event_loop(),
|
|
alt_uart_rx_task
|
|
]
|
|
.await;
|
|
}
|
|
|
|
struct UserController {
|
|
sub: ControllerSub,
|
|
}
|
|
|
|
impl UserController {
|
|
fn new() -> Self {
|
|
Self {
|
|
sub: CONTROLLER_CHANNEL.subscriber().unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Controller for UserController {
|
|
type Event = ControllerEvent;
|
|
|
|
async fn process_event(&mut self, event: Self::Event) {
|
|
if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::Key(keycode))) = event
|
|
&& (KeyCode::Kb0..=KeyCode::User31).contains(&keycode)
|
|
{
|
|
warn!("{keycode:?} pressed.");
|
|
|
|
if keycode as u16 == CustomKeycodes::FOCUS_LCD as u16 {
|
|
warn!("Focus LCD pressed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn next_message(&mut self) -> Self::Event {
|
|
self.sub.next_message_pure().await
|
|
}
|
|
}
|
|
|
|
#[embassy_executor::task]
|
|
async fn run_renderer_task(backend: SlintBackend) {
|
|
// TODO: Disable rendering because it causes crashes in allocation
|
|
loop {
|
|
SIGNAL_LCD_SUBMIT.signal(());
|
|
#[cfg(feature = "limit-fps")]
|
|
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
|
|
SIGNAL_UI_RENDER.wait().await;
|
|
}
|
|
|
|
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
|
|
|
|
let main = AppWindow::new().unwrap();
|
|
|
|
// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve
|
|
// async by having only one iteration of the loop run, and `await`ing here.
|
|
// The following block is analogous to `main.run()`.
|
|
{
|
|
main.show().unwrap();
|
|
|
|
loop {
|
|
slint::run_event_loop().unwrap();
|
|
SIGNAL_LCD_SUBMIT.signal(());
|
|
#[cfg(feature = "limit-fps")]
|
|
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
|
|
SIGNAL_UI_RENDER.wait().await;
|
|
}
|
|
|
|
#[expect(unreachable_code)]
|
|
main.hide().unwrap();
|
|
}
|
|
}
|
|
|
|
struct Framebuffer {
|
|
width: u32,
|
|
height: u32,
|
|
dma_buf: Option<DmaTxBuf>,
|
|
}
|
|
|
|
impl Framebuffer {
|
|
pub fn new(width: u32, height: u32) -> Self {
|
|
let buffer_len = width as usize * height as usize * core::mem::size_of::<u16>();
|
|
// Allocate the framebuffer in the external PSRAM memory.
|
|
// Note: We just leak this buffer.
|
|
let buffer_ptr = unsafe {
|
|
// ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM
|
|
// have extra alignment requirements. The address and size of the buffer pointed to by each
|
|
// descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3.
|
|
PSRAM_ALLOCATOR.alloc_caps(
|
|
MemoryCapability::External.into(),
|
|
Layout::from_size_align(buffer_len, 32).unwrap(),
|
|
)
|
|
};
|
|
let buffer = unsafe { core::slice::from_raw_parts_mut(buffer_ptr, buffer_len) };
|
|
let burst_config: BurstConfig = ExternalBurstConfig::Size16.into();
|
|
|
|
info!(
|
|
"PSRAM SPI burst config: max_compatible_chunk_size={}",
|
|
burst_config.max_compatible_chunk_size()
|
|
);
|
|
let dma_buf_descs_len = esp_hal::dma::descriptor_count(
|
|
buffer_len,
|
|
burst_config.max_compatible_chunk_size(),
|
|
false,
|
|
);
|
|
// Descriptors are initialized by `DmaTxBuf::new`.
|
|
let dma_buf_descs =
|
|
Box::leak(vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice());
|
|
let dma_buf = DmaTxBuf::new(dma_buf_descs, buffer).unwrap();
|
|
|
|
Self {
|
|
width,
|
|
height,
|
|
dma_buf: Some(dma_buf),
|
|
}
|
|
}
|
|
|
|
pub fn as_target_pixels(&mut self) -> &mut [Rgb565Pixel] {
|
|
bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.dma_buf.as_mut().unwrap().as_mut_slice())
|
|
}
|
|
}
|
|
|
|
#[embassy_executor::task]
|
|
async fn run_lcd_task(st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) {
|
|
run_lcd(st7701s, framebuffer).await
|
|
}
|
|
|
|
async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) {
|
|
loop {
|
|
// Timer::after(Duration::from_millis(100)).await;
|
|
// yield_now().await;
|
|
SIGNAL_LCD_SUBMIT.wait().await;
|
|
|
|
// TODO: Use bounce buffers:
|
|
// https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
|
|
// They need to be implemented in esp-hal.
|
|
let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
|
|
Err((error, result_dpi, result_dma_buf)) => {
|
|
error!(
|
|
"An error occurred while initiating transfer of the framebuffer to the LCD display: {error:?}"
|
|
);
|
|
st7701s.dpi = result_dpi;
|
|
framebuffer.dma_buf = Some(result_dma_buf);
|
|
continue;
|
|
}
|
|
Ok(transfer) => transfer,
|
|
};
|
|
|
|
// This could be used to allow other tasks to be executed on the first core, but that causes
|
|
// the flash to be accessed, which interferes with the framebuffer transfer.
|
|
// For that reason, it is disabled, and this task blocks the first core, until the transfer
|
|
// is complete.
|
|
#[cfg(not(feature = "limit-fps"))]
|
|
while !transfer.is_done() {
|
|
// Timer::after_millis(1).await;
|
|
yield_now().await;
|
|
}
|
|
|
|
let result;
|
|
let dma_buf;
|
|
(result, st7701s.dpi, dma_buf) = transfer.wait();
|
|
framebuffer.dma_buf = Some(dma_buf);
|
|
|
|
SIGNAL_UI_RENDER.signal(());
|
|
|
|
if let Err(error) = result {
|
|
error!(
|
|
"An error occurred while transferring framebuffer to the LCD display: {error:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// // TODO: Not needed currently. If it is ever enabled, don't forget to register it in Io.
|
|
// #[handler]
|
|
// #[ram] // TODO: Is this necessary?
|
|
// fn interrupt_handler() {
|
|
// // esp_println::println!(
|
|
// // "GPIO Interrupt with priority {}",
|
|
// // esp_hal::xtensa_lx::interrupt::get_level()
|
|
// // );
|
|
// }
|