end-to-end working: ESP32 connects to server, renders widgets
boot logo (procedural hexagon + K), WiFi (WPA auto-detect with retries), TCP client connects and receives ScreenUpdate/DataUpdate messages, display renders widget data. Makefile with esp-flash/server/desktop targets. known issues: boot logo not cleared, text overlaps, occasional reconnect
This commit is contained in:
@@ -14,3 +14,8 @@ CONFIG_SPI_MASTER_IN_IRAM=y
|
||||
|
||||
# Use single large app partition (no OTA)
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
# Watchdog
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
|
||||
|
||||
118
crates/client-esp32/src/boot/logo.rs
Normal file
118
crates/client-esp32/src/boot/logo.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use embedded_graphics::{
|
||||
pixelcolor::Rgb565,
|
||||
prelude::*,
|
||||
primitives::{Triangle, PrimitiveStyle},
|
||||
};
|
||||
|
||||
fn rgb(r: u8, g: u8, b: u8) -> Rgb565 {
|
||||
Rgb565::new(r >> 3, g >> 2, b >> 3)
|
||||
}
|
||||
|
||||
fn orange() -> Rgb565 { rgb(0xF7, 0x94, 0x1D) }
|
||||
fn dark() -> Rgb565 { rgb(0x1A, 0x1A, 0x1A) }
|
||||
fn light() -> Rgb565 { rgb(0xDB, 0xDB, 0xDB) }
|
||||
fn bg() -> Rgb565 { rgb(0x1E, 0x1E, 0x1E) }
|
||||
|
||||
fn p(x: i32, y: i32) -> Point {
|
||||
Point::new(x, y)
|
||||
}
|
||||
|
||||
fn scaled(points: &[(f32, f32)], scale: f32, cx: f32, cy: f32) -> Vec<(i32, i32)> {
|
||||
points.iter().map(|(x, y)| {
|
||||
((x * scale + cx) as i32, (y * scale + cy) as i32)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn fill_triangles<D: DrawTarget<Color = Rgb565>>(
|
||||
display: &mut D,
|
||||
tris: &[(usize, usize, usize)],
|
||||
pts: &[(i32, i32)],
|
||||
color: Rgb565,
|
||||
) {
|
||||
let style = PrimitiveStyle::with_fill(color);
|
||||
for &(a, b, c) in tris {
|
||||
let _ = Triangle::new(
|
||||
p(pts[a].0, pts[a].1),
|
||||
p(pts[b].0, pts[b].1),
|
||||
p(pts[c].0, pts[c].1),
|
||||
)
|
||||
.into_styled(style)
|
||||
.draw(display);
|
||||
}
|
||||
}
|
||||
|
||||
// Outer hexagon vertices (150x150 viewBox)
|
||||
const OUTER_HEX: [(f32, f32); 6] = [
|
||||
(75.0, 0.0), (150.0, 37.5), (150.0, 112.5),
|
||||
(75.0, 150.0), (0.0, 112.5), (0.0, 37.5),
|
||||
];
|
||||
|
||||
// Converted from SVG: (150,75)(112.5,150)(37.5,150)(0,75)(37.5,0)(112.5,0)
|
||||
// Reordered to start from top for cleaner fan
|
||||
const OUTER_HEX_SVG: [(f32, f32); 6] = [
|
||||
(37.5, 0.0), (112.5, 0.0), (150.0, 75.0),
|
||||
(112.5, 150.0), (37.5, 150.0), (0.0, 75.0),
|
||||
];
|
||||
|
||||
const INNER_HEX_SVG: [(f32, f32); 6] = [
|
||||
(49.25, 23.0), (101.75, 23.0), (128.0, 75.5),
|
||||
(101.75, 128.0), (49.25, 128.0), (23.0, 75.5),
|
||||
];
|
||||
|
||||
// Fan triangulation works for convex hexagons
|
||||
const HEX_TRIS: [(usize, usize, usize); 4] = [
|
||||
(0, 1, 2), (0, 2, 3), (0, 3, 4), (0, 4, 5),
|
||||
];
|
||||
|
||||
// K letter vertices from SVG path, in order
|
||||
const K_POINTS: [(f32, f32); 10] = [
|
||||
(53.41, 49.02), // 0: top-left vertical bar
|
||||
(63.66, 49.02), // 1: top-right vertical bar
|
||||
(63.66, 72.87), // 2: inner notch (where diagonals meet bar)
|
||||
(83.48, 49.02), // 3: top arm inner
|
||||
(95.85, 49.02), // 4: top arm outer
|
||||
(74.17, 75.51), // 5: center junction
|
||||
(95.85, 101.07), // 6: bottom arm outer
|
||||
(82.95, 101.07), // 7: bottom arm inner
|
||||
(67.38, 82.23), // 8: bottom notch
|
||||
(53.41, 98.04), // 9: bottom-left vertical bar
|
||||
];
|
||||
|
||||
// Manual triangulation for the concave K shape
|
||||
const K_TRIS: [(usize, usize, usize); 8] = [
|
||||
// Vertical bar
|
||||
(0, 1, 9),
|
||||
(1, 8, 9),
|
||||
// Upper diagonal
|
||||
(1, 2, 5),
|
||||
(2, 3, 5),
|
||||
(3, 4, 5),
|
||||
// Lower diagonal
|
||||
(1, 5, 8),
|
||||
(5, 7, 8),
|
||||
(5, 6, 7),
|
||||
];
|
||||
|
||||
pub fn draw_logo<D: DrawTarget<Color = Rgb565>>(
|
||||
display: &mut D,
|
||||
screen_w: u16,
|
||||
screen_h: u16,
|
||||
) {
|
||||
let logo_size = (screen_h as f32 * 0.7).min(screen_w as f32 * 0.8);
|
||||
let scale = logo_size / 150.0;
|
||||
let cx = (screen_w as f32 - 150.0 * scale) / 2.0;
|
||||
let cy = (screen_h as f32 - 150.0 * scale) / 2.0;
|
||||
|
||||
let outer = scaled(&OUTER_HEX_SVG, scale, cx, cy);
|
||||
fill_triangles(display, &HEX_TRIS, &outer, orange());
|
||||
|
||||
let inner = scaled(&INNER_HEX_SVG, scale, cx, cy);
|
||||
fill_triangles(display, &HEX_TRIS, &inner, dark());
|
||||
|
||||
let k = scaled(&K_POINTS, scale, cx, cy);
|
||||
fill_triangles(display, &K_TRIS, &k, light());
|
||||
}
|
||||
|
||||
pub fn draw_background<D: DrawTarget<Color = Rgb565>>(display: &mut D) {
|
||||
let _ = display.clear(bg());
|
||||
}
|
||||
14
crates/client-esp32/src/boot/mod.rs
Normal file
14
crates/client-esp32/src/boot/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod logo;
|
||||
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::prelude::*;
|
||||
use esp_idf_hal::delay::Ets;
|
||||
use crate::config::{SCREEN_WIDTH, SCREEN_HEIGHT};
|
||||
|
||||
pub fn run<D: DrawTarget<Color = Rgb565>>(display: &mut D) {
|
||||
logo::draw_background(display);
|
||||
Ets::delay_ms(200);
|
||||
|
||||
logo::draw_logo(display, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
Ets::delay_ms(1500);
|
||||
}
|
||||
@@ -11,6 +11,10 @@ pub const SCREEN: BoundingBox = BoundingBox {
|
||||
height: SCREEN_HEIGHT,
|
||||
};
|
||||
|
||||
// Physical panel dimensions (before rotation)
|
||||
pub const PANEL_WIDTH: u16 = 240;
|
||||
pub const PANEL_HEIGHT: u16 = 320;
|
||||
|
||||
pub const SPI_BAUDRATE: Hertz = Hertz(26_000_000);
|
||||
pub const SPI_BUFFER_SIZE: usize = 512;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use esp_idf_hal::delay::{Delay, Ets};
|
||||
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin, PinDriver};
|
||||
use esp_idf_hal::spi::{SpiDeviceDriver, SpiDriver, SpiDriverConfig, SPI2, config::Config as SpiConfig};
|
||||
use mipidsi::{Builder, models::ILI9341Rgb565, options::{Orientation, Rotation}, interface::SpiInterface};
|
||||
use mipidsi::{Builder, models::ILI9341Rgb565, options::{ColorOrder, Orientation, Rotation}, interface::SpiInterface};
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::prelude::*;
|
||||
use log::info;
|
||||
|
||||
use crate::config::{SCREEN_WIDTH, SCREEN_HEIGHT, SPI_BAUDRATE, SPI_BUFFER_SIZE};
|
||||
use crate::config::{self, SPI_BAUDRATE, SPI_BUFFER_SIZE};
|
||||
use crate::adapters::display::Esp32DisplayAdapter;
|
||||
use crate::boot;
|
||||
|
||||
pub struct DisplayHardware<'d> {
|
||||
pub spi: SPI2<'d>,
|
||||
@@ -40,19 +41,20 @@ pub fn init(hw: DisplayHardware<'static>) -> Esp32DisplayAdapter {
|
||||
rst_pin.set_high().unwrap();
|
||||
Ets::delay_ms(120);
|
||||
|
||||
// Keep RST pin high — dropping PinDriver may release the GPIO
|
||||
let rst_pin: &'static mut _ = Box::leak(Box::new(rst_pin));
|
||||
let _rst_pin: &'static mut _ = Box::leak(Box::new(rst_pin));
|
||||
|
||||
let buf: &'static mut [u8; SPI_BUFFER_SIZE] = Box::leak(Box::new([0u8; SPI_BUFFER_SIZE]));
|
||||
let di = SpiInterface::new(spi_device, dc_pin, buf);
|
||||
|
||||
info!("Initializing ILI9341...");
|
||||
let mut raw_display = Builder::new(ILI9341Rgb565, di)
|
||||
.display_size(240, 320)
|
||||
.display_size(config::PANEL_WIDTH, config::PANEL_HEIGHT)
|
||||
.orientation(Orientation { rotation: Rotation::Deg90, mirrored: true })
|
||||
.color_order(ColorOrder::Bgr)
|
||||
.init(&mut Delay::new_default())
|
||||
.expect("Display init failed");
|
||||
|
||||
raw_display.clear(Rgb565::BLACK).unwrap();
|
||||
boot::run(&mut raw_display);
|
||||
|
||||
Esp32DisplayAdapter::new(raw_display)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use esp_idf_hal::modem::Modem;
|
||||
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||
use esp_idf_svc::wifi::{
|
||||
AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi,
|
||||
};
|
||||
use log::info;
|
||||
use log::{info, error};
|
||||
|
||||
const MAX_RETRIES: u32 = 5;
|
||||
const RETRY_DELAY: Duration = Duration::from_secs(3);
|
||||
|
||||
pub fn init<'d>(
|
||||
modem: Modem<'d>,
|
||||
@@ -22,17 +27,30 @@ pub fn init<'d>(
|
||||
let config = Configuration::Client(ClientConfiguration {
|
||||
ssid: ssid.try_into().unwrap(),
|
||||
password: password.try_into().unwrap(),
|
||||
auth_method: AuthMethod::WPA2Personal,
|
||||
auth_method: AuthMethod::WPA,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
wifi.set_configuration(&config).map_err(|e| format!("wifi config: {e:?}"))?;
|
||||
wifi.start().map_err(|e| format!("wifi start: {e:?}"))?;
|
||||
|
||||
info!("WiFi started, connecting...");
|
||||
wifi.connect().map_err(|e| format!("wifi connect: {e:?}"))?;
|
||||
wifi.wait_netif_up().map_err(|e| format!("wifi netif: {e:?}"))?;
|
||||
info!("WiFi started, connecting to '{ssid}'...");
|
||||
|
||||
info!("WiFi connected");
|
||||
Ok(wifi)
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
match wifi.connect() {
|
||||
Ok(()) => {
|
||||
wifi.wait_netif_up().map_err(|e| format!("wifi netif: {e:?}"))?;
|
||||
info!("WiFi connected");
|
||||
return Ok(wifi);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("WiFi connect attempt {attempt}/{MAX_RETRIES} failed: {e:?}");
|
||||
if attempt < MAX_RETRIES {
|
||||
thread::sleep(RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("WiFi failed after {MAX_RETRIES} attempts"))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
mod adapters;
|
||||
mod boot;
|
||||
mod config;
|
||||
mod hal;
|
||||
mod tasks;
|
||||
|
||||
use client_domain::{BoundingBox, DisplayPort};
|
||||
use std::sync::mpsc;
|
||||
use esp_idf_hal::peripherals::Peripherals;
|
||||
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||
use log::info;
|
||||
|
||||
const WIFI_SSID: &str = env!("KFRAME_WIFI_SSID");
|
||||
const WIFI_PASS: &str = env!("KFRAME_WIFI_PASS");
|
||||
const SERVER_ADDR: &str = env!("KFRAME_SERVER_ADDR");
|
||||
|
||||
fn main() {
|
||||
esp_idf_svc::sys::link_patches();
|
||||
esp_idf_svc::log::EspLogger::initialize_default();
|
||||
@@ -14,8 +21,10 @@ fn main() {
|
||||
info!("=== K-Frame ESP32 ===");
|
||||
|
||||
let peripherals = Peripherals::take().unwrap();
|
||||
let sysloop = EspSystemEventLoop::take().unwrap();
|
||||
let nvs = EspDefaultNvsPartition::take().unwrap();
|
||||
|
||||
let mut display = hal::display::init(hal::display::DisplayHardware {
|
||||
let display = hal::display::init(hal::display::DisplayHardware {
|
||||
spi: peripherals.spi2,
|
||||
sclk: peripherals.pins.gpio18.into(),
|
||||
mosi: peripherals.pins.gpio23.into(),
|
||||
@@ -23,23 +32,13 @@ fn main() {
|
||||
dc: peripherals.pins.gpio21.into(),
|
||||
rst: peripherals.pins.gpio22.into(),
|
||||
});
|
||||
info!("Display initialized");
|
||||
info!("Display ready");
|
||||
|
||||
display.fill_background(config::SCREEN).unwrap();
|
||||
display.draw_text(
|
||||
"K-Frame",
|
||||
10, 10,
|
||||
BoundingBox::new(10, 10, 220, 40),
|
||||
).unwrap();
|
||||
display.draw_text(
|
||||
"Display test OK",
|
||||
10, 60,
|
||||
BoundingBox::new(10, 60, 220, 40),
|
||||
).unwrap();
|
||||
display.flush().unwrap();
|
||||
info!("Connecting WiFi...");
|
||||
let _wifi = hal::wifi::init(peripherals.modem, sysloop, nvs, WIFI_SSID, WIFI_PASS)
|
||||
.expect("WiFi init failed");
|
||||
|
||||
info!("Display test complete — looping forever");
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
let (tx, rx) = mpsc::channel();
|
||||
tasks::network::spawn(SERVER_ADDR.into(), tx);
|
||||
tasks::render::run(config::SCREEN, display, rx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user