Compare commits

...

14 Commits
dhcp ... main

Author SHA1 Message Date
Arkitu
6c57c3aaaf change refresh rate of chat to 1s 2025-05-07 19:45:41 +02:00
Arkitu
bfdf18c1da working utf8 messages 2025-05-06 22:22:00 +02:00
Arkitu
21e6a50e36 save 2025-05-05 22:08:48 +02:00
Arkitu
4521697f48 multiple chat clients + handle showing long discussion to new client 2025-05-04 13:37:31 +02:00
Arkitu
f523ec812c chat works 2025-05-04 11:59:10 +02:00
Arkitu
82c86bba16 working send + view messages 2025-05-03 23:11:55 +02:00
Arkitu
c629ae5ce3 save 2025-05-02 16:18:52 +02:00
Arkitu
1c0108cefa messages in mutex 2025-05-02 14:08:14 +02:00
Arkitu
e6bc8561d4 save 2025-05-02 00:37:11 +02:00
Arkitu
b90c978a38 save 2025-05-01 21:30:55 +02:00
Arkitu
f085ceba1e refactor + index page 2025-04-29 23:07:26 +02:00
Arkitu
6bc4d823b7 dns works 2025-04-28 21:10:35 +02:00
Arkitu
937aafb099 change wifi name + handle client bad request 2025-04-27 15:21:33 +02:00
Arkitu
c0402e0dca opti htmx in release mode 2025-04-26 22:51:27 +02:00
13 changed files with 670 additions and 132 deletions

22
Cargo.lock generated
View File

@ -348,6 +348,12 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "dnsparse"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18b5892f4beae62ac9681eb96df926ebcebca009e6f2e23a216acf7d3f5c5a97"
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.11" version = "0.2.11"
@ -1057,6 +1063,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "petgraph" name = "petgraph"
version = "0.7.1" version = "0.7.1"
@ -1093,17 +1105,21 @@ dependencies = [
"defmt", "defmt",
"defmt-rtt", "defmt-rtt",
"dhcparse", "dhcparse",
"dnsparse",
"embassy-executor", "embassy-executor",
"embassy-net", "embassy-net",
"embassy-rp", "embassy-rp",
"embassy-sync",
"embassy-time", "embassy-time",
"embassy-usb-logger", "embassy-usb-logger",
"embedded-io-async", "embedded-io-async",
"heapless", "heapless",
"log", "log",
"panic-probe", "panic-probe",
"percent-encoding",
"portable-atomic", "portable-atomic",
"rand_core", "rand_core",
"ringbuffer",
"serde", "serde",
"serde-json-core", "serde-json-core",
"static_cell", "static_cell",
@ -1297,6 +1313,12 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "ringbuffer"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53"
[[package]] [[package]]
name = "rp-pac" name = "rp-pac"
version = "7.0.0" version = "7.0.0"

View File

@ -9,7 +9,8 @@ wifi-connect = [
"dep:serde", "dep:serde",
] # you need to add a wifi.conf file for this to work ] # you need to add a wifi.conf file for this to work
dhcp = ["dep:dhcparse"] dhcp = ["dep:dhcparse"]
default = ["dhcp"] dns = ["dep:dnsparse"]
default = ["dhcp", "dns"]
[dependencies] [dependencies]
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [ embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
@ -35,6 +36,7 @@ embassy-net = { git = "https://github.com/embassy-rs/embassy", features = [
"udp", "udp",
"dhcpv4", "dhcpv4",
] } ] }
embassy-sync = { git = "https://github.com/embassy-rs/embassy" }
cyw43-pio = { git = "https://github.com/embassy-rs/embassy" } cyw43-pio = { git = "https://github.com/embassy-rs/embassy" }
cyw43 = { git = "https://github.com/embassy-rs/embassy" } cyw43 = { git = "https://github.com/embassy-rs/embassy" }
@ -54,4 +56,7 @@ serde-json-core = { version = "*", optional = true }
serde = { version = "*", optional = true, default-features = false, features = [ serde = { version = "*", optional = true, default-features = false, features = [
"derive", "derive",
] } ] }
dhcparse = { version = "*", default-features = false, optional = true } dhcparse = { version = "*", default-features = false, optional = true }
dnsparse = { version = "*", optional = true }
ringbuffer = { version = "*", default-features = false }
percent-encoding = { version = "*", default-features = false }

