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:
61
Makefile
Normal file
61
Makefile
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
.DEFAULT_GOAL := check
|
||||||
|
|
||||||
|
ESP_PORT ?= /dev/ttyACM0
|
||||||
|
|
||||||
|
# Run the full local check suite — same order as CI would.
|
||||||
|
check: fmt-check clippy test
|
||||||
|
@echo "✅ All checks passed"
|
||||||
|
|
||||||
|
# Apply rustfmt to all files.
|
||||||
|
fmt:
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
# Check formatting without modifying files (CI-safe).
|
||||||
|
fmt-check:
|
||||||
|
cargo fmt --check
|
||||||
|
|
||||||
|
# Run Clippy and treat warnings as errors.
|
||||||
|
clippy:
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
# Run the test suite.
|
||||||
|
test:
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Apply fmt + clippy auto-fixes in one shot.
|
||||||
|
fix:
|
||||||
|
cargo fmt
|
||||||
|
cargo clippy --fix --allow-dirty --allow-staged
|
||||||
|
|
||||||
|
# Start the K-Frame server.
|
||||||
|
server:
|
||||||
|
cargo run --bin bootstrap
|
||||||
|
|
||||||
|
# Start the desktop client.
|
||||||
|
desktop:
|
||||||
|
cargo run --bin client-desktop
|
||||||
|
|
||||||
|
# Build ESP32 firmware. Requires env: KFRAME_WIFI_SSID, KFRAME_WIFI_PASS, KFRAME_SERVER_ADDR
|
||||||
|
esp-build:
|
||||||
|
@test -n "$(KFRAME_WIFI_SSID)" || (echo "Set KFRAME_WIFI_SSID, KFRAME_WIFI_PASS, KFRAME_SERVER_ADDR" && exit 1)
|
||||||
|
cd crates/client-esp32 && cargo build --release
|
||||||
|
|
||||||
|
# Flash ESP32 firmware.
|
||||||
|
esp-flash:
|
||||||
|
@test -n "$(KFRAME_WIFI_SSID)" || (echo "Set KFRAME_WIFI_SSID, KFRAME_WIFI_PASS, KFRAME_SERVER_ADDR" && exit 1)
|
||||||
|
cd crates/client-esp32 && cargo espflash flash --port $(ESP_PORT) --release
|
||||||
|
|
||||||
|
# Flash and monitor ESP32.
|
||||||
|
esp-run:
|
||||||
|
@test -n "$(KFRAME_WIFI_SSID)" || (echo "Set KFRAME_WIFI_SSID, KFRAME_WIFI_PASS, KFRAME_SERVER_ADDR" && exit 1)
|
||||||
|
cd crates/client-esp32 && cargo espflash flash --port $(ESP_PORT) --release --monitor
|
||||||
|
|
||||||
|
# Monitor ESP32 serial output.
|
||||||
|
esp-monitor:
|
||||||
|
cd crates/client-esp32 && cargo espflash monitor --port $(ESP_PORT)
|
||||||
|
|
||||||
|
# Erase ESP32 flash (recovery).
|
||||||
|
esp-erase:
|
||||||
|
esptool --port $(ESP_PORT) erase-flash
|
||||||
|
|
||||||
|
.PHONY: check fmt fmt-check clippy test fix server desktop esp-build esp-flash esp-run esp-monitor esp-erase
|
||||||
@@ -81,12 +81,8 @@ async fn main() {
|
|||||||
all_changed.extend(changed_portfolio);
|
all_changed.extend(changed_portfolio);
|
||||||
|
|
||||||
if !all_changed.is_empty() {
|
if !all_changed.is_empty() {
|
||||||
if counter == 1 {
|
if let Some(l) = &layout {
|
||||||
if let Some(l) = &layout {
|
broadcaster.push_screen_update(l, &all_changed).await.unwrap();
|
||||||
broadcaster.push_screen_update(l, &all_changed).await.unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
broadcaster.push_data_update(&all_changed).await.unwrap();
|
|
||||||
}
|
}
|
||||||
println!("Pushed {} widget updates (tick {counter})", all_changed.len());
|
println!("Pushed {} widget updates (tick {counter})", all_changed.len());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ CONFIG_SPI_MASTER_IN_IRAM=y
|
|||||||
|
|
||||||
# Use single large app partition (no OTA)
|
# Use single large app partition (no OTA)
|
||||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
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,
|
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_BAUDRATE: Hertz = Hertz(26_000_000);
|
||||||
pub const SPI_BUFFER_SIZE: usize = 512;
|
pub const SPI_BUFFER_SIZE: usize = 512;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use esp_idf_hal::delay::{Delay, Ets};
|
use esp_idf_hal::delay::{Delay, Ets};
|
||||||
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin, PinDriver};
|
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin, PinDriver};
|
||||||
use esp_idf_hal::spi::{SpiDeviceDriver, SpiDriver, SpiDriverConfig, SPI2, config::Config as SpiConfig};
|
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::pixelcolor::Rgb565;
|
||||||
use embedded_graphics::prelude::*;
|
use embedded_graphics::prelude::*;
|
||||||
use log::info;
|
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::adapters::display::Esp32DisplayAdapter;
|
||||||
|
use crate::boot;
|
||||||
|
|
||||||
pub struct DisplayHardware<'d> {
|
pub struct DisplayHardware<'d> {
|
||||||
pub spi: SPI2<'d>,
|
pub spi: SPI2<'d>,
|
||||||
@@ -40,19 +41,20 @@ pub fn init(hw: DisplayHardware<'static>) -> Esp32DisplayAdapter {
|
|||||||
rst_pin.set_high().unwrap();
|
rst_pin.set_high().unwrap();
|
||||||
Ets::delay_ms(120);
|
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 buf: &'static mut [u8; SPI_BUFFER_SIZE] = Box::leak(Box::new([0u8; SPI_BUFFER_SIZE]));
|
||||||
let di = SpiInterface::new(spi_device, dc_pin, buf);
|
let di = SpiInterface::new(spi_device, dc_pin, buf);
|
||||||
|
|
||||||
info!("Initializing ILI9341...");
|
info!("Initializing ILI9341...");
|
||||||
let mut raw_display = Builder::new(ILI9341Rgb565, di)
|
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 })
|
.orientation(Orientation { rotation: Rotation::Deg90, mirrored: true })
|
||||||
|
.color_order(ColorOrder::Bgr)
|
||||||
.init(&mut Delay::new_default())
|
.init(&mut Delay::new_default())
|
||||||
.expect("Display init failed");
|
.expect("Display init failed");
|
||||||
|
|
||||||
raw_display.clear(Rgb565::BLACK).unwrap();
|
boot::run(&mut raw_display);
|
||||||
|
|
||||||
Esp32DisplayAdapter::new(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_hal::modem::Modem;
|
||||||
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||||
use esp_idf_svc::wifi::{
|
use esp_idf_svc::wifi::{
|
||||||
AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi,
|
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>(
|
pub fn init<'d>(
|
||||||
modem: Modem<'d>,
|
modem: Modem<'d>,
|
||||||
@@ -22,17 +27,30 @@ pub fn init<'d>(
|
|||||||
let config = Configuration::Client(ClientConfiguration {
|
let config = Configuration::Client(ClientConfiguration {
|
||||||
ssid: ssid.try_into().unwrap(),
|
ssid: ssid.try_into().unwrap(),
|
||||||
password: password.try_into().unwrap(),
|
password: password.try_into().unwrap(),
|
||||||
auth_method: AuthMethod::WPA2Personal,
|
auth_method: AuthMethod::WPA,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
wifi.set_configuration(&config).map_err(|e| format!("wifi config: {e:?}"))?;
|
wifi.set_configuration(&config).map_err(|e| format!("wifi config: {e:?}"))?;
|
||||||
wifi.start().map_err(|e| format!("wifi start: {e:?}"))?;
|
wifi.start().map_err(|e| format!("wifi start: {e:?}"))?;
|
||||||
|
|
||||||
info!("WiFi started, connecting...");
|
info!("WiFi started, connecting to '{ssid}'...");
|
||||||
wifi.connect().map_err(|e| format!("wifi connect: {e:?}"))?;
|
|
||||||
wifi.wait_netif_up().map_err(|e| format!("wifi netif: {e:?}"))?;
|
|
||||||
|
|
||||||
info!("WiFi connected");
|
for attempt in 1..=MAX_RETRIES {
|
||||||
Ok(wifi)
|
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 adapters;
|
||||||
|
mod boot;
|
||||||
mod config;
|
mod config;
|
||||||
mod hal;
|
mod hal;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use client_domain::{BoundingBox, DisplayPort};
|
use std::sync::mpsc;
|
||||||
use esp_idf_hal::peripherals::Peripherals;
|
use esp_idf_hal::peripherals::Peripherals;
|
||||||
|
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||||
|
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||||
use log::info;
|
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() {
|
fn main() {
|
||||||
esp_idf_svc::sys::link_patches();
|
esp_idf_svc::sys::link_patches();
|
||||||
esp_idf_svc::log::EspLogger::initialize_default();
|
esp_idf_svc::log::EspLogger::initialize_default();
|
||||||
@@ -14,8 +21,10 @@ fn main() {
|
|||||||
info!("=== K-Frame ESP32 ===");
|
info!("=== K-Frame ESP32 ===");
|
||||||
|
|
||||||
let peripherals = Peripherals::take().unwrap();
|
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,
|
spi: peripherals.spi2,
|
||||||
sclk: peripherals.pins.gpio18.into(),
|
sclk: peripherals.pins.gpio18.into(),
|
||||||
mosi: peripherals.pins.gpio23.into(),
|
mosi: peripherals.pins.gpio23.into(),
|
||||||
@@ -23,23 +32,13 @@ fn main() {
|
|||||||
dc: peripherals.pins.gpio21.into(),
|
dc: peripherals.pins.gpio21.into(),
|
||||||
rst: peripherals.pins.gpio22.into(),
|
rst: peripherals.pins.gpio22.into(),
|
||||||
});
|
});
|
||||||
info!("Display initialized");
|
info!("Display ready");
|
||||||
|
|
||||||
display.fill_background(config::SCREEN).unwrap();
|
info!("Connecting WiFi...");
|
||||||
display.draw_text(
|
let _wifi = hal::wifi::init(peripherals.modem, sysloop, nvs, WIFI_SSID, WIFI_PASS)
|
||||||
"K-Frame",
|
.expect("WiFi init failed");
|
||||||
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!("Display test complete — looping forever");
|
let (tx, rx) = mpsc::channel();
|
||||||
loop {
|
tasks::network::spawn(SERVER_ADDR.into(), tx);
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
tasks::render::run(config::SCREEN, display, rx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
6410
other/logo_splash.h
Normal file
6410
other/logo_splash.h
Normal file
File diff suppressed because it is too large
Load Diff
43
other/logo_to_rgb565.py
Normal file
43
other/logo_to_rgb565.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
INPUT = "/home/gabriel/Downloads/Logo.png"
|
||||||
|
OUTPUT = "/home/gabriel/Downloads/logo_splash.h"
|
||||||
|
DISPLAY_W, DISPLAY_H = 240, 320
|
||||||
|
BG_COLOR = (30, 30, 30) # dark background, change to (0,0,0) for pure black
|
||||||
|
|
||||||
|
img = Image.open(INPUT).convert("RGB")
|
||||||
|
|
||||||
|
# Scale logo to fit within display, preserving aspect ratio
|
||||||
|
img.thumbnail((DISPLAY_W, DISPLAY_H), Image.LANCZOS)
|
||||||
|
|
||||||
|
# Center on display canvas
|
||||||
|
canvas = Image.new("RGB", (DISPLAY_W, DISPLAY_H), BG_COLOR)
|
||||||
|
x = (DISPLAY_W - img.width) // 2
|
||||||
|
y = (DISPLAY_H - img.height) // 2
|
||||||
|
canvas.paste(img, (x, y))
|
||||||
|
|
||||||
|
# Convert to RGB565
|
||||||
|
pixels = []
|
||||||
|
for r, g, b in canvas.getdata():
|
||||||
|
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||||
|
# Swap bytes for ILI9341 (big-endian over SPI)
|
||||||
|
pixels.append(((rgb565 & 0xFF) << 8) | (rgb565 >> 8))
|
||||||
|
|
||||||
|
# Write header file
|
||||||
|
with open(OUTPUT, "w") as f:
|
||||||
|
f.write("#pragma once\n")
|
||||||
|
f.write("#include <pgmspace.h>\n\n")
|
||||||
|
f.write(f"// {DISPLAY_W}x{DISPLAY_H} RGB565, big-endian\n")
|
||||||
|
f.write(f"const uint16_t SPLASH_WIDTH = {DISPLAY_W};\n")
|
||||||
|
f.write(f"const uint16_t SPLASH_HEIGHT = {DISPLAY_H};\n\n")
|
||||||
|
f.write("const uint16_t splash_logo[] PROGMEM = {\n")
|
||||||
|
for i, p in enumerate(pixels):
|
||||||
|
if i % 12 == 0:
|
||||||
|
f.write(" ")
|
||||||
|
f.write(f"0x{p:04X},")
|
||||||
|
if i % 12 == 11:
|
||||||
|
f.write("\n")
|
||||||
|
f.write("\n};\n")
|
||||||
|
|
||||||
|
print(f"Done — {len(pixels)} pixels, {len(pixels)*2/1024:.1f} KB")
|
||||||
|
print(f"Output: {OUTPUT}")
|
||||||
Reference in New Issue
Block a user