From 3782686d4bbb41bfd8f91be6b198b4021aff4e40 Mon Sep 17 00:00:00 2001 From: Arkitu Date: Fri, 1 Aug 2025 02:35:25 +0200 Subject: [PATCH] a lot of things + ws can rcv multiple msgs at once --- Cargo.toml | 4 +- src/apps/index.rs | 31 ++++++-- src/apps/mod.rs | 31 +++++++- src/apps/ttt.html | 30 +++++-- src/apps/ttt.js | 52 ++++++++++++ src/apps/ttt.rs | 197 ++++++++++++++-------------------------------- src/lib.rs | 9 +-- src/main.rs | 1 + src/socket.rs | 58 +++++++------- src/socket/ws.rs | 82 ++++++++++++++----- 10 files changed, 283 insertions(+), 212 deletions(-) create mode 100644 src/apps/ttt.js diff --git a/Cargo.toml b/Cargo.toml index f90062a..858f77a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,8 @@ wifi-connect = [ dhcp = ["dep:dhcparse"] dns = ["dep:dnsparse"] chat = ["dep:ringbuffer"] -ttt = [] -default = ["dhcp", "dns"] +ttt = ["dep:serde-json-core", "dep:serde"] +default = ["dhcp", "dns", "ttt"] [dependencies] embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [ diff --git a/src/apps/index.rs b/src/apps/index.rs index 8c4c254..80c6af1 100644 --- a/src/apps/index.rs +++ b/src/apps/index.rs @@ -1,4 +1,7 @@ -use crate::socket::{HttpRequestType, HttpResCode}; +use crate::{ + apps::Content, + socket::{HttpRequestType, HttpResCode}, +}; use super::App; @@ -7,17 +10,29 @@ impl App for IndexApp { fn socket_name(&self) -> &'static str { "index" } - async fn handle_request<'a>( - &'a mut self, + async fn handle_request( + &mut self, path: &str, _req_type: HttpRequestType, _content: &str, - ) -> (HttpResCode, &'static str, &'a str) { + ) -> (HttpResCode, &'static str, Option>) { match path { - "/" | "/index" | "/index.html" => { - (HttpResCode::Ok, "html", include_str!("./index.html")) - } - _ => (HttpResCode::NotFound, "", ""), + "/" | "/index" | "/index.html" => ( + HttpResCode::Ok, + "html", + Some(include_str!("./index.html").into()), + ), + "/ttt" => ( + HttpResCode::Ok, + "html", + Some(include_str!("ttt.html").into()), + ), + "/ttt.js" => ( + HttpResCode::Ok, + "javascript", + Some(include_str!("ttt.js").into()), + ), + _ => (HttpResCode::NotFound, "", None), } } } diff --git a/src/apps/mod.rs b/src/apps/mod.rs index 80180e1..fc6413b 100644 --- a/src/apps/mod.rs +++ b/src/apps/mod.rs @@ -1,3 +1,5 @@ +use heapless::Vec; + use crate::socket::{HttpRequestType, HttpResCode, ws::Ws}; #[cfg(feature = "chat")] @@ -10,11 +12,11 @@ pub trait App { fn socket_name(&self) -> &'static str; async fn handle_request<'a>( &'a mut self, - _path: &str, + _path: &'a str, _req_type: HttpRequestType, - _content: &str, - ) -> (HttpResCode, &'static str, &'a str) { - (HttpResCode::NotFound, "", "") + _content: &'a str, + ) -> (HttpResCode, &'static str, Option>) { + (HttpResCode::NotFound, "", None) } fn accept_ws(&self, _path: &str) -> bool { false @@ -26,3 +28,24 @@ pub trait App { ) { } } + +pub struct Content<'a>(pub Vec<&'a str, 8>); + +// pub enum Content<'a> { +// Str(&'a str), +// /// Return the number of bytes written +// /// (fn that writes content, length) +// Fn(fn(&mut [u8]) -> usize, usize), +// } +impl<'a> From<&'a str> for Content<'a> { + fn from(value: &'a str) -> Self { + let mut v = Vec::new(); + v.push(value).unwrap(); + Content(v) + } +} +impl Content<'_> { + pub fn len(&self) -> usize { + self.0.iter().fold(0, |acc, s| acc + s.len()) + } +} diff --git a/src/apps/ttt.html b/src/apps/ttt.html index f5e788e..4a2d0c5 100644 --- a/src/apps/ttt.html +++ b/src/apps/ttt.html @@ -1,6 +1,6 @@ - + +

TicTacToe

-
+

+ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/apps/ttt.js b/src/apps/ttt.js new file mode 100644 index 0000000..97ae49a --- /dev/null +++ b/src/apps/ttt.js @@ -0,0 +1,52 @@ +const team = 0; +const teamName = "blue"; +const color = "dodgerblue"; +const otherColor = "firebrick"; + +document.getElementById("team").innerHTML = + 'Team : ' + teamName + ""; + +const ws = new WebSocket("ws://192.254.0.2:8080/" + teamName); + +ws.onmessage = (event) => { + console.log(event.data); + if (typeof event.data == "string") { + let msg = JSON.parse(event.data); + let cells = []; + for (let i = 0; i < 9; i++) { + let owner = null; + if (((msg.board >> (17 - i)) & 1) == 1) { + owner = 0; + } else if (((msg.board >> (8 - i)) & 1) == 1) { + owner = 1; + } + + let tagName; + if (msg.turn == team && owner === null) { + tagName = "button"; + } else { + tagName = "div"; + } + let cell = document.createElement(tagName); + + cell.classList.add("cell"); + + if (tagName === "button") { + cell.addEventListener("click", (event) => { + console.log(i); + ws.send(new Uint8Array([i])); + }); + } + + cell.setAttribute("team", owner); + + // if (msg.board & (1 << i != 0) || msg.board & (1 << (i + 9))) { + // if (msg.board & (1 << (i + 9) != 0)) { + // col = "firebrick"; + // } + // } + cells.push(cell); + } + document.getElementById("grid").replaceChildren(...cells); + } +}; diff --git a/src/apps/ttt.rs b/src/apps/ttt.rs index bc3a7b6..ea6e7cf 100644 --- a/src/apps/ttt.rs +++ b/src/apps/ttt.rs @@ -1,31 +1,32 @@ use core::fmt::Write; use core::{ops::Not, sync::atomic::Ordering}; -use embassy_time::{Duration, Instant}; -use heapless::String; +use embassy_time::{Duration, Instant, Timer}; +use heapless::{String, Vec}; +use log::info; use pico_website::unwrap; use portable_atomic::{AtomicBool, AtomicU32}; +use serde::Serialize; +use crate::apps::Content; +use crate::socket::ws::{Ws, WsMsg}; use crate::socket::{HttpRequestType, HttpResCode}; use super::App; static TURN: AtomicBool = AtomicBool::new(false); -// bits [0; 8] : player zero board / bits [9; 17] : player one board -static BOARD: AtomicU32 = AtomicU32::new(0); +// bits [0; 8] : player zero board / bits [9; 17] : player one board / is_ended [18] / is_draw [19] / winner [20]: 0=blue 1=green / current_turn [21]: 0=blue 1=green +static BOARD: AtomicU32 = AtomicU32::new(0b01000000_001000000); pub struct TttApp { - res_buf: String<2048>, - /// State of the board last time it has been sent - last_board: u32, team: Team, + last_board: u32, end: Option<(Instant, Option)>, } impl TttApp { pub fn new(team: Team) -> Self { Self { - res_buf: String::new(), - last_board: 0, team, + last_board: 0, end: None, } } @@ -67,83 +68,6 @@ impl TttApp { } } } - /// Generate board html - async fn generate_board_res<'a>( - &'a mut self, - board: u32, - turn: Team, - outer_html: bool, - ) -> &'a str { - self.res_buf.clear(); - if outer_html { - unwrap(self.res_buf.push_str( - "
", - )) - .await; - } - unwrap(write!( - self.res_buf, - "

Team : {}

", - self.team.color(), - self.team.name() - )) - .await; - match self.end { - Some((_, Some(t))) => { - unwrap(write!( - self.res_buf, - "

Team {} has won!


", - t.color(), - t.name() - )) - .await - } - Some((_, None)) => unwrap(write!(self.res_buf, "

Draw!


",)).await, - None => {} - } - unwrap(self.res_buf.push_str("
")).await; - for c in 0..=8 { - let picked_by = if board & (1 << c) != 0 { - Some(Team::Zero) - } else if board & (1 << (9 + c)) != 0 { - Some(Team::One) - } else { - None - }; - match picked_by { - Some(t) => { - unwrap(write!( - self.res_buf, - "
", - t.color() - )) - .await; - } - None => { - if self.team == turn.into() && self.end.is_none() { - unwrap(write!( - self.res_buf, - "", - c - )).await; - } else { - unwrap(self.res_buf.push_str("
")).await; - } - } - }; - } - unwrap(self.res_buf.push_str("
")).await; - if outer_html { - unwrap(self.res_buf.push_str("
")).await; - } - &self.res_buf - } } impl App for TttApp { @@ -155,59 +79,52 @@ impl App for TttApp { path: &str, _req_type: HttpRequestType, _content: &str, - ) -> (HttpResCode, &'static str, &'a str) { + ) -> (HttpResCode, &'static str, Option>) { match path { - "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => { - (HttpResCode::Ok, "html", include_str!("./ttt.html")) - } - "/ttt/initial_game" => { - let board = BOARD.load(Ordering::Acquire); - let turn = TURN.load(Ordering::Acquire); - ( - HttpResCode::Ok, - "html", - self.generate_board_res(board, turn.into(), true).await, - ) - } - path => { - if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/game" { - let mut board = BOARD.load(Ordering::Acquire); - let mut turn = TURN.load(Ordering::Acquire); - - // just return correct board in case of unauthorized move - if path.starts_with("/ttt/cell") && self.team == turn.into() { - let clicked_c: Cell = match TryInto::::try_into( - unwrap(path.chars().nth(9).ok_or("no 9th char")).await, - ) { - Ok(c) => c, - Err(_) => return (HttpResCode::NotFound, "", ""), - }; - if board & ((1 << (clicked_c as u32)) + (1 << (9 + clicked_c as u32))) != 0 - { - return (HttpResCode::Forbidden, "", ""); - } - board = board | (1 << ((self.team as u32 * 9) + clicked_c as u32)); - turn = (!self.team).into(); - BOARD.store(board, Ordering::Release); - TURN.store(turn, Ordering::Release); - } - self.update_end_state(&mut board); - if self.last_board != board { - self.last_board = board; - ( - HttpResCode::Ok, - "html", - self.generate_board_res(board, turn.into(), false).await, - ) - } else { - (HttpResCode::NoContent, "", "") - } - } else { - (HttpResCode::NotFound, "", "") - } - } + "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => ( + HttpResCode::Ok, + "html", + Some(include_str!("ttt.html").into()), + ), + _ => (HttpResCode::NotFound, "", None), } } + fn accept_ws(&self, path: &str) -> bool { + matches!(path, "/blue" | "/red") + } + async fn handle_ws<'a, const BUF_SIZE: usize, const RES_HEAD_BUF_SIZE: usize>( + &'a mut self, + _path: &str, + mut ws: Ws<'a, BUF_SIZE, RES_HEAD_BUF_SIZE>, + ) { + let r: Result<(), ()> = try { + loop { + let board = BOARD.load(Ordering::Acquire); + ws.send(WsMsg::Text( + &serde_json_core::to_string::<_, 40>(&ServerMsg { + board, + turn: Some(Team::Zero), + winner: None, + }) + .unwrap(), + )) + .await?; + while let Some(r) = ws.rcv().await? { + info!("{:?}", r); + } + + Timer::after_secs(1).await; + } + }; + info!("{:?}", r); + } +} + +#[derive(Debug, Serialize)] +struct ServerMsg { + board: u32, + turn: Option, + winner: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -237,6 +154,14 @@ impl Not for Team { } } } +impl Serialize for Team { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(*self as u8) + } +} impl Team { fn color(self) -> &'static str { match self { diff --git a/src/lib.rs b/src/lib.rs index 2e2ba52..5bb0c12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,12 +7,9 @@ use log::info; pub async fn unwrap(res: Result) -> T { match res { Ok(v) => v, - Err(e) => { + Err(e) => loop { info!("FATAL ERROR : {:?}", e); - loop { - info!("FATAL ERROR : {:?}", e); - Timer::after_secs(5).await; - } - } + Timer::after_secs(5).await; + }, } } diff --git a/src/main.rs b/src/main.rs index 5e609dc..de9284f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ #![feature(impl_trait_in_assoc_type)] #![feature(slice_split_once)] #![feature(try_blocks)] +#![feature(impl_trait_in_bindings)] #[cfg(feature = "wifi-connect")] use core::net::Ipv4Addr; diff --git a/src/socket.rs b/src/socket.rs index f223d54..ec0872a 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -8,6 +8,7 @@ use heapless::{String, Vec}; use log::{info, warn}; use sha1::{Digest, Sha1}; +use crate::apps::Content; use crate::{apps, socket::ws::Ws}; pub mod ws; @@ -55,6 +56,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps: socket.remote_endpoint() ); + let mut ws_path: Option> = None; loop { Timer::after_secs(0).await; let n = match socket.read(&mut buf).await { @@ -145,7 +147,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps: Timer::after_secs(0).await; head_buf.clear(); - let res_content: Result<&str, core::fmt::Error> = try { + let res_content: Result, core::fmt::Error> = try { if ws_handshake { if !app.accept_ws(path) { write!( @@ -153,7 +155,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps: "{}\r\n\r\n", Into::<&str>::into(HttpResCode::NotFound) )?; - "" + None } else { if path.len() > 16 { warn!("Ws socket cannot have path longer than 16 chars!"); @@ -180,31 +182,31 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps: Into::<&str>::into(HttpResCode::SwitchingProtocols), accept )?; - "" + None } } else { - let (code, res_type, res_content): (HttpResCode, &str, &str) = match path { + let (code, res_type, res_content) = match path { "/htmx.js" => ( HttpResCode::Ok, "javascript", #[cfg(debug_assertions)] - include_str!("../static/htmx.js"), + Some(include_str!("../static/htmx.js").into()), #[cfg(not(debug_assertions))] - include_bytes!("../static/htmx.min.js"), + Some(include_bytes!("../static/htmx.min.js").into()), ), _ => app.handle_request(path, request_type, content).await, }; write!(&mut head_buf, "{}", Into::<&str>::into(code))?; - if res_type.len() > 0 { + if let Some(ref c) = res_content { write!( &mut head_buf, "\r\n\ Content-Type: text/{}\r\n\ Content-Length: {}\r\n", res_type, - res_content.len() - )?; + c.len() + )? } write!(&mut head_buf, "\r\n\r\n")?; res_content @@ -221,31 +223,33 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps: info!("\n{}\n", from_utf8(&head_buf).unwrap()); - match socket.write_all(&head_buf).await { - Ok(()) => {} - Err(e) => { - warn!("write error: {:?}", e); - break; - } - }; - match socket.write_all(res_content.as_bytes()).await { - Ok(()) => {} - Err(e) => { - warn!("write error: {:?}", e); - break; + let w: Result<(), embassy_net::tcp::Error> = try { + socket.write_all(&head_buf).await?; + if let Some(ref c) = res_content { + for s in c.0.iter() { + socket.write_all(s.as_bytes()).await?; + } + } else { } }; + if let Err(e) = w { + warn!("write error: {:?}", e); + break; + }; + if ws_handshake { - let path: String<16> = String::from_str(path).unwrap(); - app.handle_ws( - &path, - Ws::new(&mut socket, &mut buf, &mut head_buf, app.socket_name()), - ) - .await; + ws_path = Some(String::from_str(path).unwrap()); break; } } + if let Some(path) = ws_path { + app.handle_ws( + &path, + Ws::new(&mut socket, &mut buf, &mut head_buf, app.socket_name()), + ) + .await; + } } } diff --git a/src/socket/ws.rs b/src/socket/ws.rs index c8119b2..a5b68cb 100644 --- a/src/socket/ws.rs +++ b/src/socket/ws.rs @@ -1,12 +1,12 @@ use core::str::from_utf8; use embassy_net::tcp::{TcpReader, TcpSocket, TcpWriter}; -use embassy_time::Instant; +use embassy_time::{Instant, Timer}; use embedded_io_async::{ErrorType, ReadReady, Write}; use heapless::Vec; use log::{info, warn}; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum WsMsg<'a> { Ping(&'a [u8]), Pong(&'a [u8]), @@ -39,6 +39,7 @@ struct WsRx<'a, const BUF_SIZE: usize> { socket: TcpReader<'a>, buf: &'a mut [u8; BUF_SIZE], last_msg: Instant, + msg_in_buf: Option<(usize, usize)>, // (start, length) } struct WsTx<'a, const HEAD_BUF_SIZE: usize> { socket: TcpWriter<'a>, @@ -86,6 +87,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA socket: rx, buf, last_msg: Instant::MIN, + msg_in_buf: None, }, tx: WsTx { socket: tx, @@ -95,27 +97,52 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA } } // Do this often to respond to pings - async fn rcv(&mut self) -> Result, ()> { - if !self.rx.socket.read_ready().unwrap() { - return Ok(None); - } - let n = match self.rx.socket.read(self.rx.buf).await { - Ok(0) => { - warn!("read EOF"); - return Err(()); + pub async fn rcv(&mut self) -> Result, ()> { + let n = match self.rx.msg_in_buf.take() { + Some(n) => { + self.rx.buf.copy_within(n.0..n.0 + n.1, 0); + if self.rx.socket.read_ready().unwrap() { + let n_rcv = match self.rx.socket.read(&mut self.rx.buf[n.1..]).await { + Ok(0) => { + info!("read EOF"); + return Err(()); + } + Ok(n) => n, + Err(e) => { + info!("Socket {}: read error: {:?}", self.name, e); + return Err(()); + } + }; + n.1 + n_rcv + } else { + n.1 + } } - Ok(n) => n, - Err(e) => { - warn!("Socket {}: read error: {:?}", self.name, e); - return Err(()); + None => { + if self.rx.socket.read_ready().unwrap() { + match self.rx.socket.read(self.rx.buf).await { + Ok(0) => { + info!("read EOF"); + return Err(()); + } + Ok(n) => n, + Err(e) => { + info!("Socket {}: read error: {:?}", self.name, e); + return Err(()); + } + } + } else { + return Ok(None); + } } }; + if self.rx.buf[0] & 0b1000_0000 == 0 { - warn!("Fragmented ws messages are not supported!"); + info!("Fragmented ws messages are not supported!"); return Err(()); } if self.rx.buf[0] & 0b0111_0000 != 0 { - warn!( + info!( "Reserved ws bits are set : {}", (self.rx.buf[0] >> 4) & 0b0111 ); @@ -133,14 +160,14 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA l => (l as u64, 2), }; if length > 512 { - warn!("ws payload bigger than 512!"); + info!("ws payload bigger than 512!"); return Err(()); } let content = if self.rx.buf[1] & 0b1000_0000 != 0 { // masked message if n_after_length + 4 + length as usize > n { - warn!("ws payload smaller than length"); + info!("ws payload smaller than length"); return Err(()); } let mask_key: [u8; 4] = self.rx.buf[n_after_length..n_after_length + 4] @@ -152,12 +179,24 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA { *x ^= mask_key[i & 0xff]; } + if n_after_length + 4 + (length as usize) < n { + self.rx.msg_in_buf = Some(( + n_after_length + 4 + (length as usize), + (n - (n_after_length + 4 + (length as usize))), + )); + } &self.rx.buf[n_after_length + 4..n_after_length + 4 + length as usize] } else { if n_after_length + length as usize > n { - warn!("ws payload smaller than length"); + info!("ws payload smaller than length"); return Err(()); } + if n_after_length + (length as usize) < n { + self.rx.msg_in_buf = Some(( + n_after_length + (length as usize), + (n - (n_after_length + (length as usize))), + )); + } &self.rx.buf[n_after_length..n_after_length + length as usize] }; self.rx.last_msg = Instant::now(); @@ -165,9 +204,10 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA // Text message 1 => { let content = from_utf8(&content).map_err(|_| ())?; - info!("Received text : {:?}", content); Ok(Some(WsMsg::Text(content))) } + // Bytes + 2 => Ok(Some(WsMsg::Bytes(content))), // Ping 9 => { self.tx.send(WsMsg::Pong(&content)).await?; @@ -181,7 +221,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA } } } - pub async fn send(&mut self, msg: WsMsg<'a>) -> Result<(), ()> { + pub async fn send(&mut self, msg: WsMsg<'_>) -> Result<(), ()> { self.tx.send(msg).await } }