41
src/apps/chat.html Normal file
View File

@ -0,0 +1,41 @@
<!doctype html>
<head>
<script src="./htmx.js"></script>
<style type="text/css">
body {
/* #grid {
.cell {
border: 1px dotted black;
padding: 33%;
}
display: grid;
border: 1px solid black;
grid-template-rows: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
} */
}
</style>
</head>
<html>
<body>
<h1>Chat</h1>
<form id="login-page">
Enter your name :
<input
name="username"
type="text"
autocomplete="username"
minlength="3"
maxlength="16"
required
/>
<button
hx-post="/chat/connect"
hx-target="#login-page"
hx-swap="outerHTML"
>
Connect
</button>
</form>
</body>
</html>

276
src/apps/chat.rs Normal file
View File

@ -0,0 +1,276 @@
use core::fmt::Write;
use dhcparse::dhcpv4::MAX_MESSAGE_SIZE;
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
use heapless::{String, Vec};
use log::{info, warn};
use percent_encoding::percent_decode_str;
use pico_website::unwrap;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use crate::socket::{HttpRequestType, HttpResCode};
use super::App;
const MEMORY_SIZE: usize = 16;
const USERNAME_MIN_SIZE: usize = 3;
const USERNAME_SIZE: usize = 16;
const MSG_SIZE: usize = 128;
static MESSAGES: Mutex<ThreadModeRawMutex, Messages> = Mutex::new(Messages::new());
pub struct ChatApp {
res_buf: Vec<u8, 1100>,
}
impl ChatApp {
pub fn new() -> Self {
Self {
res_buf: Vec::new(),
}
}
}
impl App for ChatApp {
fn socket_name(&self) -> &'static str {
"chat"
}
async fn handle_request<'a>(
&'a mut self,
path: &str,
req_type: HttpRequestType,
content: &str,
) -> (HttpResCode, &'static str, &'a [u8]) {
match (req_type, path) {
(HttpRequestType::Get, "/" | "/index" | "/index.html" | "/chat" | "/chat.html") => {
(HttpResCode::Ok, "html", include_bytes!("./chat.html"))
}
(_, path) => {
let (path, args) = path.split_once('?').unwrap_or((path, ""));
let mut load = None;
let mut username = None;
let mut msg_content = None;
let mut poll = false;
for arg in args.split('&').chain(content.split('&')) {
match arg.split_once('=') {
Some(("load", n)) => {
let n: u16 = match n.parse() {
Ok(v) => v,
Err(_) => return (HttpResCode::BadRequest, "", &[]),
};
if n > 0 {
load = Some(n);
}
}
Some(("username", u)) => {
let mut name = String::<USERNAME_SIZE>::new();
for c in percent_decode_str(u) {
if let Err(_) = name.push(c as char) {
return (HttpResCode::BadRequest, "", &[]);
}
}
if u.len() < USERNAME_MIN_SIZE {
return (HttpResCode::BadRequest, "", &[]);
}
username = Some(name);
}
Some(("msg", m)) => {
let mut msg = Vec::<u8, MSG_SIZE>::new();
let mut i = 0;
while i < m.len() {
let c = if m.as_bytes()[i] == b'%' {
let c = match m
.get(i + 1..=i + 2)
.map(|s| u8::from_str_radix(s, 16))
{
Some(Ok(c)) => c,
_ => {
warn!("Invalid percent encoding of msg argument");
return (HttpResCode::BadRequest, "", &[]);
}
};
i += 2;
c
} else {
m.as_bytes()[i]
};
if let Err(_) = msg.push(c) {
return (HttpResCode::BadRequest, "", &[]);
}
i += 1;
}
msg_content = Some(match String::from_utf8(msg) {
Ok(msg) => msg,
Err(_) => {
warn!("Invalid utf8 msg argument");
return (HttpResCode::BadRequest, "", &[]);
}
});
}
Some(("poll", "true")) => poll = true,
_ => {}
}
}
info!(
"load:{:?} | username:{:?} | msg:{:?}",
load, username, msg_content
);
if path == "/chat/connect" && username.is_some() {
self.res_buf.clear();
unwrap(write!(
&mut self.res_buf,
"<input id=\"username\" style=\"display: none;\" name=\"username\" value=\"{}\">
<div id=\"messages\" >\
<div \
class=\"message\" \
hx-get=\"/chat/message/0?load={}\" \
hx-target=\"this\" \
hx-swap=\"outerHTML\" \
hx-trigger=\"load\" \
></div>\
</div>\
<form id=\"send-message\" \
hx-post=\"/chat/send\" \
hx-include=\"#username\" \
hx-target=\"this\" \
hx-swap=\"innerHTML\"\
>\
<input \
id=\"msg\" \
name=\"msg\" \
maxlength=\"{}\"\
>\
<button>Send</button>
</form>",
username.unwrap(),
MEMORY_SIZE,
MAX_MESSAGE_SIZE
))
.await;
return (HttpResCode::Ok, "html", &self.res_buf);
} else if path == "/chat/send" && username.is_some() && msg_content.is_some() {
let mut msgs = MESSAGES.lock().await;
msgs.push(Message {
author: username.unwrap(),
content: msg_content.unwrap(),
});
self.res_buf.clear();
unwrap(write!(
&mut self.res_buf,
"<input \
id=\"msg\" \
name=\"msg\" \
maxlength=\"{}\"\
>\
<button>Send</button>",
MAX_MESSAGE_SIZE
))
.await;
return (HttpResCode::Ok, "html", &self.res_buf);
} else if path.starts_with("/chat/message/") && path.len() > 14 {
let msg_id: u16 = match path[14..].parse() {
Ok(n) => n,
Err(_) => return (HttpResCode::BadRequest, "", &[]),
};
let msgs = MESSAGES.lock().await;
if msg_id > msgs.next {
return (HttpResCode::BadRequest, "", &[]);
}
self.res_buf.clear();
unwrap(write!(&mut self.res_buf, "<div class=\"message\"")).await;
if msg_id == msgs.next {
if poll {
return (HttpResCode::NoContent, "", &[]);
}
unwrap(write!(
&mut self.res_buf,
" style=\"display: none;\" \
hx-get=\"/chat/message/{}?load={}&poll=true\" \
hx-target=\"this\" \
hx-swap=\"outerHTML\" \
hx-trigger=\"every 1s\"",
msg_id,
load.unwrap_or(0)
))
.await;
} else {
if let Some(n) = load {
unwrap(write!(
&mut self.res_buf,
" hx-get=\"/chat/message/{}?load={}\" \
hx-target=\"this\" \
hx-swap=\"afterend\" \
hx-trigger=\"load\"",
msg_id + 1,
n - 1,
))
.await;
}
match msgs.get_abs(msg_id) {
Some(msg) => {
unwrap(write!(
&mut self.res_buf,
"><b>{}</b>: {}</div>",
msg.author, msg.content
))
.await
}
None => {
if load.is_some() {
if (msg_id as isize)
== (msgs.next as isize - MEMORY_SIZE as isize - 1)
{
unwrap(write!(
&mut self.res_buf,
"><em>Older messages forgotten</em></div>"
))
.await;
} else {
unwrap(write!(
&mut self.res_buf,
" style=\"display: none;\"></div>"
))
.await;
}
} else {
return (HttpResCode::NoContent, "", &[]);
}
}
};
};
return (HttpResCode::Ok, "html", &self.res_buf);
} else {
(HttpResCode::NotFound, "", &[])
}
}
}
}
}
struct Message {
author: String<USERNAME_SIZE>,
content: String<MSG_SIZE>,
}
struct Messages {
inner: ConstGenericRingBuffer<Message, MEMORY_SIZE>,
next: u16,
}
impl Messages {
const fn new() -> Self {
Self {
inner: ConstGenericRingBuffer::new(),
next: 0,
}
}
fn get_abs(&self, id: u16) -> Option<&Message> {
if (id as isize) < (self.next as isize - MEMORY_SIZE as isize) {
return None;
}
self.inner.get_signed((id as isize) - (self.next as isize))
}
fn push(&mut self, msg: Message) {
info!("{}: {}", msg.author, msg.content);
self.inner.push(msg);
self.next += 1;
}
}

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>

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

