refactor + index page

This commit is contained in:
Arkitu 2025-04-29 23:07:26 +02:00
parent 6bc4d823b7
commit f085ceba1e
9 changed files with 187 additions and 99 deletions

11
src/apps/index.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<head> </head>
<html>
<body>
<h1>Apps</h1>
<ul>
<li><a href="http://pico.wifi:8080">Tic Tac Toe</a> (team blue)</li>
<li><a href="http://pico.wifi:8081">Tic Tac Toe</a> (team red)</li>
</ul>
</body>
</html>

18
src/apps/index.rs Normal file
View File

@ -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, "", &[]),
}
}
}

9
src/apps/mod.rs Normal file
View File

@ -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]);
}

View File

@ -1,30 +1,32 @@
use core::fmt::Write;
use core::{ops::Not, sync::atomic::Ordering}; use core::{ops::Not, sync::atomic::Ordering};
use embassy_time::{Duration, Instant}; use embassy_time::{Duration, Instant};
use heapless::Vec; use heapless::Vec;
use pico_website::unwrap; use pico_website::unwrap;
use portable_atomic::{AtomicBool, AtomicU32}; use portable_atomic::{AtomicBool, AtomicU32};
use core::fmt::Write;
use crate::socket::HttpResCode; use crate::socket::HttpResCode;
use super::App;
static TURN: AtomicBool = AtomicBool::new(false); static TURN: AtomicBool = AtomicBool::new(false);
// bits [0; 8] : player zero board / bits [9; 17] : player one board // bits [0; 8] : player zero board / bits [9; 17] : player one board
static BOARD: AtomicU32 = AtomicU32::new(0); static BOARD: AtomicU32 = AtomicU32::new(0);
pub struct GameClient { pub struct TttApp {
res_buf: Vec<u8, 4096>, res_buf: Vec<u8, 4096>,
/// State of the board last time it has been sent /// State of the board last time it has been sent
last_board: u32, last_board: u32,
team: Team, team: Team,
end: Option<(Instant, Option<Team>)> end: Option<(Instant, Option<Team>)>,
} }
impl GameClient { impl TttApp {
pub fn new(team: Team) -> Self { pub fn new(team: Team) -> Self {
Self { Self {
res_buf: Vec::new(), res_buf: Vec::new(),
last_board: 0, last_board: 0,
team, team,
end: None end: None,
} }
} }
pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) { pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) {
@ -40,15 +42,15 @@ impl GameClient {
0b010010010, 0b010010010,
0b001001001, 0b001001001,
0b100010001, 0b100010001,
0b001010100 0b001010100,
] { ] {
if board & (w << m) == (w << m) { if board & (w << m) == (w << m) {
return (true, Some(t)) return (true, Some(t));
} }
} }
} }
if ((board | (board >> 9)) & 0b111111111) == 0b111111111 { if ((board | (board >> 9)) & 0b111111111) == 0b111111111 {
return (true, None) return (true, None);
} }
(false, None) (false, None)
} }
@ -66,7 +68,12 @@ impl GameClient {
} }
} }
/// Generate board html /// 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(); self.res_buf.clear();
if outer_html { if outer_html {
unwrap(self.res_buf.extend_from_slice( unwrap(self.res_buf.extend_from_slice(
@ -76,26 +83,28 @@ impl GameClient {
hx-swap=\"innerHTML\" \ hx-swap=\"innerHTML\" \
hx-trigger=\"every 100ms\" \ hx-trigger=\"every 100ms\" \
hx-target=\"this\"\ hx-target=\"this\"\
>" >",
)).await; ))
.await;
} }
unwrap(write!( unwrap(write!(
self.res_buf, self.res_buf,
"<h3>Team : <span style=\"color:{}\">{}</span></h3>", "<h3>Team : <span style=\"color:{}\">{}</span></h3>",
self.team.color(), self.team.color(),
self.team.name() self.team.name()
)).await; ))
.await;
match self.end { match self.end {
Some((_, Some(t))) => unwrap(write!( Some((_, Some(t))) => {
unwrap(write!(
self.res_buf, self.res_buf,
"<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>", "<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>",
t.color(), t.color(),
t.name() t.name()
)).await, ))
Some((_, None)) => unwrap(write!( .await
self.res_buf, }
"<br><h3>Draw!</h3><br>", Some((_, None)) => unwrap(write!(self.res_buf, "<br><h3>Draw!</h3><br>",)).await,
)).await,
None => {} None => {}
} }
unwrap(self.res_buf.extend_from_slice(b"<div id=\"grid\">")).await; unwrap(self.res_buf.extend_from_slice(b"<div id=\"grid\">")).await;
@ -113,18 +122,23 @@ impl GameClient {
self.res_buf, self.res_buf,
"<div class=\"cell\" style=\"background-color:{}\"></div>", "<div class=\"cell\" style=\"background-color:{}\"></div>",
t.color() t.color()
)).await; ))
.await;
} }
None => if self.team == turn.into() && self.end.is_none() { None => {
if self.team == turn.into() && self.end.is_none() {
unwrap(write!( unwrap(write!(
self.res_buf, self.res_buf,
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>", "<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>",
c c
)).await; )).await;
} else { } else {
unwrap(self.res_buf.extend_from_slice( unwrap(
b"<div class=\"cell\"></div>", self.res_buf
)).await; .extend_from_slice(b"<div class=\"cell\"></div>"),
)
.await;
}
} }
}; };
} }
@ -134,7 +148,27 @@ impl GameClient {
} }
&self.res_buf &self.res_buf
} }
pub async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) { }
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" { if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/game" {
let mut board = BOARD.load(Ordering::Acquire); let mut board = BOARD.load(Ordering::Acquire);
let mut turn = TURN.load(Ordering::Acquire); let mut turn = TURN.load(Ordering::Acquire);
@ -147,11 +181,7 @@ impl GameClient {
Ok(c) => c, Ok(c) => c,
Err(_) => return (HttpResCode::NotFound, "", &[]), Err(_) => return (HttpResCode::NotFound, "", &[]),
}; };
if board if board & ((1 << (clicked_c as u32)) + (1 << (9 + clicked_c as u32))) != 0
& (
(1<<(clicked_c as u32)) + (1<<(9 + clicked_c as u32))
)
!= 0
{ {
return (HttpResCode::Forbidden, "", &[]); return (HttpResCode::Forbidden, "", &[]);
} }
@ -163,19 +193,21 @@ impl GameClient {
self.update_end_state(&mut board); self.update_end_state(&mut board);
if self.last_board != board { if self.last_board != board {
self.last_board = board; self.last_board = board;
(HttpResCode::Ok, "html", self.generate_board_res(board, turn.into(), false).await) (
HttpResCode::Ok,
"html",
self.generate_board_res(board, turn.into(), false).await,
)
} else { } else {
(HttpResCode::NoContent, "", &[]) (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 { } else {
(HttpResCode::NotFound, "", &[]) (HttpResCode::NotFound, "", &[])
} }
} }
} }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Team { pub enum Team {
@ -191,7 +223,7 @@ impl Into<bool> for Team {
fn into(self) -> bool { fn into(self) -> bool {
match self { match self {
Team::Zero => false, Team::Zero => false,
Team::One => true Team::One => true,
} }
} }
} }
@ -200,7 +232,7 @@ impl Not for Team {
fn not(self) -> Self::Output { fn not(self) -> Self::Output {
match self { match self {
Team::Zero => Team::One, Team::Zero => Team::One,
Team::One => Team::Zero Team::One => Team::Zero,
} }
} }
} }
@ -208,13 +240,13 @@ impl Team {
fn color(self) -> &'static str { fn color(self) -> &'static str {
match self { match self {
Team::Zero => "dodgerblue", Team::Zero => "dodgerblue",
Team::One => "firebrick" Team::One => "firebrick",
} }
} }
fn name(self) -> &'static str { fn name(self) -> &'static str {
match self { match self {
Team::Zero => "blue", Team::Zero => "blue",
Team::One => "red" Team::One => "red",
} }
} }
} }

