Compare commits

..

3 Commits

Author SHA1 Message Date
23d03920ae works! 2025-08-09 15:09:49 +02:00
1d7eaa028b working win game 2025-08-09 14:43:33 +02:00
5a9ac5c436 ttt backend api kinda works 2025-08-09 13:53:33 +02:00
10 changed files with 315 additions and 131 deletions

View File

@ -1,3 +1,6 @@
use heapless::Vec;
use pico_website::unwrap;
use crate::{
apps::Content,
socket::{HttpRequestType, HttpResCode},
@ -22,17 +25,50 @@ impl App for IndexApp {
"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),
path => {
let (path, args) = path.split_once('?').unwrap_or((path, ""));
let mut team = None;
for arg in args.split('&') {
match arg.split_once('=') {
Some(("team", "0")) => team = Some("0"),
Some(("team", "1")) => team = Some("1"),
_ => {}
}
}
if path == "/ttt" {
let Some(team) = team else {
return (HttpResCode::BadRequest, "", None);
};
let html = include_str!("ttt.html");
let mut content = Vec::new();
let r: Result<(), &str> = try {
let (html1, html2) = html.split_once("/ttt.js").ok_or("")?;
content.push(html1)?;
content.push("/ttt.js?team=")?;
content.push(team)?;
content.push(html2)?;
};
unwrap(r).await;
(HttpResCode::Ok, "html", Some(Content(content)))
} else if path == "/ttt.js" {
let Some(team) = team else {
return (HttpResCode::BadRequest, "", None);
};
let mut content = Vec::new();
let r: Result<(), &str> = try {
content.push("const team = ")?;
content.push(team)?;
content.push(";\n")?;
content.push(include_str!("ttt.js"))?;
};
unwrap(r).await;
(HttpResCode::Ok, "javascript", Some(Content(content)))
} else {
(HttpResCode::NotFound, "", None)
}
}
}
}
}

View File

@ -27,18 +27,7 @@
<body>
<h1>TicTacToe</h1>
<h3 id="team"></h3>
<!-- <div class="cell" style="background-color:"></div> -->
<div id="grid">
<div class="cell" id="cell0"></div>
<div class="cell" id="cell1"></div>
<div class="cell" id="cell2"></div>
<div class="cell" id="cell3"></div>
<div class="cell" id="cell4"></div>
<div class="cell" id="cell5"></div>
<div class="cell" id="cell6"></div>
<div class="cell" id="cell7"></div>
<div class="cell" id="cell8"></div>
<div class="cell" id="cell9"></div>
</div>
<h3 id="winner"></h3>
<div id="grid"></div>
</body>
</html>

View File