@ -0,0 +1,23 @@
use crate::socket::{HttpRequestType, 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,
_req_type: HttpRequestType,
_content: &str,
) -> (HttpResCode, &'static str, &'a [u8]) {
match path {
"/" | "/index" | "/index.html" => {
(HttpResCode::Ok, "html", include_bytes!("./index.html"))
}
_ => (HttpResCode::NotFound, "", &[]),
}
}
}

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

@ -0,0 +1,15 @@
use crate::socket::{HttpRequestType, HttpResCode};
pub mod chat;
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,
req_type: HttpRequestType,
content: &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::{HttpRequestType, 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, 2048>,
/// 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,21 +42,21 @@ 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)
} }
pub fn update_end_state(&mut self, board: &mut u32) { pub fn update_end_state(&mut self, board: &mut u32) {
if let Some((i, _)) = self.end { 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; self.end = None;
BOARD.store(0, Ordering::Release); BOARD.store(0, Ordering::Release);
*board = 0; *board = 0;
@ -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,33 +83,35 @@ 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;
for c in 0..=8 { for c in 0..=8 {
let picked_by = if board & (1<<c) != 0 { let picked_by = if board & (1 << c) != 0 {
Some(Team::Zero) Some(Team::Zero)
} else if board & (1<<(9 + c)) != 0 { } else if board & (1 << (9 + c)) != 0 {
Some(Team::One) Some(Team::One)
} else { } else {
None None
@ -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 => {
unwrap(write!( if self.team == turn.into() && self.end.is_none() {
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,45 +148,68 @@ impl GameClient {
} }
&self.res_buf &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 impl App for TttApp {
if path.starts_with("/ttt/cell") && self.team == turn.into() { fn socket_name(&self) -> &'static str {
let clicked_c: Cell = match TryInto::<Cell>::try_into( self.team.name()
unwrap(path.chars().nth(9).ok_or("no 9th char")).await, }
) { async fn handle_request<'a>(
Ok(c) => c, &'a mut self,
Err(_) => return (HttpResCode::NotFound, "", &[]), path: &str,
}; _req_type: HttpRequestType,
if board _content: &str,
& ( ) -> (HttpResCode, &'static str, &'a [u8]) {
(1<<(clicked_c as u32)) + (1<<(9 + clicked_c as u32)) match path {
) "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => {
!= 0 (HttpResCode::Ok, "html", include_bytes!("./ttt.html"))
{ }
return (HttpResCode::Forbidden, "", &[]); "/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::<Cell>::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 +228,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 +237,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 +245,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",
} }
} }
} }
@ -264,4 +301,4 @@ impl TryFrom<u8> for Cell {
_ => return Err(()), _ => return Err(()),
}) })
} }
} }

