diff --git a/src/apps/index.html b/src/apps/index.html new file mode 100644 index 0000000..c5396cb --- /dev/null +++ b/src/apps/index.html @@ -0,0 +1,11 @@ + + + + +

Apps

+ + + diff --git a/src/apps/index.rs b/src/apps/index.rs new file mode 100644 index 0000000..03114cf --- /dev/null +++ b/src/apps/index.rs @@ -0,0 +1,18 @@ +use crate::socket::HttpResCode; + +use super::App; + +pub struct IndexApp; +impl App for IndexApp { + fn socket_name(&self) -> &'static str { + "index" + } + async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) { + match path { + "/" | "/index" | "/index.html" => { + (HttpResCode::Ok, "html", include_bytes!("./index.html")) + } + _ => (HttpResCode::NotFound, "", &[]), + } + } +} diff --git a/src/apps/mod.rs b/src/apps/mod.rs new file mode 100644 index 0000000..9699932 --- /dev/null +++ b/src/apps/mod.rs @@ -0,0 +1,9 @@ +use crate::socket::HttpResCode; + +pub mod index; +pub mod ttt; + +pub trait App { + fn socket_name(&self) -> &'static str; + async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]); +} diff --git a/static/index.html b/src/apps/ttt.html similarity index 100% rename from static/index.html rename to src/apps/ttt.html diff --git a/src/game.rs b/src/apps/ttt.rs similarity index 56% rename from src/game.rs rename to src/apps/ttt.rs index 67903cb..3d4d049 100644 --- a/src/game.rs +++ b/src/apps/ttt.rs @@ -1,30 +1,32 @@ +use core::fmt::Write; use core::{ops::Not, sync::atomic::Ordering}; use embassy_time::{Duration, Instant}; use heapless::Vec; use pico_website::unwrap; use portable_atomic::{AtomicBool, AtomicU32}; -use core::fmt::Write; use crate::socket::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); -pub struct GameClient { +pub struct TttApp { res_buf: Vec, /// State of the board last time it has been sent last_board: u32, team: Team, - end: Option<(Instant, Option)> + end: Option<(Instant, Option)>, } -impl GameClient { +impl TttApp { pub fn new(team: Team) -> Self { Self { res_buf: Vec::new(), last_board: 0, team, - end: None + end: None, } } pub fn is_ended(&self, board: u32) -> (bool, Option) { @@ -40,21 +42,21 @@ impl GameClient { 0b010010010, 0b001001001, 0b100010001, - 0b001010100 + 0b001010100, ] { - if board & (w<>9)) & 0b111111111) == 0b111111111 { - return (true, None) + if ((board | (board >> 9)) & 0b111111111) == 0b111111111 { + return (true, None); } (false, None) } pub fn update_end_state(&mut self, board: &mut u32) { if let Some((i, _)) = self.end { - if i+Duration::from_secs(7) < Instant::now() { + if i + Duration::from_secs(7) < Instant::now() { self.end = None; BOARD.store(0, Ordering::Release); *board = 0; @@ -66,7 +68,12 @@ impl GameClient { } } /// Generate board html - pub async fn generate_board_res<'a>(&'a mut self, board: u32, turn: Team, outer_html: bool) -> &'a [u8] { + async fn generate_board_res<'a>( + &'a mut self, + board: u32, + turn: Team, + outer_html: bool, + ) -> &'a [u8] { self.res_buf.clear(); if outer_html { unwrap(self.res_buf.extend_from_slice( @@ -76,33 +83,35 @@ impl GameClient { hx-swap=\"innerHTML\" \ hx-trigger=\"every 100ms\" \ hx-target=\"this\"\ - >" - )).await; + >", + )) + .await; } unwrap(write!( self.res_buf, "

Team : {}

", self.team.color(), self.team.name() - )).await; + )) + .await; match self.end { - Some((_, Some(t))) => unwrap(write!( + 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, + )) + .await + } + Some((_, None)) => unwrap(write!(self.res_buf, "

Draw!


",)).await, None => {} } unwrap(self.res_buf.extend_from_slice(b"
")).await; for c in 0..=8 { - let picked_by = if board & (1<
", t.color() - )).await; + )) + .await; } - None => if self.team == turn.into() && self.end.is_none() { - unwrap(write!( + None => { + if self.team == turn.into() && self.end.is_none() { + unwrap(write!( self.res_buf, "", c )).await; - } else { - unwrap(self.res_buf.extend_from_slice( - b"
", - )).await; + } else { + unwrap( + self.res_buf + .extend_from_slice(b"
"), + ) + .await; + } } }; } @@ -134,45 +148,63 @@ impl GameClient { } &self.res_buf } - pub async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) { - 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, "", &[]); +impl App for TttApp { + fn socket_name(&self) -> &'static str { + self.team.name() + } + async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) { + match path { + "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => { + (HttpResCode::Ok, "html", include_bytes!("./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, "", &[]) } - 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 if path == "/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) - } else { - (HttpResCode::NotFound, "", &[]) } } } @@ -191,7 +223,7 @@ impl Into for Team { fn into(self) -> bool { match self { Team::Zero => false, - Team::One => true + Team::One => true, } } } @@ -200,7 +232,7 @@ impl Not for Team { fn not(self) -> Self::Output { match self { Team::Zero => Team::One, - Team::One => Team::Zero + Team::One => Team::Zero, } } } @@ -208,13 +240,13 @@ impl Team { fn color(self) -> &'static str { match self { Team::Zero => "dodgerblue", - Team::One => "firebrick" + Team::One => "firebrick", } } fn name(self) -> &'static str { match self { Team::Zero => "blue", - Team::One => "red" + Team::One => "red", } } } @@ -264,4 +296,4 @@ impl TryFrom for Cell { _ => return Err(()), }) } -} \ No newline at end of file +} diff --git a/src/dhcp.rs b/src/dhcp.rs index 6c65554..72471f7 100644 --- a/src/dhcp.rs +++ b/src/dhcp.rs @@ -152,12 +152,12 @@ async fn write_dhcp_opts(buf: &mut Vec, op_codes: &[u8]) 6 => (4, &[192, 254, 0, 2]), // DhcpOption::DomainNameServer(&[dhcpv4::Addr([0, 0, 0, 0])]), 12 => (4, b"blue"), // DhcpOption::HostName(b"blue"), 15 => (4, b"wifi"), // DhcpOption::DomainName(b"LocalDomain"), - 26 => (2, &1514_u16.to_be_bytes()), // DhcpOption::Unknown(26, &[0x5, 0xEA]), // mtu - 28 => (4, &[192, 254, 0, 255]), // DhcpOption::Unknown(28, &[192, 254, 0, 255]), // broadcast - 51 => (4, &700_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(700), + 26 => (2, &1514_u16.to_be_bytes()), // mtu + 28 => (4, &[192, 254, 0, 255]), // broadcast + 51 => (4, &3600_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(3600), 54 => (4, &[192, 254, 0, 2]), // DhcpOption::ServerIdentifier(&dhcpv4::Addr([192, 254, 0, 2])), - 58 => (4, &500_u32.to_be_bytes()), // DhcpOption::Unknown(58, &[0, 0, 0x1, 0xF4]), // renewal time = 500s - 59 => (4, &600_u32.to_be_bytes()), // DhcpOption::Unknown(59, &[0, 0, 0x2, 0x58]), // rebinding time = 600s + 58 => (4, &3400_u32.to_be_bytes()), // renewal time + 59 => (4, &3500_u32.to_be_bytes()), // rebinding time 80 => (0, &[]), _ => { info!("Dhcp: unhandled requested option {}", o); diff --git a/src/dns.rs b/src/dns.rs index 2280815..938025e 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -59,13 +59,13 @@ pub async fn dns_server(stack: Stack<'static>) { for q in msg.questions() { match q.kind() { QueryKind::A => { - if q.name() == "ttt.wifi" || q.name() == "www.ttt.wifi" { + if q.name() == "pico.wifi" || q.name() == "www.pico.wifi" { res.add_question(&q); res.add_answer(&Answer { name: q.name().clone(), kind: QueryKind::A, class: QueryClass::IN, - ttl: 60, + ttl: 600, rdata: &[192, 254, 0, 2], }); info!("Dns: Giving {}", q.name()); diff --git a/src/main.rs b/src/main.rs index b0adb48..8774e8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ mod dhcp; #[cfg(feature = "dns")] mod dns; -mod game; +mod apps; mod socket; bind_interrupts!(struct Irqs { @@ -115,7 +115,7 @@ async fn main(spawner: Spawner) { let seed = rng.next_u64(); // Init network stack - static RESOURCES: StaticCell> = StaticCell::new(); + static RESOURCES: StaticCell> = StaticCell::new(); let (stack, runner) = embassy_net::new( net_device, config, @@ -126,8 +126,8 @@ async fn main(spawner: Spawner) { unwrap(spawner.spawn(net_task(runner))).await; #[cfg(not(feature = "wifi-connect"))] - control.start_ap_open("TicTacToe", 5).await; - // control.start_ap_wpa2("TicTacToe", "password", 5).await; + control.start_ap_open("pico", 5).await; + // control.start_ap_wpa2("pico", "password", 5).await; #[cfg(feature = "wifi-connect")] { @@ -169,8 +169,19 @@ async fn main(spawner: Spawner) { #[cfg(feature = "dns")] unwrap(spawner.spawn(dns::dns_server(stack))).await; - unwrap(spawner.spawn(socket::listen_task(stack, game::Team::Zero, 80))).await; - unwrap(spawner.spawn(socket::listen_task(stack, game::Team::One, 81))).await; + unwrap(spawner.spawn(socket::index_listen_task(stack, 80))).await; + unwrap(spawner.spawn(socket::ttt_listen_task( + stack, + apps::ttt::TttApp::new(apps::ttt::Team::Zero), + 8080, + ))) + .await; + unwrap(spawner.spawn(socket::ttt_listen_task( + stack, + apps::ttt::TttApp::new(apps::ttt::Team::One), + 8081, + ))) + .await; } #[cfg(feature = "wifi-connect")] diff --git a/src/socket.rs b/src/socket.rs index 7e67863..9355839 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -6,10 +6,19 @@ use embedded_io_async::Write as _; use heapless::Vec; use log::{info, warn}; -use crate::game::{GameClient, Team}; +use crate::apps::{App, index::IndexApp, ttt}; #[embassy_executor::task(pool_size = 2)] -pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u16) { +pub async fn ttt_listen_task(stack: embassy_net::Stack<'static>, app: ttt::TttApp, port: u16) { + listen_task(stack, app, port).await +} + +#[embassy_executor::task(pool_size = 2)] +pub async fn index_listen_task(stack: embassy_net::Stack<'static>, port: u16) { + listen_task(stack, IndexApp, port).await +} + +pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, port: u16) { // loop { // info!("team:{:?}", team); // Timer::after_millis(0).await; @@ -19,22 +28,20 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u let mut buf = [0; 4096]; let mut res_head_buf = Vec::::new(); - let mut game_client = GameClient::new(team); - 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); + info!("Socket {}: Listening on TCP:{}...", app.socket_name(), port); if let Err(e) = socket.accept(port).await { warn!("accept error: {:?}", e); continue; } info!( - "Socket {:?}: Received connection from {:?}", - team, + "Socket {}: Received connection from {:?}", + app.socket_name(), socket.remote_endpoint() ); @@ -47,7 +54,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u } Ok(n) => n, Err(e) => { - warn!("Socket {:?}: read error: {:?}", team, e); + warn!("Socket {}: read error: {:?}", app.socket_name(), e); break; } }; @@ -114,15 +121,15 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u } }; - info!("Socket {:?}: {:?} request for {}", team, request_type, path); + info!( + "Socket {}: {:?} request for {}", + app.socket_name(), + request_type, + path + ); Timer::after_secs(0).await; let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { - "/" => ( - HttpResCode::Ok, - "html", - include_bytes!("../static/index.html"), - ), "/htmx.js" => ( HttpResCode::Ok, "javascript", @@ -131,7 +138,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u #[cfg(not(debug_assertions))] include_bytes!("../static/htmx.min.js"), ), - p => game_client.handle_request(p).await, + p => app.handle_request(p).await, }; res_head_buf.clear();