From 317b8084e078887a7e41ae81ce0bbd087379620b Mon Sep 17 00:00:00 2001 From: Arkitu <85173315+Arkitu@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:08:03 +0200 Subject: [PATCH] organise code --- src/game.rs | 162 +++++++++++++++++++++++ src/lib.rs | 17 +++ src/main.rs | 349 ++------------------------------------------------ src/socket.rs | 182 ++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 341 deletions(-) create mode 100644 src/game.rs create mode 100644 src/lib.rs create mode 100644 src/socket.rs diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..c6e33c5 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,162 @@ +use core::{ops::Not, sync::atomic::Ordering}; +use heapless::Vec; +use pico_website::unwrap; +use portable_atomic::{AtomicBool, AtomicU32}; +use core::fmt::Write; + +use crate::socket::HttpResCode; + +static TURN: AtomicBool = AtomicBool::new(false); +// bits [0; 8] : player zero board / bits [9; 17] : player one board +static BOARD: AtomicU32 = AtomicU32::new(0); + +pub struct GameClient { + res_buf: Vec +} +impl GameClient { + pub fn new() -> Self { + Self { + res_buf: Vec::new(), + } + } + pub async fn handle_request<'a>(&'a mut self, path: &str, team: Team) -> (HttpResCode, &'static str, &'a [u8]) { + if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/board" { + 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") && 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 + & ((2_u32.pow(clicked_c as u32)) + + (2_u32.pow(9 + clicked_c as u32))) + != 0 + { + return (HttpResCode::Forbidden, "", &[]); + } + board = board | 2_u32.pow((team as u32 * 9) + clicked_c as u32); + turn = (!team).into(); + BOARD.store(board, Ordering::Release); + TURN.store(turn, Ordering::Release); + } + + self.res_buf.clear(); + for c in 0..=8 { + let picked_by = if board & 2_u32.pow(c) != 0 { + Some(Team::Zero) + } else if board & 2_u32.pow(9 + c) != 0 { + Some(Team::One) + } else { + None + }; + match picked_by { + Some(Team::Zero) => { + unwrap(self.res_buf + .extend_from_slice( + b"
", + )).await; + } + Some(Team::One) => { + unwrap(self.res_buf.extend_from_slice( + b"
", + )).await; + } + None => if team == turn.into() { + unwrap(write!( + self.res_buf, + "", + c + )).await; + } else { + unwrap(self.res_buf.extend_from_slice( + b"
", + )).await; + } + }; + } + (HttpResCode::Ok, "html", &self.res_buf) + } else { + (HttpResCode::NotFound, "", &[]) + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Team { + Zero = 0, + One = 1, +} +impl From for Team { + fn from(value: bool) -> Self { + if value { Team::One } else { Team::Zero } + } +} +impl Into for Team { + fn into(self) -> bool { + match self { + Team::Zero => false, + Team::One => true + } + } +} +impl Not for Team { + type Output = Team; + fn not(self) -> Self::Output { + match self { + Team::Zero => Team::One, + Team::One => Team::Zero + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Cell { + C0 = 0, + C1 = 1, + C2 = 2, + C3 = 3, + C4 = 4, + C5 = 5, + C6 = 6, + C7 = 7, + C8 = 8, +} +impl TryFrom for Cell { + type Error = (); + fn try_from(value: char) -> Result { + Ok(match value { + '0' => Cell::C0, + '1' => Cell::C1, + '2' => Cell::C2, + '3' => Cell::C3, + '4' => Cell::C4, + '5' => Cell::C5, + '6' => Cell::C6, + '7' => Cell::C7, + '8' => Cell::C8, + _ => return Err(()), + }) + } +} +impl TryFrom for Cell { + type Error = (); + fn try_from(value: u8) -> Result { + Ok(match value { + 0 => Cell::C0, + 1 => Cell::C1, + 2 => Cell::C2, + 3 => Cell::C3, + 4 => Cell::C4, + 5 => Cell::C5, + 6 => Cell::C6, + 7 => Cell::C7, + 8 => Cell::C8, + _ => return Err(()), + }) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7be1180 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +#![no_std] + +use core::fmt::Debug; +use embassy_time::Timer; +use log::error; + +pub async fn unwrap(res: Result) -> T { + match res { + Ok(v) => v, + Err(e) => { + error!("FATAL ERROR : {:?}", e); + loop { + Timer::after_millis(0).await; + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 67098f1..295e198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,33 +4,25 @@ #![feature(impl_trait_in_assoc_type)] #![feature(slice_split_once)] -use core::fmt::{Debug, Display, Write}; use core::net::Ipv4Addr; -use core::ops::Not; -use core::str::from_utf8; -use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use cortex_m::interrupt::Mutex; use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi}; -use embassy_executor::{Executor, Spawner}; -use embassy_net::tcp::TcpSocket; +use embassy_executor::Spawner; use embassy_net::{Config, DhcpConfig, StackResources}; use embassy_rp::bind_interrupts; use embassy_rp::clocks::RoscRng; use embassy_rp::gpio::{Level, Output}; -use embassy_rp::multicore::{Stack, spawn_core1}; use embassy_rp::peripherals::USB; use embassy_rp::peripherals::{DMA_CH0, PIO0}; -use embassy_rp::pio::program::ProgramWithDefines; use embassy_rp::pio::{InterruptHandler as PioInterruptHandler, Pio}; use embassy_rp::usb::{Driver, InterruptHandler as UsbInterruptHandler}; -use embassy_time::Duration; -use embassy_time::Timer; -use embedded_io_async::Write as _; -use heapless::{String, Vec}; -use log::{debug, error, info, warn}; +use log::info; use rand_core::RngCore; use static_cell::StaticCell; use {defmt_rtt as _, panic_probe as _}; +use pico_website::unwrap; + +mod socket; +mod game; bind_interrupts!(struct Irqs { USBCTRL_IRQ => UsbInterruptHandler; @@ -54,18 +46,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'sta runner.run().await } -async fn unwrap(res: Result) -> T { - match res { - Ok(v) => v, - Err(e) => { - error!("FATAL ERROR : {:?}", e); - loop { - Timer::after_millis(0).await; - } - } - } -} - #[embassy_executor::main] async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); @@ -171,321 +151,8 @@ async fn main(spawner: Spawner) { ) } - unwrap(spawner.spawn(listen_task(stack, Team::Zero, 80))).await; - unwrap(spawner.spawn(listen_task(stack, Team::One, 81))).await; -} - -static TURN: AtomicBool = AtomicBool::new(false); -// bits [0; 8] : player zero board / bits [9; 17] : player one board -static BOARD: AtomicU32 = AtomicU32::new(0); - -#[embassy_executor::task(pool_size = 2)] -async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u16) { - // loop { - // info!("team:{:?}", team); - // Timer::after_millis(0).await; - // } - let mut rx_buffer = [0; 4096]; - let mut tx_buffer = [0; 4096]; - let mut buf = [0; 4096]; - let mut res_head_buf = Vec::::new(); - let mut res_buf = Vec::::new(); - loop { - Timer::after_secs(0).await; - let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - socket.set_timeout(Some(Duration::from_secs(30))); - - info!("Socket {:?}: Listening on TCP:{}...", team, port); - if let Err(e) = socket.accept(port).await { - warn!("accept error: {:?}", e); - continue; - } - - info!( - "Socket {:?}: Received connection from {:?}", - team, - socket.remote_endpoint() - ); - - loop { - Timer::after_secs(0).await; - let n = match socket.read(&mut buf).await { - Ok(0) => { - warn!("read EOF"); - break; - } - Ok(n) => n, - Err(e) => { - warn!("Socket {:?}: read error: {:?}", team, e); - break; - } - }; - - let mut headers: &[u8] = &buf[..n]; - let mut content: &[u8] = &[]; - for i in 0..(n - 1) { - if &buf[i..i + 1] == b"\r\n" { - headers = &buf[0..i]; - if i + 2 < n { - content = &buf[i + 2..n]; - } - } - } - - let mut headers = headers.split(|x| *x == b'\n'); - let (request_type, path) = match headers.next() { - None => { - warn!("Empty request"); - break; - } - Some(l1) => { - let mut l1 = l1.split(|x| *x == b' '); - ( - match l1.next() { - Some(b"GET") => HttpRequestType::Get, - Some(b"POST") => HttpRequestType::Post, - Some(t) => { - warn!("Unknown request type : {}", unwrap(from_utf8(t)).await); - break; - } - None => { - warn!("No request type"); - break; - } - }, - match l1.next() { - Some(path) => unwrap(from_utf8(path)).await, - None => { - warn!("No path"); - break; - } - }, - ) - } - }; - - info!( - "Socket {:?}: {:?} request for {}", - team, - request_type, - path - ); - Timer::after_secs(0).await; - - let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { - "/" => (HttpResCode::Ok, "html", include_bytes!("../web/index.html")), - "/htmx.min.js" => ( - HttpResCode::Ok, - "javascript", - include_bytes!("../web/htmx.min.js"), - ), - "/htmx.js" => ( - HttpResCode::Ok, - "javascript", - include_bytes!("../web/htmx.js"), - ), - p => 'res: { - if (p.starts_with("/ttt/cell") && p.len() == 10) || p == "/ttt/board" { - let mut board = BOARD.load(Ordering::Acquire); - let mut turn = TURN.load(Ordering::Acquire); - - // just return correct board in case of unauthorized move - if p.starts_with("/ttt/cell") && team == turn.into() { - let clicked_c: Cell = match TryInto::::try_into( - unwrap(p.chars().nth(9).ok_or("no 9th char")).await, - ) { - Ok(c) => c, - Err(_) => break 'res (HttpResCode::NotFound, "", &[]), - }; - if board - & ((2_u32.pow(clicked_c as u32)) - + (2_u32.pow(9 + clicked_c as u32))) - != 0 - { - break 'res (HttpResCode::Forbidden, "", &[]); - } - board = board | 2_u32.pow((team as u32 * 9) + clicked_c as u32); - turn = (!team).into(); - BOARD.store(board, Ordering::Release); - TURN.store(turn, Ordering::Release); - } - - res_buf.clear(); - for c in 0..=8 { - let picked_by = if board & 2_u32.pow(c) != 0 { - Some(Team::Zero) - } else if board & 2_u32.pow(9 + c) != 0 { - Some(Team::One) - } else { - None - }; - match picked_by { - Some(Team::Zero) => { - unwrap(res_buf - .extend_from_slice( - b"
", - )).await; - } - Some(Team::One) => { - unwrap(res_buf.extend_from_slice( - b"
", - )).await; - } - None => if team == turn.into() { - unwrap(write!( - &mut res_buf, - "", - c - )).await; - } else { - unwrap(res_buf.extend_from_slice( - b"
", - )).await; - } - }; - } - (HttpResCode::Ok, "html", &res_buf) - } else { - (HttpResCode::NotFound, "", &[]) - } - } - }; - - res_head_buf.clear(); - if let Err(e) = write!( - &mut res_head_buf, - "{}\r\n\ - Content-Type: text/{}\r\n\ - Content-Length: {}\r\n\r\n", - Into::<&str>::into(code), - res_type, - res_content.len() - ) { - warn!("res buffer write error: {:?}", e); - break; - } - - match socket.write_all(&res_head_buf).await { - Ok(()) => {} - Err(e) => { - warn!("write error: {:?}", e); - break; - } - }; - match socket.write_all(&res_content).await { - Ok(()) => {} - Err(e) => { - warn!("write error: {:?}", e); - break; - } - }; - } - } -} - -#[derive(Clone, Copy, Debug)] -enum HttpRequestType { - Get, - Post, -} -impl Into<&str> for HttpRequestType { - fn into(self) -> &'static str { - match self { - Self::Get => "GET", - Self::Post => "POST" - } - } -} - -#[derive(Debug, Clone, Copy)] -enum HttpResCode { - Ok, - NotFound, - Forbidden, -} -impl Into<&str> for HttpResCode { - fn into(self) -> &'static str { - match self { - HttpResCode::Ok => "HTTP/1.1 200 OK", - HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND", - HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Team { - Zero = 0, - One = 1, -} -impl From for Team { - fn from(value: bool) -> Self { - if value { Team::One } else { Team::Zero } - } -} -impl Into for Team { - fn into(self) -> bool { - match self { - Team::Zero => false, - Team::One => true - } - } -} -impl Not for Team { - type Output = Team; - fn not(self) -> Self::Output { - match self { - Team::Zero => Team::One, - Team::One => Team::Zero - } - } -} - -#[derive(Debug, Clone, Copy)] -enum Cell { - C0 = 0, - C1 = 1, - C2 = 2, - C3 = 3, - C4 = 4, - C5 = 5, - C6 = 6, - C7 = 7, - C8 = 8, -} -impl TryFrom for Cell { - type Error = (); - fn try_from(value: char) -> Result { - Ok(match value { - '0' => Cell::C0, - '1' => Cell::C1, - '2' => Cell::C2, - '3' => Cell::C3, - '4' => Cell::C4, - '5' => Cell::C5, - '6' => Cell::C6, - '7' => Cell::C7, - '8' => Cell::C8, - _ => return Err(()), - }) - } -} -impl TryFrom for Cell { - type Error = (); - fn try_from(value: u8) -> Result { - Ok(match value { - 0 => Cell::C0, - 1 => Cell::C1, - 2 => Cell::C2, - 3 => Cell::C3, - 4 => Cell::C4, - 5 => Cell::C5, - 6 => Cell::C6, - 7 => Cell::C7, - 8 => Cell::C8, - _ => return Err(()), - }) - } + unwrap(spawner.spawn(socket::listen_task(stack, game::Team::Zero, 80))).await; + unwrap(spawner.spawn(socket::listen_task(stack, game::Team::One, 81))).await; } #[cfg(feature="wifi-connect")] diff --git a/src/socket.rs b/src/socket.rs new file mode 100644 index 0000000..4b8350d --- /dev/null +++ b/src/socket.rs @@ -0,0 +1,182 @@ +use core::str::from_utf8; +use embassy_net::tcp::TcpSocket; +use embassy_time::{Duration, Timer}; +use heapless::Vec; +use log::{info, warn}; +use pico_website::unwrap; +use embedded_io_async::Write as _; +use core::fmt::Write; + +use crate::game::{self, GameClient, Team}; + +#[embassy_executor::task(pool_size = 2)] +pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u16) { + // loop { + // info!("team:{:?}", team); + // Timer::after_millis(0).await; + // } + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut buf = [0; 4096]; + let mut res_head_buf = Vec::::new(); + + let mut game_client = GameClient::new(); + + loop { + Timer::after_secs(0).await; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(30))); + + info!("Socket {:?}: Listening on TCP:{}...", team, port); + if let Err(e) = socket.accept(port).await { + warn!("accept error: {:?}", e); + continue; + } + + info!( + "Socket {:?}: Received connection from {:?}", + team, + socket.remote_endpoint() + ); + + loop { + Timer::after_secs(0).await; + let n = match socket.read(&mut buf).await { + Ok(0) => { + warn!("read EOF"); + break; + } + Ok(n) => n, + Err(e) => { + warn!("Socket {:?}: read error: {:?}", team, e); + break; + } + }; + + let mut headers: &[u8] = &buf[..n]; + let mut content: &[u8] = &[]; + for i in 0..(n - 1) { + if &buf[i..i + 1] == b"\r\n" { + headers = &buf[0..i]; + if i + 2 < n { + content = &buf[i + 2..n]; + } + } + } + + let mut headers = headers.split(|x| *x == b'\n'); + let (request_type, path) = match headers.next() { + None => { + warn!("Empty request"); + break; + } + Some(l1) => { + let mut l1 = l1.split(|x| *x == b' '); + ( + match l1.next() { + Some(b"GET") => HttpRequestType::Get, + Some(b"POST") => HttpRequestType::Post, + Some(t) => { + warn!("Unknown request type : {}", unwrap(from_utf8(t)).await); + break; + } + None => { + warn!("No request type"); + break; + } + }, + match l1.next() { + Some(path) => unwrap(from_utf8(path)).await, + None => { + warn!("No path"); + break; + } + }, + ) + } + }; + + info!( + "Socket {:?}: {:?} request for {}", + team, + request_type, + path + ); + Timer::after_secs(0).await; + + let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { + "/" => (HttpResCode::Ok, "html", include_bytes!("../web/index.html")), + "/htmx.min.js" => ( + HttpResCode::Ok, + "javascript", + include_bytes!("../web/htmx.min.js"), + ), + "/htmx.js" => ( + HttpResCode::Ok, + "javascript", + include_bytes!("../web/htmx.js"), + ), + p => game_client.handle_request(p, team).await + }; + + res_head_buf.clear(); + if let Err(e) = write!( + &mut res_head_buf, + "{}\r\n\ + Content-Type: text/{}\r\n\ + Content-Length: {}\r\n\r\n", + Into::<&str>::into(code), + res_type, + res_content.len() + ) { + warn!("res buffer write error: {:?}", e); + break; + } + + match socket.write_all(&res_head_buf).await { + Ok(()) => {} + Err(e) => { + warn!("write error: {:?}", e); + break; + } + }; + match socket.write_all(&res_content).await { + Ok(()) => {} + Err(e) => { + warn!("write error: {:?}", e); + break; + } + }; + } + } +} + +#[derive(Clone, Copy, Debug)] +enum HttpRequestType { + Get, + Post, +} +impl Into<&str> for HttpRequestType { + fn into(self) -> &'static str { + match self { + Self::Get => "GET", + Self::Post => "POST" + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum HttpResCode { + Ok, + NotFound, + Forbidden, +} +impl Into<&str> for HttpResCode { + fn into(self) -> &'static str { + match self { + HttpResCode::Ok => "HTTP/1.1 200 OK", + HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND", + HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN", + } + } +} \ No newline at end of file