View File

@ -149,15 +149,15 @@ async fn write_dhcp_opts<const N: usize>(buf: &mut Vec<u8, N>, op_codes: &[u8])
1 => (4, &[255, 255, 255, 0]), // DhcpOption::SubnetMask(&dhcpv4::Addr([255, 255, 255, 0])), 1 => (4, &[255, 255, 255, 0]), // DhcpOption::SubnetMask(&dhcpv4::Addr([255, 255, 255, 0])),
2 => (4, &3600_i32.to_be_bytes()), // DhcpOption::TimeOffset(3600), 2 => (4, &3600_i32.to_be_bytes()), // DhcpOption::TimeOffset(3600),
3 => (4, &[192, 254, 0, 2]), // DhcpOption::Router(&[dhcpv4::Addr([192, 254, 0, 2])]), 3 => (4, &[192, 254, 0, 2]), // DhcpOption::Router(&[dhcpv4::Addr([192, 254, 0, 2])]),
6 => (4, &[0, 0, 0, 0]), // 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 => (11, b"LocalDomain"), // 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);

88
src/dns.rs Normal file
View File

@ -0,0 +1,88 @@
use dnsparse::{Answer, Header, HeaderKind, Message, OpCode, QueryClass, QueryKind, ResponseCode};
use embassy_net::{
Stack,
udp::{PacketMetadata, UdpSocket},
};
use embassy_time::Timer;
use log::{info, warn};
use pico_website::unwrap;
#[embassy_executor::task(pool_size = 1)]
pub async fn dns_server(stack: Stack<'static>) {
let mut rx_buffer = [0; 4096];
let mut tx_buffer = [0; 4096];
let mut rx_meta = [PacketMetadata::EMPTY; 16];
let mut tx_meta = [PacketMetadata::EMPTY; 16];
let mut buf = [0; 4096];
let mut res_buf = [0; 2048];
loop {
let mut socket = UdpSocket::new(
stack,
&mut rx_meta,
&mut rx_buffer,
&mut tx_meta,
&mut tx_buffer,
);
unwrap(socket.bind(53)).await;
info!("Starting DNS server");
loop {
let (n, meta) = unwrap(socket.recv_from(&mut buf).await).await;
let msg = match dnsparse::Message::parse(&mut buf[..n]) {
Ok(msg) => msg,
Err(e) => {
warn!("Dns: Error while parsing DNS message : {:#?}", e);
continue;
}
};
if msg.header().opcode() != OpCode::Query {
info!(
"Dns: Received unknown dns opcode ({:?}), ignoring",
msg.header().opcode()
);
Timer::after_micros(10).await;
continue;
}
let mut res = Message::builder(&mut res_buf)
.header(
Header::builder()
.id(msg.header().id())
.kind(HeaderKind::Response)
.recursion_available(false)
.recursion_desired(msg.header().recursion_desired())
.response_code(ResponseCode::NoError)
.build(),
)
.build();
for q in msg.questions() {
match q.kind() {
QueryKind::A => {
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: 600,
rdata: &[192, 254, 0, 2],
});
info!("Dns: Giving {}", q.name());
} else {
info!("Dns: Unknown uri, ignoring ({})", q.name());
}
}
_ => {
continue;
}
};
}
if let Err(e) = socket.send_to(res.as_bytes(), meta).await {
warn!("Dns: Error while sending dns response : {:?}", e);
break;
}
}
}
}

