mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-23 11:54:11 +00:00
481 lines
18 KiB
Rust
481 lines
18 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#[panic_handler]
|
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
|
esp_println::println!("Panic: {:?}", info);
|
|
loop {}
|
|
}
|
|
|
|
use alloc::boxed::Box;
|
|
use alloc::rc::Rc;
|
|
use core::cell::RefCell;
|
|
use embedded_graphics_core::draw_target::DrawTarget;
|
|
use embedded_graphics_core::geometry::OriginDimensions;
|
|
use embedded_graphics_core::pixelcolor::RgbColor;
|
|
use embedded_hal::delay::DelayNs;
|
|
use embedded_hal::digital::OutputPin;
|
|
use embedded_hal_bus::spi::ExclusiveDevice;
|
|
use esp_alloc as _;
|
|
use esp_backtrace as _;
|
|
use esp_hal::clock::CpuClock;
|
|
use esp_hal::peripherals::Peripherals;
|
|
use esp_hal::time::Instant;
|
|
use esp_hal::{
|
|
delay::Delay,
|
|
gpio::{Level, Output, OutputConfig},
|
|
i2c::master::I2c,
|
|
spi::master::{Config as SpiConfig, Spi},
|
|
spi::Mode as SpiMode,
|
|
time::Rate,
|
|
};
|
|
use esp_println::logger::init_logger_from_env;
|
|
use log::{error, info};
|
|
use mipidsi::options::{ColorInversion, ColorOrder};
|
|
|
|
// Touch support imports
|
|
use embedded_hal_bus::i2c::RefCellDevice;
|
|
use ft3x68_rs::{Ft3x68Driver, ResetInterface};
|
|
use slint::platform::{PointerEventButton, WindowEvent};
|
|
use slint::PhysicalPosition;
|
|
use static_cell::StaticCell;
|
|
|
|
// FT6336U I2C address (compatible with FT3x68 driver)
|
|
const FT6336U_DEVICE_ADDRESS: u8 = 0x38;
|
|
|
|
// AW9523 I2C address
|
|
const AW9523_I2C_ADDRESS: u8 = 0x58;
|
|
|
|
/// Touch reset implementation via AW9523 GPIO expander using direct I2C commands
|
|
/// Based on the AW9523 datasheet and M5Stack CoreS3 schematics
|
|
pub struct TouchResetDriverAW9523<I2C> {
|
|
i2c: I2C,
|
|
}
|
|
|
|
impl<I2C> TouchResetDriverAW9523<I2C> {
|
|
pub fn new(i2c: I2C) -> Self {
|
|
TouchResetDriverAW9523 { i2c }
|
|
}
|
|
}
|
|
|
|
impl<I2C> ResetInterface for TouchResetDriverAW9523<I2C>
|
|
where
|
|
I2C: embedded_hal::i2c::I2c,
|
|
{
|
|
type Error = I2C::Error;
|
|
|
|
fn reset(&mut self) -> Result<(), Self::Error> {
|
|
let delay = Delay::new();
|
|
|
|
// AW9523 register addresses:
|
|
// 0x02: Port 0 Configuration (0=output, 1=input)
|
|
// 0x03: Port 1 Configuration (0=output, 1=input)
|
|
// 0x04: Port 0 Output (pin values for outputs)
|
|
// 0x05: Port 1 Output (pin values for outputs)
|
|
|
|
// Configure P0_0 (touch reset) as output (bit 0 = 0)
|
|
// Keep other pins as they are - read current config first
|
|
let mut config_p0 = [0u8; 1];
|
|
self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x02], &mut config_p0)?;
|
|
let new_config_p0 = config_p0[0] & !0x01; // Clear bit 0 to make P0_0 output
|
|
self.i2c.write(AW9523_I2C_ADDRESS, &[0x02, new_config_p0])?;
|
|
|
|
// Pull reset (P0_0) low
|
|
let mut output_p0 = [0u8; 1];
|
|
self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x04], &mut output_p0)?;
|
|
let new_output_low = output_p0[0] & !0x01; // Clear bit 0 to pull P0_0 low
|
|
self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_low])?;
|
|
delay.delay_millis(10);
|
|
|
|
// Pull reset (P0_0) high
|
|
let new_output_high = output_p0[0] | 0x01; // Set bit 0 to pull P0_0 high
|
|
self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_high])?;
|
|
delay.delay_millis(300);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct EspBackend {
|
|
window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
|
|
peripherals: RefCell<Option<Peripherals>>,
|
|
}
|
|
|
|
impl slint::platform::Platform for EspBackend {
|
|
fn create_window_adapter(
|
|
&self,
|
|
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
|
|
let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
|
|
slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
|
|
);
|
|
self.window.replace(Some(window.clone()));
|
|
Ok(window)
|
|
}
|
|
|
|
fn duration_since_start(&self) -> core::time::Duration {
|
|
core::time::Duration::from_millis(Instant::now().duration_since_epoch().as_millis())
|
|
}
|
|
|
|
fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
|
|
self.run_event_loop()
|
|
}
|
|
}
|
|
|
|
impl Default for EspBackend {
|
|
fn default() -> Self {
|
|
EspBackend { window: RefCell::new(None), peripherals: RefCell::new(None) }
|
|
}
|
|
}
|
|
|
|
/// Initializes the heap and sets the Slint platform.
|
|
pub fn init() {
|
|
// Initialize peripherals first.
|
|
let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
|
|
init_logger_from_env();
|
|
info!("Peripherals initialized");
|
|
|
|
// Initialize the PSRAM allocator.
|
|
esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
|
|
|
|
// Create an EspBackend that now owns the peripherals.
|
|
slint::platform::set_platform(Box::new(EspBackend {
|
|
peripherals: RefCell::new(Some(peripherals)),
|
|
window: RefCell::new(None),
|
|
}))
|
|
.expect("backend already initialized");
|
|
}
|
|
|
|
/// Initialize the AXP2101 power management unit for M5Stack CoreS3 using shared I2C
|
|
/// This implements the exact same sequence as the working custom implementation
|
|
/// Based on https://github.com/tuupola/axp192
|
|
/// and https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp
|
|
fn init_axp2101_power<I2C>(mut i2c_device: I2C) -> Result<(), ()>
|
|
where
|
|
I2C: embedded_hal::i2c::I2c,
|
|
{
|
|
const AXP2101_ADDRESS: u8 = 0x34;
|
|
|
|
info!("Initializing AXP2101 power management with M5Stack CoreS3 sequence...");
|
|
|
|
// This sequence matches exactly the working custom implementation:
|
|
// 1. CHG_LED register (0x69) <- 0x35 (0b00110101)
|
|
// 2. ALDO_ENABLE register (0x90) <- 0xBF
|
|
// 3. ALDO4 register (0x95) <- 0x1C (0b00011100)
|
|
|
|
// Step 1: Configure charge LED (register 0x69 = 105 decimal)
|
|
if i2c_device.write(AXP2101_ADDRESS, &[0x69, 0x35]).is_err() {
|
|
error!("Failed to write to CHG_LED register (0x69)");
|
|
return Err(());
|
|
}
|
|
info!("AXP2101: CHG_LED configured (0x69 <- 0x35)");
|
|
|
|
// Step 2: Enable ALDO outputs (register 0x90 = 144 decimal)
|
|
if i2c_device.write(AXP2101_ADDRESS, &[0x90, 0xBF]).is_err() {
|
|
error!("Failed to write to ALDO_ENABLE register (0x90)");
|
|
return Err(());
|
|
}
|
|
info!("AXP2101: ALDO outputs enabled (0x90 <- 0xBF)");
|
|
|
|
// Step 3: Configure ALDO4 voltage (register 0x95 = 149 decimal)
|
|
if i2c_device.write(AXP2101_ADDRESS, &[0x95, 0x1C]).is_err() {
|
|
error!("Failed to write to ALDO4 register (0x95)");
|
|
return Err(());
|
|
}
|
|
info!("AXP2101: ALDO4 voltage configured (0x95 <- 0x1C)");
|
|
|
|
info!("AXP2101 power management initialized successfully with M5Stack CoreS3 sequence");
|
|
Ok(())
|
|
}
|
|
|
|
/// Initialize the AW9523 GPIO expander for M5Stack CoreS3 using shared I2C
|
|
/// This implements the exact same sequence as the working custom implementation
|
|
/// Based on: https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp
|
|
fn init_aw9523_gpio_expander<I2C>(mut i2c_device: I2C) -> Result<(), ()>
|
|
where
|
|
I2C: embedded_hal::i2c::I2c,
|
|
{
|
|
info!("Initializing AW9523 GPIO expander with M5Stack CoreS3 sequence...");
|
|
|
|
// Step 1: Configure Port 0 Configuration (register 0x02) <- 0b00000101 (0x05)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x02, 0b00000101]).is_err() {
|
|
error!("Failed to write to AW9523 Port 0 Configuration register (0x02)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Port 0 Configuration set (0x02 <- 0x05)");
|
|
|
|
// Step 2: Configure Port 1 Configuration (register 0x03) <- 0b00000011 (0x03)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x03, 0b00000011]).is_err() {
|
|
error!("Failed to write to AW9523 Port 1 Configuration register (0x03)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Port 1 Configuration set (0x03 <- 0x03)");
|
|
|
|
// Step 3: Configure Port 0 Output (register 0x04) <- 0b00011000 (0x18)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x04, 0b00011000]).is_err() {
|
|
error!("Failed to write to AW9523 Port 0 Output register (0x04)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Port 0 Output set (0x04 <- 0x18)");
|
|
|
|
// Step 4: Configure Port 1 Output (register 0x05) <- 0b00001100 (0x0C)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x05, 0b00001100]).is_err() {
|
|
error!("Failed to write to AW9523 Port 1 Output register (0x05)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Port 1 Output set (0x05 <- 0x0C)");
|
|
|
|
// Step 5: Configure register 0x11 <- 0b00010000 (0x10)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x11, 0b00010000]).is_err() {
|
|
error!("Failed to write to AW9523 register (0x11)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Register 0x11 configured (0x11 <- 0x10)");
|
|
|
|
// Step 6: Configure register 0x13 <- 0b11111111 (0xFF)
|
|
if i2c_device.write(AW9523_I2C_ADDRESS, &[0x13, 0b11111111]).is_err() {
|
|
error!("Failed to write to AW9523 register (0x13)");
|
|
return Err(());
|
|
}
|
|
info!("AW9523: Register 0x13 configured (0x13 <- 0xFF)");
|
|
|
|
info!("AW9523 GPIO expander initialized successfully with M5Stack CoreS3 sequence");
|
|
Ok(())
|
|
}
|
|
|
|
impl EspBackend {
|
|
fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
|
|
// Take and configure peripherals.
|
|
let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");
|
|
let mut delay = Delay::new();
|
|
|
|
// --- Initialize I2C bus for all I2C devices (AXP2101, AW9523, touch controller) ---
|
|
let power_i2c = I2c::new(
|
|
peripherals.I2C0,
|
|
esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
|
|
)
|
|
.unwrap()
|
|
.with_sda(peripherals.GPIO12) // AXP2101 SDA
|
|
.with_scl(peripherals.GPIO11); // AXP2101 SCL
|
|
|
|
// --- Use StaticCell to create a shared I2C bus for all I2C devices ---
|
|
static I2C_BUS: StaticCell<RefCell<I2c<'static, esp_hal::Blocking>>> = StaticCell::new();
|
|
let i2c_bus = I2C_BUS.init(RefCell::new(power_i2c));
|
|
|
|
// --- Begin AXP2101 Power Management Initialization ---
|
|
// Initialize power management using shared I2C bus - critical for M5Stack CoreS3
|
|
match init_axp2101_power(RefCellDevice::new(i2c_bus)) {
|
|
Ok(_) => {
|
|
info!("Power management initialized successfully");
|
|
}
|
|
Err(_) => {
|
|
error!("Failed to initialize AXP2101 power management");
|
|
// Return error since power management is critical
|
|
return Err(slint::PlatformError::Other("AXP2101 initialization failed".into()));
|
|
}
|
|
};
|
|
|
|
// Small delay to let power rails stabilize
|
|
delay.delay_ms(100);
|
|
|
|
// --- Begin AW9523 GPIO Expander Initialization ---
|
|
// Initialize AW9523 GPIO expander using M5Stack CoreS3 specific sequence
|
|
match init_aw9523_gpio_expander(RefCellDevice::new(i2c_bus)) {
|
|
Ok(_) => {
|
|
info!("AW9523 GPIO expander initialized successfully");
|
|
}
|
|
Err(_) => {
|
|
error!("Failed to initialize AW9523 GPIO expander");
|
|
// Return error since GPIO expander is needed for touch
|
|
return Err(slint::PlatformError::Other("AW9523 initialization failed".into()));
|
|
}
|
|
};
|
|
// --- End AW9523 Initialization ---
|
|
|
|
// --- Begin SPI and Display Initialization ---
|
|
let spi = Spi::<esp_hal::Blocking>::new(
|
|
peripherals.SPI2,
|
|
SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
|
|
)
|
|
.unwrap()
|
|
.with_sck(peripherals.GPIO36) // SPI Clock
|
|
.with_mosi(peripherals.GPIO37); // SPI MOSI
|
|
|
|
// Display control pins
|
|
let dc = Output::new(peripherals.GPIO35, Level::Low, OutputConfig::default()); // D/C pin
|
|
let cs = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default()); // CS pin
|
|
let reset = Output::new(peripherals.GPIO15, Level::High, OutputConfig::default()); // Reset pin
|
|
|
|
// Wrap SPI into a bus.
|
|
let spi_delay = Delay::new();
|
|
let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap();
|
|
|
|
// Create buffer for display interface
|
|
let mut buffer = [0u8; 512];
|
|
let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer);
|
|
|
|
// Add small delay before display initialization
|
|
delay.delay_ms(10);
|
|
|
|
// Initialize the display with settings
|
|
let mut display = mipidsi::Builder::new(mipidsi::models::ILI9342CRgb565, di)
|
|
.reset_pin(reset)
|
|
.display_size(320, 240)
|
|
.color_order(ColorOrder::Bgr)
|
|
.invert_colors(ColorInversion::Inverted)
|
|
.init(&mut delay)
|
|
.unwrap();
|
|
|
|
// Clear display to test it's working
|
|
use embedded_graphics::pixelcolor::Rgb565;
|
|
display
|
|
.clear(Rgb565::BLUE)
|
|
.map_err(|_| slint::PlatformError::Other("Display clear failed".into()))?;
|
|
info!("Display initialized and cleared to blue");
|
|
|
|
// Set up the backlight pin (controlled via AXP2101, but we can use GPIO for basic control)
|
|
let mut backlight = Output::new(peripherals.GPIO16, Level::Low, OutputConfig::default());
|
|
backlight.set_high(); // Enable backlight
|
|
|
|
// Update the Slint window size from the display (320x240 for M5Stack CoreS3)
|
|
let size = display.size();
|
|
let size = slint::PhysicalSize::new(size.width, size.height);
|
|
self.window.borrow().as_ref().unwrap().set_size(size);
|
|
|
|
// --- End Display Initialization ---
|
|
|
|
// --- Begin Touch Initialization ---
|
|
info!("Initializing FT6336U touch controller...");
|
|
|
|
// Create touch reset driver using shared I2C bus
|
|
let touch_reset = TouchResetDriverAW9523::new(RefCellDevice::new(i2c_bus));
|
|
|
|
// Initialize FT6336U touch driver using shared I2C bus
|
|
let mut touch_driver = Ft3x68Driver::new(
|
|
RefCellDevice::new(i2c_bus),
|
|
FT6336U_DEVICE_ADDRESS,
|
|
touch_reset,
|
|
delay,
|
|
);
|
|
|
|
match touch_driver.initialize() {
|
|
Ok(_) => info!("FT6336U touch controller initialized successfully"),
|
|
Err(e) => {
|
|
error!("Touch initialization failed: {:?}", e);
|
|
// Continue without touch
|
|
}
|
|
}
|
|
// --- End Touch Initialization ---
|
|
|
|
// Prepare a draw buffer for the Slint software renderer
|
|
let mut buffer_provider = DrawBuffer {
|
|
display,
|
|
buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320],
|
|
};
|
|
|
|
// Variable to track the last touch position
|
|
let mut last_touch = None;
|
|
|
|
// Main event loop
|
|
loop {
|
|
slint::platform::update_timers_and_animations();
|
|
|
|
if let Some(window) = self.window.borrow().clone() {
|
|
// Poll touch input using FT3x68Driver
|
|
match touch_driver.touch1() {
|
|
Ok(touch_state) => {
|
|
match touch_state {
|
|
ft3x68_rs::TouchState::Pressed(touch_point) => {
|
|
info!("Touch detected: x={}, y={}", touch_point.x, touch_point.y);
|
|
|
|
// Convert touch coordinates to logical position
|
|
let pos = PhysicalPosition::new(
|
|
touch_point.x as i32,
|
|
touch_point.y as i32,
|
|
)
|
|
.to_logical(window.scale_factor());
|
|
|
|
if let Some(prev_pos) = last_touch.replace(pos) {
|
|
// If position changed, send a PointerMoved event
|
|
if prev_pos != pos {
|
|
let _ =
|
|
window.try_dispatch_event(WindowEvent::PointerMoved {
|
|
position: pos,
|
|
});
|
|
}
|
|
} else {
|
|
// No previous touch, send a PointerPressed event
|
|
let _ =
|
|
window.try_dispatch_event(WindowEvent::PointerPressed {
|
|
position: pos,
|
|
button: PointerEventButton::Left,
|
|
});
|
|
}
|
|
}
|
|
ft3x68_rs::TouchState::Released => {
|
|
// Touch was released, send PointerReleased if we had a previous touch
|
|
if let Some(pos) = last_touch.take() {
|
|
let _ =
|
|
window.try_dispatch_event(WindowEvent::PointerReleased {
|
|
position: pos,
|
|
button: PointerEventButton::Left,
|
|
});
|
|
let _ = window.try_dispatch_event(WindowEvent::PointerExited);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// Touch error - ignore and continue
|
|
}
|
|
}
|
|
|
|
// Render the window if needed
|
|
window.draw_if_needed(|renderer| {
|
|
renderer.render_by_line(&mut buffer_provider);
|
|
});
|
|
|
|
if window.has_active_animations() {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provides a draw buffer for the MinimalSoftwareWindow renderer.
|
|
struct DrawBuffer<'a, Display> {
|
|
display: Display,
|
|
buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel],
|
|
}
|
|
|
|
impl<
|
|
DI: mipidsi::interface::Interface<Word = u8>,
|
|
RST: OutputPin<Error = core::convert::Infallible>,
|
|
> slint::platform::software_renderer::LineBufferProvider
|
|
for &mut DrawBuffer<'_, mipidsi::Display<DI, mipidsi::models::ILI9342CRgb565, RST>>
|
|
{
|
|
type TargetPixel = slint::platform::software_renderer::Rgb565Pixel;
|
|
|
|
fn process_line(
|
|
&mut self,
|
|
line: usize,
|
|
range: core::ops::Range<usize>,
|
|
render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]),
|
|
) {
|
|
let buffer = &mut self.buffer[range.clone()];
|
|
render_fn(buffer);
|
|
|
|
// Update the display with the rendered line
|
|
self.display
|
|
.set_pixels(
|
|
range.start as u16,
|
|
line as u16,
|
|
range.end as u16,
|
|
line as u16,
|
|
buffer
|
|
.iter()
|
|
.map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()),
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|