@ -1,12 +1,30 @@
const team = 0;
const teamName = "blue";
const color = "dodgerblue";
const otherColor = "firebrick";
//const team = 0;
if (team != 0 && team != 1) {
throw "team is not 0 or 1! team=" + team;
}
const teams = [
{
name: "blue",
color: "dodgerblue",
port: "8080",
},
{
name: "red",
color: "firebrick",
port: "8081",
},
];
document.getElementById("team").innerHTML =
'Team : <span style="color:' + color + '">' + teamName + "</span>";
'Team : <span style="color:' +
teams[team].color +
'">' +
teams[team].name +
"</span>";
const ws = new WebSocket("ws://192.254.0.2:8080/" + teamName);
const ws = new WebSocket(
"ws://192.254.0.2:" + teams[team].port + "/" + teams[team].name,
);
ws.onmessage = (event) => {
console.log(event.data);
@ -14,12 +32,7 @@ ws.onmessage = (event) => {
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 owner = msg.board[i];
let tagName;
if (msg.turn == team && owner === null) {
@ -48,5 +61,19 @@ ws.onmessage = (event) => {
cells.push(cell);
}
document.getElementById("grid").replaceChildren(...cells);
if (msg.turn == null) {
if (msg.winner == null) {
document.getElementById("winner").innerHTML = "Draw!";
} else {
document.getElementById("winner").innerHTML =
'Winner : <span style="color:' +
teams[msg.winner].color +
'">' +
teams[msg.winner].name +
"</span>";
}
} else {
document.getElementById("winner").innerHTML = "";
}
}
};

View File

@ -1,10 +1,11 @@
use core::fmt::Write;
use core::str::from_utf8_unchecked;
use core::{ops::Not, sync::atomic::Ordering};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Instant, Timer};
use heapless::{String, Vec};
use log::info;
use pico_website::unwrap;
use portable_atomic::{AtomicBool, AtomicU32};
use log::{info, warn};
use pico_website::{unwrap, unwrap_opt};
use serde::Serialize;
use crate::apps::Content;
@ -13,61 +14,112 @@ 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 / 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);
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
struct Game {
board: [Option<Team>; 9],
turn: Option<Team>,
winner: Option<Team>,
}
impl Game {
const fn default() -> Self {
Game {
board: [None; 9],
turn: Some(Team::Zero),
winner: None,
}
}
fn check_end(&mut self) -> bool {
for [a, b, c] in [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
] {
if let Some(t) = self.board[a] {
if self.board[b] == Some(t) && self.board[c] == Some(t) {
self.winner = Some(t);
self.turn = None;
return true;
}
}
}
if self.board.iter().all(|c| c.is_some()) {
self.winner = None;
self.turn = None;
return true;
}
false
}
}
static GAME: Mutex<ThreadModeRawMutex, Game> = Mutex::new(Game::default());
// {"board"=[null,null,null,null,null,null,null,null,null],"turn"=null,"winner":null}
pub struct TttApp {
team: Team,
last_board: u32,
end: Option<(Instant, Option<Team>)>,
last_game: Game,
/// Only one socket manages the end, this can be None even when it's the end
end: Option<Instant>,
json_buf: [u8; 128],
}
impl TttApp {
pub fn new(team: Team) -> Self {
Self {
team,
last_board: 0,
last_game: Game {
board: [None; 9],
turn: None,
winner: None,
},
end: None,
json_buf: [0; 128],
}
}
pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) {
if let Some((_, t)) = self.end {
return (true, t);
}
for (t, m) in [(Team::Zero, 0), (Team::One, 9)] {
for w in [
0b111000000,
0b000111000,
0b000000111,
0b100100100,
0b010010010,
0b001001001,
0b100010001,
0b001010100,
] {
if board & (w << m) == (w << m) {
return (true, Some(t));
}
}
}
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() {
self.end = None;
BOARD.store(0, Ordering::Release);
*board = 0;
}
} else {
if let (true, t) = self.is_ended(*board) {
self.end = Some((Instant::now(), t));
}
}
}
// pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) {
// if let Some((_, t)) = self.end {
// return (true, t);
// }
// for (t, m) in [(Team::Zero, 0), (Team::One, 9)] {
// for w in [
// 0b111000000,
// 0b000111000,
// 0b000000111,
// 0b100100100,
// 0b010010010,
// 0b001001001,
// 0b100010001,
// 0b001010100,
// ] {
// if board & (w << m) == (w << m) {
// return (true, Some(t));
// }
// }
// }
// 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() {
// self.end = None;
// // BOARD.store(0, Ordering::Release);
// *board = 0;
// }
// } else {
// if let (true, t) = self.is_ended(*board) {
// self.end = Some((Instant::now(), t));
// }
// }
// }
}
impl App for TttApp {
@ -97,38 +149,73 @@ impl App for TttApp {
_path: &str,
mut ws: Ws<'a, BUF_SIZE, RES_HEAD_BUF_SIZE>,
) {
Timer::after_millis(500).await;
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?;
Timer::after_millis(1).await;
let Ok(mut game) = GAME.try_lock() else {
info!("locked");
continue;
};
// match GAME.try_lock() ;
if self.last_game != *game {
// let json = unwrap(serde_json_core::to_string::<Game, 128>(&game)).await;
let n = unwrap(serde_json_core::to_slice(&(*game), &mut self.json_buf)).await;
let json =
unsafe { from_utf8_unchecked(&unwrap_opt(self.json_buf.get(..n)).await) };
info!("{:?}", json);
ws.send(WsMsg::Text(json)).await?;
self.last_game = game.clone();
}
if ws.last_msg.elapsed() >= Duration::from_secs(5) {
ws.send(WsMsg::Ping(&[])).await?;
info!("ping");
}
if self.end.map(|e| e.elapsed()).unwrap_or_default() > Duration::from_secs(5) {
self.end = None;
*game = Game {
turn: Some(!game.winner.unwrap_or_default()),
..Game::default()
};
}
while let Some(r) = ws.rcv().await? {
info!("{:?}", r);
if let WsMsg::Bytes([c]) = r {
let c = *c as usize;
info!("c={}", c);
if c >= game.board.len() {
warn!("Cell played is too big!");
return;
}
if game.board[c].is_some() {
warn!("Cell is already taken!");
return;
}
if game.turn == Some(self.team) {
game.board[c] = Some(self.team);
game.turn = Some(!self.team);
if game.check_end() {
self.end = Some(Instant::now());
}
} else {
warn!("It's not your turn!");
return;
}
info!("{:#?}", game);
}
}
Timer::after_secs(1).await;
// Timer::after_secs(1).await;
}
};
info!("{:?}", r);
warn!("error: {:?}", r);
Timer::after_micros(100).await;
}
}
#[derive(Debug, Serialize)]
struct ServerMsg {
board: u32,
turn: Option<Team>,
winner: Option<Team>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Team {
#[default]
Zero = 0,
One = 1,
}

View File

@ -11,7 +11,7 @@ use embassy_net::{
use embassy_time::Timer;
use heapless::Vec;
use log::{info, warn};
use pico_website::unwrap;
use pico_website::{unwrap, unwrap_opt};
#[embassy_executor::task(pool_size = 1)]
pub async fn dhcp_server(stack: Stack<'static>) {
@ -38,7 +38,8 @@ pub async fn dhcp_server(stack: Stack<'static>) {
loop {
let (n, _) = unwrap(socket.recv_from(&mut buf).await).await;
let msg = unwrap(dhcpv4::Message::new(&buf[..n])).await;
let msg = unwrap_opt(buf.get(..n)).await;
let msg = unwrap(dhcpv4::Message::new(&msg)).await;
let msg_type = unwrap(v4_options!(msg; MessageType required)).await;
let mut rapid_commit = false;

View File

@ -5,7 +5,7 @@ use embassy_net::{
};
use embassy_time::Timer;
use log::{info, warn};
use pico_website::unwrap;
use pico_website::{unwrap, unwrap_opt};
#[embassy_executor::task(pool_size = 1)]
pub async fn dns_server(stack: Stack<'static>) {
@ -30,7 +30,8 @@ pub async fn dns_server(stack: Stack<'static>) {
Timer::after_secs(0).await;
let (n, meta) = unwrap(socket.recv_from(&mut buf).await).await;
let msg = match dnsparse::Message::parse(&mut buf[..n]) {
let msg = unwrap_opt(buf.get_mut(..n)).await;
let msg = match dnsparse::Message::parse(msg) {
Ok(msg) => msg,
Err(e) => {
warn!("Dns: Error while parsing DNS message : {:#?}", e);

View File

@ -1,6 +1,6 @@
#![no_std]
use core::fmt::Debug;
use core::{fmt::Debug, panic::PanicInfo};
use embassy_time::Timer;
use log::info;
@ -13,3 +13,20 @@ pub async fn unwrap<T, E: Debug>(res: Result<T, E>) -> T {
},
}
}
pub async fn unwrap_opt<T>(opt: Option<T>) -> T {
unwrap(opt.ok_or(())).await
}
pub async fn assert(condition: bool) {
if !condition {
let err: Result<(), &str> = Err("assert failed");
unwrap(err).await;
}
}
// Doesn't work
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
info!("PANIC: {}", info);
loop {}
}

View File

@ -1,6 +1,12 @@
#![no_std]
#![no_main]
#![allow(async_fn_in_trait)]
#![deny(
// clippy::unwrap_used,
// clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
#![feature(impl_trait_in_assoc_type)]
#![feature(slice_split_once)]
#![feature(try_blocks)]
@ -10,6 +16,8 @@
use core::net::Ipv4Addr;
use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi};
use defmt::warn;
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_net::{Config, StackResources};
use embassy_rp::bind_interrupts;
@ -23,7 +31,6 @@ use log::info;
use pico_website::unwrap;
use rand_core::RngCore;
use static_cell::StaticCell;
use {defmt_rtt as _, panic_probe as _};
#[cfg(feature = "dhcp")]
mod dhcp;
@ -175,6 +182,12 @@ async fn main(spawner: Spawner) {
.address
)
}
// embassy_time::Timer::after_secs(5).await;
// info!("test0");
// embassy_time::Timer::after_secs(3).await;
// panic!("test");
#[cfg(feature = "dhcp")]
unwrap(spawner.spawn(dhcp::dhcp_server(stack))).await;

View File

@ -6,6 +6,7 @@ use embassy_time::{Duration, Timer};
use embedded_io_async::Write as _;
use heapless::{String, Vec};
use log::{info, warn};
use pico_website::{unwrap, unwrap_opt};
use sha1::{Digest, Sha1};
use crate::apps::Content;
@ -71,7 +72,8 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
}
};
head_buf.clear();
let (headers, content) = match from_utf8(&buf[..n]) {
let msg = unwrap_opt(buf.get(..n)).await;
let (headers, content) = match from_utf8(msg) {
Ok(b) => match b.split_once("\r\n\r\n") {
Some(t) => t,
None => (b, ""),
@ -82,7 +84,8 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
}
};
info!("\n{:?}\n", headers);
// info!("\n{:?}\n", headers);
// Timer::after_micros(100).await;
let mut hl = headers.lines();
let (request_type, path) = match hl.next() {
@ -144,12 +147,13 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
host,
path,
);
Timer::after_secs(0).await;
Timer::after_micros(100).await;
head_buf.clear();
let res_content: Result<Option<Content>, core::fmt::Error> = try {
if ws_handshake {
if !app.accept_ws(path) {
warn!("No ws there!");
write!(
&mut head_buf,
"{}\r\n\r\n",
@ -206,7 +210,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
Content-Length: {}\r\n",
res_type,
c.len()
)?
)?;
}
write!(&mut head_buf, "\r\n\r\n")?;
res_content
@ -221,7 +225,8 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
}
};
info!("\n{}\n", from_utf8(&head_buf).unwrap());
info!("\n{}\n", unwrap(from_utf8(&head_buf)).await);
Timer::after_micros(1000).await;
let w: Result<(), embassy_net::tcp::Error> = try {
socket.write_all(&head_buf).await?;
@ -229,7 +234,6 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
for s in c.0.iter() {
socket.write_all(s.as_bytes()).await?;
}
} else {
}
};
@ -239,11 +243,13 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
};
if ws_handshake {
ws_path = Some(String::from_str(path).unwrap());
ws_path = Some(unwrap(String::from_str(path)).await);
break;
}
}
if let Some(path) = ws_path {
info!("handle ws");
Timer::after_micros(200).await;
app.handle_ws(
&path,
Ws::new(&mut socket, &mut buf, &mut head_buf, app.socket_name()),
@ -291,11 +297,12 @@ impl Into<&str> for HttpResCode {
async fn compute_ws_accept(key: &str) -> Result<String<28>, EncodeSliceError> {
let mut res = Vec::<u8, 28>::new();
Timer::after_micros(10).await;
res.extend_from_slice(&[0; 28]).unwrap();
let mut hasher = Sha1::new();
hasher.update(key.as_bytes());
hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
let hash = hasher.finalize();
BASE64_STANDARD.encode_slice(hash, &mut res)?;
Ok(String::from_utf8(res).unwrap())
Ok(unwrap(String::from_utf8(res)).await)
}

View File

@ -5,6 +5,7 @@ use embassy_time::{Instant, Timer};
use embedded_io_async::{ErrorType, ReadReady, Write};
use heapless::Vec;
use log::{info, warn};
use pico_website::{assert, unwrap, unwrap_opt};
#[derive(Clone, Copy, Debug)]
pub enum WsMsg<'a> {
@ -38,7 +39,6 @@ impl WsMsg<'_> {
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> {
@ -48,15 +48,17 @@ struct WsTx<'a, const HEAD_BUF_SIZE: usize> {
impl<'a, const HEAD_BUF_SIZE: usize> WsTx<'a, HEAD_BUF_SIZE> {
pub async fn send<'m>(&mut self, msg: WsMsg<'m>) -> Result<(), ()> {
self.head_buf.clear();
self.head_buf.push(0b1000_0000 | msg.code()).unwrap();
unwrap(self.head_buf.push(0b1000_0000 | msg.code())).await;
if msg.len() < 126 {
self.head_buf.push(msg.len() as u8).unwrap();
unwrap(self.head_buf.push(msg.len() as u8)).await;
} else {
self.head_buf.push(0b0111_1110).unwrap();
unwrap(self.head_buf.push(0b0111_1110)).await;
unwrap(
self.head_buf
.extend_from_slice(&(msg.len() as u16).to_le_bytes())
.unwrap();
self.head_buf.extend_from_slice(msg.as_bytes()).unwrap();
.extend_from_slice(&(msg.len() as u16).to_le_bytes()),
)
.await;
// self.head_buf.extend_from_slice(msg.as_bytes()).unwrap();
}
let w: Result<(), <TcpSocket<'_> as ErrorType>::Error> = try {
self.socket.write_all(&self.head_buf).await?;
@ -72,6 +74,7 @@ impl<'a, const HEAD_BUF_SIZE: usize> WsTx<'a, HEAD_BUF_SIZE> {
pub struct Ws<'a, const BUF_SIZE: usize = 1024, const RES_HEAD_BUF_SIZE: usize = 256> {
rx: WsRx<'a, BUF_SIZE>,
tx: WsTx<'a, RES_HEAD_BUF_SIZE>,
pub last_msg: Instant,
name: &'a str,
}
impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEAD_BUF_SIZE> {
@ -86,13 +89,13 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
rx: WsRx {
socket: rx,
buf,
last_msg: Instant::MIN,
msg_in_buf: None,
},
tx: WsTx {
socket: tx,
head_buf,
},
last_msg: Instant::MIN,
name,
}
}
@ -100,8 +103,9 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
pub async fn rcv(&mut self) -> Result<Option<WsMsg>, ()> {
let n = match self.rx.msg_in_buf.take() {
Some(n) => {
assert(n.0 + n.1 <= self.rx.buf.len()).await;
self.rx.buf.copy_within(n.0..n.0 + n.1, 0);
if self.rx.socket.read_ready().unwrap() {
if unwrap(self.rx.socket.read_ready()).await {
let n_rcv = match self.rx.socket.read(&mut self.rx.buf[n.1..]).await {
Ok(0) => {
info!("read EOF");
@ -119,7 +123,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
}
}
None => {
if self.rx.socket.read_ready().unwrap() {
if unwrap(self.rx.socket.read_ready()).await {
match self.rx.socket.read(self.rx.buf).await {
Ok(0) => {
info!("read EOF");
@ -177,7 +181,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
.iter_mut()
.enumerate()
{
*x ^= mask_key[i & 0xff];
*x ^= unwrap_opt(mask_key.get(i & 0xff)).await;
}
if n_after_length + 4 + (length as usize) < n {
self.rx.msg_in_buf = Some((
@ -199,7 +203,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
}
&self.rx.buf[n_after_length..n_after_length + length as usize]
};
self.rx.last_msg = Instant::now();
self.last_msg = Instant::now();
match self.rx.buf[0] & 0b0000_1111 {
// Text message
1 => {
@ -222,6 +226,8 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
}
}
pub async fn send(&mut self, msg: WsMsg<'_>) -> Result<(), ()> {
self.tx.send(msg).await
self.tx.send(msg).await?;
self.last_msg = Instant::now();
Ok(())
}
}