View File

@ -22,7 +22,10 @@ use {defmt_rtt as _, panic_probe as _};
#[cfg(feature = "dhcp")] #[cfg(feature = "dhcp")]
mod dhcp; mod dhcp;
mod game; #[cfg(feature = "dns")]
mod dns;
mod apps;
mod socket; mod socket;
bind_interrupts!(struct Irqs { bind_interrupts!(struct Irqs {
@ -112,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<20>> = StaticCell::new();
let (stack, runner) = embassy_net::new( let (stack, runner) = embassy_net::new(
net_device, net_device,
config, config,
@ -123,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("cyw43", 5).await; control.start_ap_open("pico", 5).await;
control.start_ap_wpa2("cyw43", "password", 5).await; // control.start_ap_wpa2("pico", "password", 5).await;
#[cfg(feature = "wifi-connect")] #[cfg(feature = "wifi-connect")]
{ {
@ -163,8 +166,15 @@ async fn main(spawner: Spawner) {
#[cfg(feature = "dhcp")] #[cfg(feature = "dhcp")]
unwrap(spawner.spawn(dhcp::dhcp_server(stack))).await; unwrap(spawner.spawn(dhcp::dhcp_server(stack))).await;
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::Zero, 80))).await; #[cfg(feature = "dns")]
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::One, 81))).await; unwrap(spawner.spawn(dns::dns_server(stack))).await;
unwrap(spawner.spawn(socket::index_listen_task(stack, 80))).await;
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::Zero, 8080))).await;
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
for _ in 0..4 {
unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await;
}
} }
#[cfg(feature = "wifi-connect")] #[cfg(feature = "wifi-connect")]

View File