View File

@ -152,12 +152,12 @@ async fn write_dhcp_opts<const N: usize>(buf: &mut Vec<u8, N>, op_codes: &[u8])
6 => (4, &[192, 254, 0, 2]), // DhcpOption::DomainNameServer(&[dhcpv4::Addr([0, 0, 0, 0])]), 6 => (4, &[192, 254, 0, 2]), // DhcpOption::DomainNameServer(&[dhcpv4::Addr([0, 0, 0, 0])]),
12 => (4, b"blue"), // DhcpOption::HostName(b"blue"), 12 => (4, b"blue"), // DhcpOption::HostName(b"blue"),
15 => (4, b"wifi"), // DhcpOption::DomainName(b"LocalDomain"), 15 => (4, b"wifi"), // DhcpOption::DomainName(b"LocalDomain"),
26 => (2, &1514_u16.to_be_bytes()), // DhcpOption::Unknown(26, &[0x5, 0xEA]), // mtu 26 => (2, &1514_u16.to_be_bytes()), // mtu
28 => (4, &[192, 254, 0, 255]), // DhcpOption::Unknown(28, &[192, 254, 0, 255]), // broadcast 28 => (4, &[192, 254, 0, 255]), // broadcast
51 => (4, &700_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(700), 51 => (4, &3600_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(3600),
54 => (4, &[192, 254, 0, 2]), // DhcpOption::ServerIdentifier(&dhcpv4::Addr([192, 254, 0, 2])), 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 58 => (4, &3400_u32.to_be_bytes()), // renewal time
59 => (4, &600_u32.to_be_bytes()), // DhcpOption::Unknown(59, &[0, 0, 0x2, 0x58]), // rebinding time = 600s 59 => (4, &3500_u32.to_be_bytes()), // rebinding time
80 => (0, &[]), 80 => (0, &[]),
_ => { _ => {
info!("Dhcp: unhandled requested option {}", o); info!("Dhcp: unhandled requested option {}", o);

View File

@ -59,13 +59,13 @@ pub async fn dns_server(stack: Stack<'static>) {
for q in msg.questions() { for q in msg.questions() {
match q.kind() { match q.kind() {
QueryKind::A => { 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_question(&q);
res.add_answer(&Answer { res.add_answer(&Answer {
name: q.name().clone(), name: q.name().clone(),
kind: QueryKind::A, kind: QueryKind::A,
class: QueryClass::IN, class: QueryClass::IN,
ttl: 60, ttl: 600,
rdata: &[192, 254, 0, 2], rdata: &[192, 254, 0, 2],
}); });
info!("Dns: Giving {}", q.name()); info!("Dns: Giving {}", q.name());

View File

@ -25,7 +25,7 @@ mod dhcp;
#[cfg(feature = "dns")] #[cfg(feature = "dns")]
mod dns; mod dns;
mod game; mod apps;
mod socket; mod socket;
bind_interrupts!(struct Irqs { bind_interrupts!(struct Irqs {
@ -115,7 +115,7 @@ async fn main(spawner: Spawner) {
let seed = rng.next_u64(); let seed = rng.next_u64();
// Init network stack // Init network stack
static RESOURCES: StaticCell<StackResources<4>> = StaticCell::new(); static RESOURCES: StaticCell<StackResources<10>> = StaticCell::new();
let (stack, runner) = embassy_net::new( let (stack, runner) = embassy_net::new(
net_device, net_device,
config, config,
@ -126,8 +126,8 @@ async fn main(spawner: Spawner) {
unwrap(spawner.spawn(net_task(runner))).await; unwrap(spawner.spawn(net_task(runner))).await;
#[cfg(not(feature = "wifi-connect"))] #[cfg(not(feature = "wifi-connect"))]
control.start_ap_open("TicTacToe", 5).await; control.start_ap_open("pico", 5).await;
// control.start_ap_wpa2("TicTacToe", "password", 5).await; // control.start_ap_wpa2("pico", "password", 5).await;
#[cfg(feature = "wifi-connect")] #[cfg(feature = "wifi-connect")]
{ {
@ -169,8 +169,19 @@ async fn main(spawner: Spawner) {
#[cfg(feature = "dns")] #[cfg(feature = "dns")]
unwrap(spawner.spawn(dns::dns_server(stack))).await; unwrap(spawner.spawn(dns::dns_server(stack))).await;
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::Zero, 80))).await; unwrap(spawner.spawn(socket::index_listen_task(stack, 80))).await;
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::One, 81))).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")] #[cfg(feature = "wifi-connect")]

View File

@ -6,10 +6,19 @@ use embedded_io_async::Write as _;
use heapless::Vec; use heapless::Vec;
use log::{info, warn}; use log::{info, warn};
use crate::game::{GameClient, Team}; use crate::apps::{App, index::IndexApp, ttt};
#[embassy_executor::task(pool_size = 2)] #[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 { // loop {
// info!("team:{:?}", team); // info!("team:{:?}", team);
// Timer::after_millis(0).await; // 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 buf = [0; 4096];
let mut res_head_buf = Vec::<u8, 4096>::new(); let mut res_head_buf = Vec::<u8, 4096>::new();
let mut game_client = GameClient::new(team);
loop { loop {
Timer::after_secs(0).await; Timer::after_secs(0).await;
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
socket.set_timeout(Some(Duration::from_secs(30))); 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 { if let Err(e) = socket.accept(port).await {
warn!("accept error: {:?}", e); warn!("accept error: {:?}", e);
continue; continue;
} }
info!( info!(
"Socket {:?}: Received connection from {:?}", "Socket {}: Received connection from {:?}",
team, app.socket_name(),
socket.remote_endpoint() socket.remote_endpoint()
); );
@ -47,7 +54,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
} }
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
warn!("Socket {:?}: read error: {:?}", team, e); warn!("Socket {}: read error: {:?}", app.socket_name(), e);
break; 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; Timer::after_secs(0).await;
let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path {
"/" => (
HttpResCode::Ok,
"html",
include_bytes!("../static/index.html"),
),
"/htmx.js" => ( "/htmx.js" => (
HttpResCode::Ok, HttpResCode::Ok,
"javascript", "javascript",
@ -131,7 +138,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
include_bytes!("../static/htmx.min.js"), include_bytes!("../static/htmx.min.js"),
), ),
p => game_client.handle_request(p).await, p => app.handle_request(p).await,
}; };
res_head_buf.clear(); res_head_buf.clear();