add all crates: domain, protocol, application, client, adapters, ESP32 firmware
Server: domain (entities, value objects, ports), protocol (postcard wire types), application (config service, data projection), adapters (config-memory, tcp-server), bootstrap (composition root with fake data). Client: client-domain (layout engine, render tree, HAL ports), client-application (message handling, repaint commands), adapters (tcp-client, display-terminal), client-desktop (end-to-end working). ESP32: client-esp32 firmware with ILI9341 display over SPI, WiFi networking. Display test verified on hardware — landscape orientation, text rendering works. 60 workspace tests, all passing.
This commit is contained in:
13
crates/client-esp32/.cargo/config.toml
Normal file
13
crates/client-esp32/.cargo/config.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[build]
|
||||
target = "xtensa-esp32-espidf"
|
||||
|
||||
[target.xtensa-esp32-espidf]
|
||||
linker = "ldproxy"
|
||||
runner = "espflash flash --monitor"
|
||||
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[env]
|
||||
MCU = "esp32"
|
||||
ESP_IDF_VERSION = "v5.4"
|
||||
2
crates/client-esp32/.gitignore
vendored
Normal file
2
crates/client-esp32/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
.embuild/
|
||||
1820
crates/client-esp32/Cargo.lock
generated
Normal file
1820
crates/client-esp32/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
crates/client-esp32/Cargo.toml
Normal file
30
crates/client-esp32/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "client-esp32"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
domain = { path = "../domain" }
|
||||
protocol = { path = "../protocol" }
|
||||
client-domain = { path = "../client-domain" }
|
||||
client-application = { path = "../client-application" }
|
||||
|
||||
esp-idf-hal = "0.46"
|
||||
esp-idf-svc = { version = "0.52", features = ["experimental"] }
|
||||
esp-idf-sys = "0.37"
|
||||
|
||||
mipidsi = "0.10"
|
||||
embedded-graphics = "0.8"
|
||||
embedded-text = "0.7"
|
||||
embedded-hal-bus = "0.3"
|
||||
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
"alloc",
|
||||
] }
|
||||
postcard = { version = "1.1", default-features = false, features = ["alloc"] }
|
||||
|
||||
log = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
embuild = "0.33"
|
||||
3
crates/client-esp32/build.rs
Normal file
3
crates/client-esp32/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
embuild::espidf::sysenv::output();
|
||||
}
|
||||
2
crates/client-esp32/rust-toolchain.toml
Normal file
2
crates/client-esp32/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "esp"
|
||||
16
crates/client-esp32/sdkconfig.defaults
Normal file
16
crates/client-esp32/sdkconfig.defaults
Normal file
@@ -0,0 +1,16 @@
|
||||
# K-Frame ESP32 firmware config
|
||||
|
||||
# WiFi
|
||||
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
|
||||
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
|
||||
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
|
||||
|
||||
# Task stack sizes
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=8192
|
||||
|
||||
# SPI
|
||||
CONFIG_SPI_MASTER_IN_IRAM=y
|
||||
|
||||
# Use single large app partition (no OTA)
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
124
crates/client-esp32/src/adapters/display.rs
Normal file
124
crates/client-esp32/src/adapters/display.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use client_domain::{BoundingBox, DisplayPort};
|
||||
use embedded_graphics::{
|
||||
mono_font::{ascii::FONT_6X10, ascii::FONT_10X20, MonoTextStyle},
|
||||
pixelcolor::Rgb565,
|
||||
prelude::*,
|
||||
primitives::{PrimitiveStyle, Rectangle},
|
||||
text::Text,
|
||||
};
|
||||
use embedded_text::{TextBox, style::TextBoxStyleBuilder};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DisplayError {
|
||||
Draw(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DisplayError::Draw(e) => write!(f, "draw: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Esp32DisplayAdapter {
|
||||
inner: Box<dyn ErasedDisplay>,
|
||||
}
|
||||
|
||||
trait ErasedDisplay {
|
||||
fn clear_region(&mut self, bounds: BoundingBox) -> Result<(), DisplayError>;
|
||||
fn draw_text(&mut self, text: &str, x: u16, y: u16, bounds: BoundingBox) -> Result<(), DisplayError>;
|
||||
fn draw_icon(&mut self, icon: &str, x: u16, y: u16) -> Result<(), DisplayError>;
|
||||
fn fill_background(&mut self, bounds: BoundingBox) -> Result<(), DisplayError>;
|
||||
fn flush(&mut self) -> Result<(), DisplayError>;
|
||||
}
|
||||
|
||||
impl<D> ErasedDisplay for D
|
||||
where
|
||||
D: DrawTarget<Color = Rgb565>,
|
||||
D::Error: std::fmt::Debug,
|
||||
{
|
||||
fn clear_region(&mut self, bounds: BoundingBox) -> Result<(), DisplayError> {
|
||||
Rectangle::new(
|
||||
Point::new(bounds.x as i32, bounds.y as i32),
|
||||
Size::new(bounds.width as u32, bounds.height as u32),
|
||||
)
|
||||
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
|
||||
.draw(self)
|
||||
.map_err(|e| DisplayError::Draw(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, text: &str, _x: u16, _y: u16, bounds: BoundingBox) -> Result<(), DisplayError> {
|
||||
let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE);
|
||||
let textbox_style = TextBoxStyleBuilder::new().build();
|
||||
let rect = Rectangle::new(
|
||||
Point::new(bounds.x as i32, bounds.y as i32),
|
||||
Size::new(bounds.width as u32, bounds.height as u32),
|
||||
);
|
||||
TextBox::with_textbox_style(text, rect, style, textbox_style)
|
||||
.draw(self)
|
||||
.map_err(|e| DisplayError::Draw(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_icon(&mut self, icon: &str, x: u16, y: u16) -> Result<(), DisplayError> {
|
||||
let style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
|
||||
let icon_char = match icon {
|
||||
"sunny" | "clear" => "*",
|
||||
"cloud_rain" | "rain" => "~",
|
||||
"cloud" | "cloudy" => "=",
|
||||
"dollar" | "money" => "$",
|
||||
"music" | "note" => "#",
|
||||
_ => "?",
|
||||
};
|
||||
Text::new(icon_char, Point::new(x as i32, y as i32 + 20), style)
|
||||
.draw(self)
|
||||
.map_err(|e| DisplayError::Draw(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fill_background(&mut self, bounds: BoundingBox) -> Result<(), DisplayError> {
|
||||
self.clear_region(bounds)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), DisplayError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Esp32DisplayAdapter {
|
||||
pub fn new<D>(display: D) -> Self
|
||||
where
|
||||
D: DrawTarget<Color = Rgb565> + 'static,
|
||||
D::Error: std::fmt::Debug,
|
||||
{
|
||||
Self {
|
||||
inner: Box::new(display),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayPort for Esp32DisplayAdapter {
|
||||
type Error = DisplayError;
|
||||
|
||||
fn clear_region(&mut self, bounds: BoundingBox) -> Result<(), Self::Error> {
|
||||
self.inner.clear_region(bounds)
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, text: &str, x: u16, y: u16, bounds: BoundingBox) -> Result<(), Self::Error> {
|
||||
self.inner.draw_text(text, x, y, bounds)
|
||||
}
|
||||
|
||||
fn draw_icon(&mut self, icon: &str, x: u16, y: u16) -> Result<(), Self::Error> {
|
||||
self.inner.draw_icon(icon, x, y)
|
||||
}
|
||||
|
||||
fn fill_background(&mut self, bounds: BoundingBox) -> Result<(), Self::Error> {
|
||||
self.inner.fill_background(bounds)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
2
crates/client-esp32/src/adapters/mod.rs
Normal file
2
crates/client-esp32/src/adapters/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod display;
|
||||
pub mod network;
|
||||
85
crates/client-esp32/src/adapters/network.rs
Normal file
85
crates/client-esp32/src/adapters/network.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use client_domain::NetworkPort;
|
||||
use protocol::MAX_FRAME_SIZE;
|
||||
use crate::config::NET_READ_TIMEOUT;
|
||||
use log::info;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkError {
|
||||
Io(std::io::Error),
|
||||
NotConnected,
|
||||
FrameTooLarge(usize),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NetworkError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NetworkError::Io(e) => write!(f, "io: {e}"),
|
||||
NetworkError::NotConnected => write!(f, "not connected"),
|
||||
NetworkError::FrameTooLarge(n) => write!(f, "frame too large: {n}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Esp32Network {
|
||||
stream: Option<TcpStream>,
|
||||
}
|
||||
|
||||
impl Esp32Network {
|
||||
pub fn new() -> Self {
|
||||
Self { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkPort for Esp32Network {
|
||||
type Error = NetworkError;
|
||||
|
||||
fn connect(&mut self, addr: &str) -> Result<(), Self::Error> {
|
||||
info!("TCP connecting to {addr}...");
|
||||
let stream = TcpStream::connect(addr).map_err(NetworkError::Io)?;
|
||||
stream.set_nonblocking(true).map_err(NetworkError::Io)?;
|
||||
stream.set_read_timeout(Some(NET_READ_TIMEOUT)).map_err(NetworkError::Io)?;
|
||||
self.stream = Some(stream);
|
||||
info!("TCP connected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> Result<(), Self::Error> {
|
||||
self.stream = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let stream = self.stream.as_mut().ok_or(NetworkError::NotConnected)?;
|
||||
stream.write_all(data).map_err(NetworkError::Io)
|
||||
}
|
||||
|
||||
fn receive(&mut self) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let stream = self.stream.as_mut().ok_or(NetworkError::NotConnected)?;
|
||||
|
||||
let mut len_buf = [0u8; 4];
|
||||
match stream.read_exact(&mut len_buf) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(None),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => return Ok(None),
|
||||
Err(e) => return Err(NetworkError::Io(e)),
|
||||
}
|
||||
|
||||
let len = u32::from_be_bytes(len_buf) as usize;
|
||||
if len > MAX_FRAME_SIZE {
|
||||
return Err(NetworkError::FrameTooLarge(len));
|
||||
}
|
||||
|
||||
let mut payload = vec![0u8; len];
|
||||
stream.set_nonblocking(false).map_err(NetworkError::Io)?;
|
||||
stream.read_exact(&mut payload).map_err(NetworkError::Io)?;
|
||||
stream.set_nonblocking(true).map_err(NetworkError::Io)?;
|
||||
|
||||
Ok(Some(payload))
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.stream.is_some()
|
||||
}
|
||||
}
|
||||
22
crates/client-esp32/src/config.rs
Normal file
22
crates/client-esp32/src/config.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::time::Duration;
|
||||
use esp_idf_hal::units::Hertz;
|
||||
use client_domain::BoundingBox;
|
||||
|
||||
pub const SCREEN_WIDTH: u16 = 320;
|
||||
pub const SCREEN_HEIGHT: u16 = 240;
|
||||
pub const SCREEN: BoundingBox = BoundingBox {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
};
|
||||
|
||||
pub const SPI_BAUDRATE: Hertz = Hertz(26_000_000);
|
||||
pub const SPI_BUFFER_SIZE: usize = 512;
|
||||
|
||||
pub const NET_THREAD_STACK_SIZE: usize = 8192;
|
||||
pub const NET_READ_TIMEOUT: Duration = Duration::from_millis(10);
|
||||
pub const NET_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
||||
pub const NET_RECONNECT_DELAY: Duration = Duration::from_secs(2);
|
||||
|
||||
pub const RENDER_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||
58
crates/client-esp32/src/hal/display.rs
Normal file
58
crates/client-esp32/src/hal/display.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
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 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::adapters::display::Esp32DisplayAdapter;
|
||||
|
||||
pub struct DisplayHardware<'d> {
|
||||
pub spi: SPI2<'d>,
|
||||
pub sclk: AnyOutputPin<'d>,
|
||||
pub mosi: AnyOutputPin<'d>,
|
||||
pub cs: AnyOutputPin<'d>,
|
||||
pub dc: AnyOutputPin<'d>,
|
||||
pub rst: AnyOutputPin<'d>,
|
||||
}
|
||||
|
||||
pub fn init(hw: DisplayHardware<'static>) -> Esp32DisplayAdapter {
|
||||
let spi_driver = SpiDriver::new(
|
||||
hw.spi, hw.sclk, hw.mosi,
|
||||
None::<AnyIOPin>,
|
||||
&SpiDriverConfig::new(),
|
||||
).expect("SPI driver init failed");
|
||||
|
||||
let spi_config = SpiConfig::new().baudrate(SPI_BAUDRATE);
|
||||
let spi_device = SpiDeviceDriver::new(spi_driver, Some(hw.cs), &spi_config)
|
||||
.expect("SPI device init failed");
|
||||
|
||||
let dc_pin = PinDriver::output(hw.dc).expect("DC pin failed");
|
||||
let mut rst_pin = PinDriver::output(hw.rst).expect("RST pin failed");
|
||||
|
||||
info!("Hardware reset...");
|
||||
rst_pin.set_high().unwrap();
|
||||
Ets::delay_ms(10);
|
||||
rst_pin.set_low().unwrap();
|
||||
Ets::delay_ms(10);
|
||||
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 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)
|
||||
.orientation(Orientation { rotation: Rotation::Deg90, mirrored: true })
|
||||
.init(&mut Delay::new_default())
|
||||
.expect("Display init failed");
|
||||
|
||||
raw_display.clear(Rgb565::BLACK).unwrap();
|
||||
Esp32DisplayAdapter::new(raw_display)
|
||||
}
|
||||
2
crates/client-esp32/src/hal/mod.rs
Normal file
2
crates/client-esp32/src/hal/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod display;
|
||||
pub mod wifi;
|
||||
38
crates/client-esp32/src/hal/wifi.rs
Normal file
38
crates/client-esp32/src/hal/wifi.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
|
||||
pub fn init<'d>(
|
||||
modem: Modem<'d>,
|
||||
sysloop: EspSystemEventLoop,
|
||||
nvs: EspDefaultNvsPartition,
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
) -> Result<BlockingWifi<EspWifi<'d>>, String> {
|
||||
let esp_wifi = EspWifi::new(modem, sysloop.clone(), Some(nvs))
|
||||
.map_err(|e| format!("wifi new: {e:?}"))?;
|
||||
|
||||
let mut wifi = BlockingWifi::wrap(esp_wifi, sysloop)
|
||||
.map_err(|e| format!("wifi wrap: {e:?}"))?;
|
||||
|
||||
let config = Configuration::Client(ClientConfiguration {
|
||||
ssid: ssid.try_into().unwrap(),
|
||||
password: password.try_into().unwrap(),
|
||||
auth_method: AuthMethod::WPA2Personal,
|
||||
..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 connected");
|
||||
Ok(wifi)
|
||||
}
|
||||
45
crates/client-esp32/src/main.rs
Normal file
45
crates/client-esp32/src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
mod adapters;
|
||||
mod config;
|
||||
mod hal;
|
||||
mod tasks;
|
||||
|
||||
use client_domain::{BoundingBox, DisplayPort};
|
||||
use esp_idf_hal::peripherals::Peripherals;
|
||||
use log::info;
|
||||
|
||||
fn main() {
|
||||
esp_idf_svc::sys::link_patches();
|
||||
esp_idf_svc::log::EspLogger::initialize_default();
|
||||
|
||||
info!("=== K-Frame ESP32 ===");
|
||||
|
||||
let peripherals = Peripherals::take().unwrap();
|
||||
|
||||
let mut display = hal::display::init(hal::display::DisplayHardware {
|
||||
spi: peripherals.spi2,
|
||||
sclk: peripherals.pins.gpio18.into(),
|
||||
mosi: peripherals.pins.gpio23.into(),
|
||||
cs: peripherals.pins.gpio26.into(),
|
||||
dc: peripherals.pins.gpio21.into(),
|
||||
rst: peripherals.pins.gpio22.into(),
|
||||
});
|
||||
info!("Display initialized");
|
||||
|
||||
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!("Display test complete — looping forever");
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
2
crates/client-esp32/src/tasks/mod.rs
Normal file
2
crates/client-esp32/src/tasks/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod network;
|
||||
pub mod render;
|
||||
50
crates/client-esp32/src/tasks/network.rs
Normal file
50
crates/client-esp32/src/tasks/network.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use client_domain::NetworkPort;
|
||||
use protocol::{ServerMessage, decode_server_message};
|
||||
use crate::config::{NET_THREAD_STACK_SIZE, NET_POLL_INTERVAL, NET_RECONNECT_DELAY};
|
||||
use crate::adapters::network::Esp32Network;
|
||||
use log::*;
|
||||
|
||||
pub fn spawn(server_addr: String, tx: mpsc::Sender<ServerMessage>) {
|
||||
thread::Builder::new()
|
||||
.stack_size(NET_THREAD_STACK_SIZE)
|
||||
.name("net".into())
|
||||
.spawn(move || run(server_addr, tx))
|
||||
.expect("failed to spawn network thread");
|
||||
}
|
||||
|
||||
fn run(server_addr: String, tx: mpsc::Sender<ServerMessage>) {
|
||||
let mut net = Esp32Network::new();
|
||||
|
||||
loop {
|
||||
if !net.is_connected() {
|
||||
info!("Connecting to server {server_addr}...");
|
||||
match net.connect(&server_addr) {
|
||||
Ok(()) => info!("Server connected"),
|
||||
Err(e) => {
|
||||
error!("Connection failed: {e}, retrying...");
|
||||
thread::sleep(NET_RECONNECT_DELAY);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match net.receive() {
|
||||
Ok(Some(payload)) => {
|
||||
match decode_server_message(&payload) {
|
||||
Ok(msg) => { let _ = tx.send(msg); }
|
||||
Err(e) => error!("Decode error: {e}"),
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
thread::sleep(NET_POLL_INTERVAL);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Receive error: {e}, reconnecting...");
|
||||
let _ = net.disconnect();
|
||||
thread::sleep(NET_RECONNECT_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
crates/client-esp32/src/tasks/render.rs
Normal file
46
crates/client-esp32/src/tasks/render.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::sync::mpsc;
|
||||
use client_domain::{BoundingBox, DisplayPort};
|
||||
use client_application::ClientApp;
|
||||
use protocol::ServerMessage;
|
||||
use crate::config::RENDER_POLL_INTERVAL;
|
||||
use crate::adapters::display::Esp32DisplayAdapter;
|
||||
use log::*;
|
||||
|
||||
pub fn run(
|
||||
screen: BoundingBox,
|
||||
mut display: Esp32DisplayAdapter,
|
||||
rx: mpsc::Receiver<ServerMessage>,
|
||||
) {
|
||||
let mut app = ClientApp::new(screen);
|
||||
info!("Render loop started");
|
||||
|
||||
loop {
|
||||
match rx.recv_timeout(RENDER_POLL_INTERVAL) {
|
||||
Ok(msg) => {
|
||||
let repaints = app.handle_message(msg);
|
||||
for cmd in &repaints {
|
||||
display.clear_region(cmd.bounds).unwrap();
|
||||
|
||||
for kv in &cmd.state.data {
|
||||
if let protocol::WireValue::String(s) = &kv.value {
|
||||
display.draw_text(
|
||||
&format!("{}: {s}", kv.key),
|
||||
cmd.bounds.x,
|
||||
cmd.bounds.y,
|
||||
cmd.bounds,
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if !repaints.is_empty() {
|
||||
display.flush().unwrap();
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
error!("Network thread died");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user