@ -1,41 +1,52 @@
use core::fmt::Write;
use core::str::from_utf8; use core::str::from_utf8;
use embassy_net::tcp::TcpSocket; use embassy_net::tcp::TcpSocket;
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use embedded_io_async::Write as _;
use heapless::Vec; use heapless::Vec;
use log::{info, warn}; use log::{info, warn};
use pico_website::unwrap;
use embedded_io_async::Write as _;
use core::fmt::Write;
use crate::game::{GameClient, Team}; use crate::apps::{App, chat, 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>, team: ttt::Team, port: u16) {
listen_task(stack, ttt::TttApp::new(team), 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
}
#[embassy_executor::task(pool_size = 4)]
pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, port: u16) {
listen_task(stack, chat::ChatApp::new(), 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;
// } // }
let mut rx_buffer = [0; 4096]; let mut rx_buffer = [0; 1024];
let mut tx_buffer = [0; 4096]; let mut tx_buffer = [0; 2048];
let mut buf = [0; 4096]; let mut buf = [0; 1024];
let mut res_head_buf = Vec::<u8, 4096>::new(); let mut res_head_buf = Vec::<u8, 128>::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()
); );
@ -48,36 +59,36 @@ 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;
} }
}; };
let mut headers: &[u8] = &buf[..n]; let (headers, content) = match from_utf8(&buf[..n]) {
let mut _content: &[u8] = &[]; Ok(b) => match b.split_once("\r\n\r\n") {
for i in 0..(n - 1) { Some(t) => t,
if &buf[i..i + 1] == b"\r\n" { None => (b, ""),
headers = &buf[0..i]; },
if i + 2 < n { Err(_) => {
_content = &buf[i + 2..n]; warn!("Non utf8 http request");
} break;
} }
} };
let mut headers = headers.split(|x| *x == b'\n'); // let mut headers = headers.split(|x| *x == b'\n');
let (request_type, path) = match headers.next() { let (request_type, path) = match headers.lines().next() {
None => { None => {
warn!("Empty request"); warn!("Empty request");
break; break;
} }
Some(l1) => { Some(l1) => {
let mut l1 = l1.split(|x| *x == b' '); let mut l1 = l1.split(' ');
( (
match l1.next() { match l1.next() {
Some(b"GET") => HttpRequestType::Get, Some("GET") => HttpRequestType::Get,
Some(b"POST") => HttpRequestType::Post, Some("POST") => HttpRequestType::Post,
Some(t) => { Some(t) => {
warn!("Unknown request type : {}", unwrap(from_utf8(t)).await); warn!("Unknown request type : {}", t);
break; break;
} }
None => { None => {
@ -86,7 +97,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
} }
}, },
match l1.next() { match l1.next() {
Some(path) => unwrap(from_utf8(path)).await, Some(path) => path,
None => { None => {
warn!("No path"); warn!("No path");
break; break;
@ -97,26 +108,23 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
}; };
info!( info!(
"Socket {:?}: {:?} request for {}", "Socket {}: {:?} request for {}",
team, app.socket_name(),
request_type, request_type,
path 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.min.js" => (
HttpResCode::Ok,
"javascript",
include_bytes!("../static/htmx.min.js"),
),
"/htmx.js" => ( "/htmx.js" => (
HttpResCode::Ok, HttpResCode::Ok,
"javascript", "javascript",
#[cfg(debug_assertions)]
include_bytes!("../static/htmx.js"), include_bytes!("../static/htmx.js"),
#[cfg(not(debug_assertions))]
include_bytes!("../static/htmx.min.js"),
), ),
p => game_client.handle_request(p).await p => app.handle_request(p, request_type, content).await,
}; };
res_head_buf.clear(); res_head_buf.clear();
@ -152,7 +160,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
enum HttpRequestType { pub enum HttpRequestType {
Get, Get,
Post, Post,
} }
@ -160,7 +168,7 @@ impl Into<&str> for HttpRequestType {
fn into(self) -> &'static str { fn into(self) -> &'static str {
match self { match self {
Self::Get => "GET", Self::Get => "GET",
Self::Post => "POST" Self::Post => "POST",
} }
} }
} }
@ -169,6 +177,7 @@ impl Into<&str> for HttpRequestType {
pub enum HttpResCode { pub enum HttpResCode {
Ok, Ok,
NoContent, NoContent,
BadRequest,
NotFound, NotFound,
Forbidden, Forbidden,
} }
@ -177,8 +186,9 @@ impl Into<&str> for HttpResCode {
match self { match self {
HttpResCode::Ok => "HTTP/1.1 200 OK", HttpResCode::Ok => "HTTP/1.1 200 OK",
HttpResCode::NoContent => "HTTP/1.1 204 NO CONTENT", HttpResCode::NoContent => "HTTP/1.1 204 NO CONTENT",
HttpResCode::BadRequest => "HTTP/1.1 400 BAD REQUEST",
HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND", HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND",
HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN", HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN",
} }
} }
} }