Compare commits

..

13 Commits
main ... dev

Author SHA1 Message Date
Arkitu
785671d8cd save 2025-08-28 14:35:25 +02:00
ea43024446 save 2025-08-27 14:40:16 +02:00
4cb3736e56 started changing socket.rs to remove some buffers (not finished) 2025-08-23 09:20:12 +02:00
Arkitu
47e8ba586c save 2025-08-20 14:07:31 +02:00
Arkitu
078734e8a1 send new users on chat + cleaning 2025-08-18 14:31:23 +02:00
Arkitu
f5fa4647f4 things + rework ws 2025-08-16 12:18:15 +02:00
Arkitu
9c8a7af782 save 2025-08-15 09:39:05 +02:00
2af8cacc76 save 2025-08-15 08:36:31 +02:00
fc6423ad44 clean + minify 2025-08-10 03:14:11 +02:00
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
3782686d4b a lot of things + ws can rcv multiple msgs at once 2025-08-01 02:35:25 +02:00
22 changed files with 1373 additions and 1017 deletions

530
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,48 +4,44 @@ version = "0.1.0"
edition = "2024"
[features]
json = ["dep:serde-json-core", "dep:serde"]
wifi-connect = [
"dep:serde-json-core",
"dep:serde",
] # you need to add a wifi.json file for this to work
dhcp = ["dep:dhcparse"]
dns = ["dep:dnsparse"]
chat = ["dep:ringbuffer"]
ttt = []
default = ["dhcp", "dns"]
chat = ["dep:ringbuf", "json"]
ttt = ["json"]
default = ["dhcp", "dns", "chat"]
[dependencies]
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt",
# git = "https://github.com/embassy-rs/embassy",
embassy-executor = { version = "*", features = [
"nightly",
"arch-cortex-m",
"executor-thread",
"executor-interrupt",
] }
embassy-rp = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt",
embassy-rp = { version = "*", features = [
"unstable-pac",
"rp2040",
"time-driver",
"critical-section-impl",
] }
embassy-time = { git = "https://github.com/embassy-rs/embassy" }
embassy-usb-logger = { git = "https://github.com/embassy-rs/embassy" }
embassy-net = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt",
embassy-time = { version = "*" }
embassy-usb-logger = { version = "*" }
embassy-net = { version = "*", features = [
"proto-ipv4",
"tcp",
"udp",
"dhcpv4",
] }
embassy-sync = { git = "https://github.com/embassy-rs/embassy" }
cyw43-pio = { git = "https://github.com/embassy-rs/embassy" }
cyw43 = { git = "https://github.com/embassy-rs/embassy" }
embassy-sync = { version = "*" }
cyw43-pio = { version = "*" }
cyw43 = { version = "*" }
embedded-io-async = "*"
defmt = "*"
defmt-rtt = "*"
panic-probe = "*"
cortex-m = { version = "*", features = ["inline-asm"] }
cortex-m-rt = "*"
static_cell = "*"
@ -60,7 +56,9 @@ serde = { version = "*", optional = true, default-features = false, features = [
] }
dhcparse = { version = "*", default-features = false, optional = true }
dnsparse = { version = "*", optional = true }
ringbuffer = { version = "*", default-features = false, optional = true }
ringbuf = { version = "*", default-features = false, features = [
"portable-atomic",
], optional = true }
percent-encoding = { version = "*", default-features = false }
sha1 = { version = "*", default-features = false }
base64 = { version = "*", default-features = false }
base64 = { version = "*", default-features = false }

View File

@ -32,5 +32,5 @@ fn main() {
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tlink-rp.x");
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
// println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}

View File

@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2025-03-18"

View File

@ -1,41 +1,30 @@
<!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>
<head>
<script src="/chat.js" defer></script>
<style>
#users_box {
float: right;
}
</style>
</head>
<body>
<h1>Chat</h1>
<form id="login-page">
Enter your name :
<div id="users_box">
<h3>Online Users :</h3>
<div id="users"></div>
</div>
<div id="msgs"></div>
<form id="send">
<input
name="username"
type="text"
autocomplete="username"
minlength="3"
maxlength="16"
id="sendcontent"
name="content"
autofocus
required
minlength="1"
maxlength="500"
/>
<button
hx-post="/chat/connect"
hx-target="#login-page"
hx-swap="outerHTML"
>
Connect
</button>
<input type="submit" value="Send" />
</form>
</body>
</html>

47
src/apps/chat.js Normal file
View File

@ -0,0 +1,47 @@
// const id = 0;
if (id === undefined) {
throw "id is undefined!";
}
const ws = new WebSocket("ws://192.254.0.2:" + (9000 + id));
ws.onmessage = (event) => {
console.log(event.data);
if (typeof event.data == "string") {
let msg = JSON.parse(event.data);
if (event.data[0] == "[") {
// Handle list of users
users = msg;
let usersElems = [];
for (u of users) {
if (typeof u == "string") {
let un = document.createElement("div");
un.innerText = u;
usersElems.push(un);
}
}
document.getElementById("users").replaceChildren(...usersElems);
} else {
// Handle message
let msgs = document.getElementById("msgs");
let elem = document.createElement("p");
elem["data-id"] = msg.id;
elem.innerHTML =
"<strong>" + users[msg.author] + " :</strong> " + msg.content;
for (c of msgs.children) {
if (c["data-id"] > msg.id) {
msgs.insertBefore(elem, c);
return;
}
}
msgs.appendChild(elem);
}
}
};
document.getElementById("send").onsubmit = (event) => {
event.preventDefault();
// console.log(event, document.getElementById("sendcontent").value);
let content = document.getElementById("sendcontent");
ws.send("send " + content.value);
content.value = "";
};

View File

@ -1,30 +1,176 @@
use core::fmt::Write;
use dhcparse::dhcpv4::MAX_MESSAGE_SIZE;
use core::sync::atomic::Ordering;
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
use heapless::{String, Vec};
use embassy_time::{Duration, Timer};
use heapless::String;
use log::{info, warn};
use percent_encoding::percent_decode_str;
use pico_website::unwrap;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use pico_website::unimplemented;
use portable_atomic::AtomicUsize;
use serde::Serialize;
use crate::socket::{HttpRequestType, HttpResCode};
use crate::{apps::App, socket::ws::WsMsg};
use super::App;
// Must be <= u8::MAX-1;
pub const USERS_LEN: u8 = 4;
const MEMORY_SIZE: usize = 16;
const USERNAME_MIN_SIZE: usize = 3;
const USERNAME_SIZE: usize = 16;
const MSG_SIZE: usize = 128;
const MSG_MAX_SIZE: usize = 500;
#[derive(Debug, Serialize)]
struct Msg<'a> {
id: usize,
author: u8,
content: &'a str,
}
static MESSAGES: Mutex<ThreadModeRawMutex, Messages> = Mutex::new(Messages::new());
const MSGS_SIZE: usize = 30;
#[derive(Debug)]
struct Msgs {
/// * Memory layout with sizes in bytes : ...|content: len|len: 2|author+1: 1|...
/// * `author=0` means theres no message, it's just padding and should be skipped.
/// * No message is splitted
inner: [u8; MSGS_SIZE],
/// next byte index
head: usize,
next_msg: usize,
}
impl Msgs {
const fn default() -> Self {
Self {
inner: [0; _],
head: 0,
next_msg: 0,
}
}
fn push(&mut self, author: u8, content: &str) {
if self.head + content.len() + 3 >= MSGS_SIZE {
self.inner[self.head..].fill(0);
self.head = 0
}
self.inner[self.head..self.head + content.len()].copy_from_slice(content.as_bytes());
self.head += content.len();
self.inner[self.head..self.head + 2].copy_from_slice(&(content.len() as u16).to_le_bytes());
self.inner[self.head + 2] = author + 1;
self.head += 3;
self.next_msg += 1;
}
/// Iter messages from present to past
fn iter(&self) -> MsgsIter {
if self.head == 0 {
MsgsIter {
msgs: self,
head: 0,
current_id: 0,
finished: true,
}
} else {
MsgsIter {
msgs: self,
head: self.head,
current_id: self.next_msg - 1,
finished: false,
}
}
}
}
#[derive(Debug)]
struct MsgsIter<'a> {
msgs: &'a Msgs,
/// next byte index
head: usize,
finished: bool,
current_id: usize,
}
impl<'a> Iterator for MsgsIter<'a> {
type Item = Msg<'a>;
/// We trust msgs.inner validity in this function, it might panic or do UB if msgs.inner is not valid
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
if self.head == 0 {
self.head = MSGS_SIZE;
}
let above = self.head > self.msgs.head;
if above && self.head < self.msgs.head + 3 {
self.finished = true;
return None;
}
let author = self.msgs.inner[self.head - 1];
self.head -= 1;
if author == 0 {
return self.next();
}
let author = author - 1;
let len = u16::from_le_bytes([
self.msgs.inner[self.head - 2],
self.msgs.inner[self.head - 1],
]) as usize;
self.head -= 2;
let content =
unsafe { str::from_utf8_unchecked(&self.msgs.inner[self.head - len..self.head]) };
self.head -= len;
if above && self.head < self.msgs.head {
self.finished = true;
return None;
}
let id = self.current_id;
if self.current_id == 0 {
self.finished = true;
} else {
self.current_id -= 1;
}
Some(Msg {
id,
author,
content,
})
}
}
static MSGS: Mutex<ThreadModeRawMutex, Msgs> = Mutex::new(Msgs::default());
const USERNAME_MAX_LEN: usize = 16;
#[derive(Serialize)]
pub struct Usernames([Option<String<USERNAME_MAX_LEN>>; USERS_LEN as usize]);
impl Usernames {
const fn default() -> Self {
Self([None, None, None, None])
}
pub fn get_id(&mut self, name: &str) -> Option<u8> {
for (i, un) in self.0.iter().enumerate() {
if let Some(n) = un {
if n.as_str() == name {
return Some(i as u8);
}
}
}
for (i, un) in self.0.iter_mut().enumerate() {
if *un == None {
*un = Some(String::new());
un.as_mut().unwrap().push_str(name).unwrap();
USERNAMES_VERSION.add(1, Ordering::Relaxed);
return Some(i as u8);
}
}
None
}
}
pub static USERNAMES: Mutex<ThreadModeRawMutex, Usernames> = Mutex::new(Usernames::default());
pub static USERNAMES_VERSION: AtomicUsize = AtomicUsize::new(0);
pub struct ChatApp {
res_buf: String<1100>,
id: u8,
/// Id of the next message to send to client (so that 0 means no message has been sent)
next_msg: usize,
usernames_version: usize,
}
impl ChatApp {
pub fn new() -> Self {
pub fn new(id: u8) -> Self {
Self {
res_buf: String::new(),
id,
next_msg: 0,
usernames_version: 0,
}
}
}
@ -32,245 +178,81 @@ 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 str) {
match (req_type, path) {
(HttpRequestType::Get, "/" | "/index" | "/index.html" | "/chat" | "/chat.html") => {
(HttpResCode::Ok, "html", include_str!("./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,
_ => {}
fn accept_ws(&self, path: &str) -> bool {
path == "/"
}
async fn handle_ws<const BUF_SIZE: usize>(
&mut self,
_path: &str,
mut ws: crate::socket::ws::Ws<'_, BUF_SIZE>,
) {
Timer::after_millis(500).await;
let r: Result<(), ()> = try {
loop {
Timer::after_millis(1).await;
{
let uv = USERNAMES_VERSION.load(Ordering::Relaxed);
if self.usernames_version < uv {
ws.send_json(&(*USERNAMES.lock().await)).await?;
}
}
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, "", "");
{
let msgs = MSGS.lock().await;
for m in msgs.iter() {
if m.id >= self.next_msg {
ws.send_json(&m).await?;
}
}
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
self.next_msg = msgs.next_msg;
}
if ws.last_msg.elapsed() >= Duration::from_secs(5) {
ws.send(WsMsg::Ping(&[])).await?;
}
while let Some(r) = ws.rcv().await? {
info!("{:?}", r);
if let WsMsg::Text(r) = r {
if r.starts_with("send ") {
if r.len() > 5 + MSG_MAX_SIZE {
warn!("Message too long! (len={})", r.len() - 5);
return;
}
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, "", "");
}
{
MSGS.lock()
.await
.push(self.id, r.get(5..).unwrap_or_default());
}
};
};
return (HttpResCode::Ok, "html", &self.res_buf);
} else {
(HttpResCode::NotFound, "", "")
}
}
}
}
};
if r.is_err() {
warn!(
"Socket {}: error in ws, terminating connection",
self.socket_name()
);
}
}
}
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;
pub async fn id_to_static_str(id: u8) -> &'static str {
match id {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
10 => "10",
11 => "11",
12 => "12",
13 => "13",
14 => "14",
15 => "15",
_ => unimplemented().await,
}
}

View File

@ -1,15 +1,10 @@
<!doctype html>
<head>
<script>
var ws = new WebSocket("/chat");
</script>
</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>
<li><a href="/ttt?team=0">Tic Tac Toe</a> (team blue)</li>
<li><a href="/ttt?team=1">Tic Tac Toe</a> (team red)</li>
</ul>
</body>
</html>

View File

@ -1,4 +1,10 @@
use crate::socket::{HttpRequestType, HttpResCode};
use heapless::Vec;
use pico_website::unwrap;
use crate::{
apps::{self, Content, chat::id_to_static_str},
socket::{HttpRequestType, HttpResCode},
};
use super::App;
@ -7,17 +13,138 @@ impl App for IndexApp {
fn socket_name(&self) -> &'static str {
"index"
}
async fn handle_request<'a>(
&'a mut self,
async fn handle_request(
&mut self,
path: &str,
_req_type: HttpRequestType,
_content: &str,
) -> (HttpResCode, &'static str, &'a str) {
) -> (HttpResCode, &'static str, Option<Content<'_>>) {
match path {
"/" | "/index" | "/index.html" => {
(HttpResCode::Ok, "html", include_str!("./index.html"))
"/" | "/index" | "/index.html" => (
HttpResCode::Ok,
"html",
#[cfg(debug_assertions)]
Some(include_str!("index.html").into()),
#[cfg(not(debug_assertions))]
Some(include_str!("../../static/index.min.html").into()),
),
"/htmx.js" => (
HttpResCode::Ok,
"javascript",
#[cfg(debug_assertions)]
Some(include_str!("../../static/htmx.js").into()),
#[cfg(not(debug_assertions))]
Some(include_str!("../../static/htmx.min.js").into()),
),
path => {
let (path, args) = path.split_once('?').unwrap_or((path, ""));
let mut team = None;
let mut name = None;
let mut id = None;
for arg in args.split('&') {
match arg.split_once('=') {
Some(("team", "0")) => team = Some("0"),
Some(("team", "1")) => team = Some("1"),
Some(("name", n)) => {
if n.len() >= 1 && n.len() <= 16 {
name = Some(n);
}
}
Some(("id", i)) => {
if let Ok(i) = i.parse::<u8>() {
if i < apps::chat::USERS_LEN {
id = Some(i);
}
}
}
_ => {}
}
}
match path {
"/ttt" => {
let Some(team) = team else {
return (HttpResCode::BadRequest, "", None);
};
#[cfg(debug_assertions)]
let html = include_str!("ttt.html");
#[cfg(not(debug_assertions))]
let html = include_str!("../../static/ttt.min.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)))
}
"/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(";")?;
#[cfg(debug_assertions)]
content.push(include_str!("ttt.js"))?;
#[cfg(not(debug_assertions))]
content.push(include_str!("../../static/ttt.min.js"))?;
};
unwrap(r).await;
(HttpResCode::Ok, "javascript", Some(Content(content)))
}
"/chat" => {
let Some(name) = name else {
return (HttpResCode::BadRequest, "", None);
};
// #[cfg(debug_assertions)]
let html = include_str!("chat.html");
// #[cfg(not(debug_assertions))]
// let html = include_str!("../../static/chat.min.html");
let Some(id) = apps::chat::USERNAMES.lock().await.get_id(name) else {
return (HttpResCode::NoContent, "", None);
};
let mut content = Vec::new();
let r: Result<(), &str> = try {
let (html1, html2) = html.split_once("/chat.js").ok_or("")?;
content.push(html1)?;
content.push("/chat.js?id=")?;
content.push(id_to_static_str(id).await)?;
content.push(html2)?;
};
unwrap(r).await;
(HttpResCode::Ok, "html", Some(Content(content)))
}
"/chat.js" => {
let Some(id) = id else {
return (HttpResCode::BadRequest, "", None);
};
let mut content = Vec::new();
let r: Result<(), &str> = try {
content.push("const id = ")?;
content.push(id_to_static_str(id).await)?;
content.push(";")?;
// #[cfg(debug_assertions)]
content.push(include_str!("chat.js"))?;
// #[cfg(not(debug_assertions))]
// content.push(include_str!("../../static/chat.min.js"))?;
};
unwrap(r).await;
(HttpResCode::Ok, "javascript", Some(Content(content)))
}
_ => (HttpResCode::NotFound, "", None),
}
}
_ => (HttpResCode::NotFound, "", ""),
}
}
}

View File

@ -1,3 +1,5 @@
use heapless::Vec;
use crate::socket::{HttpRequestType, HttpResCode, ws::Ws};
#[cfg(feature = "chat")]
@ -10,19 +12,29 @@ pub trait App {
fn socket_name(&self) -> &'static str;
async fn handle_request<'a>(
&'a mut self,
_path: &str,
_path: &'a str,
_req_type: HttpRequestType,
_content: &str,
) -> (HttpResCode, &'static str, &'a str) {
(HttpResCode::NotFound, "", "")
_content: &'a str,
) -> (HttpResCode, &'static str, Option<Content<'a>>) {
(HttpResCode::NotFound, "", None)
}
fn accept_ws(&self, _path: &str) -> bool {
false
}
async fn handle_ws<'a, const BUF_SIZE: usize, const RES_HEAD_BUF_SIZE: usize>(
&'a mut self,
_path: &str,
_ws: Ws<'a, BUF_SIZE, RES_HEAD_BUF_SIZE>,
) {
async fn handle_ws<const BUF_SIZE: usize>(&mut self, _path: &str, _ws: Ws<'_, BUF_SIZE>) {}
}
pub struct Content<'a>(pub Vec<&'a str, 8>);
impl<'a> From<&'a str> for Content<'a> {
fn from(value: &'a str) -> Self {
let mut v = Vec::new();
v.push(value).unwrap();
Content(v)
}
}
impl Content<'_> {
pub fn len(&self) -> usize {
self.0.iter().fold(0, |acc, s| acc + s.len())
}
}

View File

@ -1,30 +1,32 @@
<!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>
<head>
<style type="text/css">
body {
#grid {
.cell {
border: 1px dotted black;
padding: 33%;
}
.cell[team="0"] {
background-color: dodgerblue;
}
.cell[team="1"] {
background-color: firebrick;
}
display: grid;
border: 1px solid black;
grid-template-rows: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
}
}
</style>
<script src="/ttt.js" defer></script>
</head>
<body>
<h1>TicTacToe</h1>
<div
id="game"
hx-get="/ttt/initial_game"
hx-swap="outerHTML"
hx-trigger="load"
hx-target="this"
></div>
<h3 id="team"></h3>
<h3 id="winner"></h3>
<div id="grid"></div>
</body>
</html>

72
src/apps/ttt.js Normal file
View File

@ -0,0 +1,72 @@
//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:' +
teams[team].color +
'">' +
teams[team].name +
"</span>";
const ws = new WebSocket(
"ws://192.254.0.2:" + teams[team].port + "/" + teams[team].name,
);
ws.onmessage = (event) => {
if (typeof event.data == "string") {
let msg = JSON.parse(event.data);
let cells = [];
for (let i = 0; i < 9; i++) {
let owner = msg.board[i];
let tagName;
if (msg.turn == team && owner === null) {
tagName = "button";
} else {
tagName = "div";
}
let cell = document.createElement(tagName);
cell.classList.add("cell");
if (tagName === "button") {
cell.addEventListener("click", (event) => {
ws.send(new Uint8Array([i]));
});
}
cell.setAttribute("team", owner);
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,217 +1,161 @@
use core::fmt::Write;
use core::{ops::Not, sync::atomic::Ordering};
use embassy_time::{Duration, Instant};
use heapless::String;
use pico_website::unwrap;
use portable_atomic::{AtomicBool, AtomicU32};
use core::ops::Not;
use core::str::from_utf8_unchecked;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Instant, Timer};
use log::{info, warn};
use pico_website::{unwrap, unwrap_opt};
use serde::Serialize;
use crate::apps::Content;
use crate::socket::ws::{Ws, WsMsg};
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
static BOARD: AtomicU32 = AtomicU32::new(0);
#[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());
pub struct TttApp {
res_buf: String<2048>,
/// State of the board last time it has been sent
last_board: u32,
team: Team,
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 {
res_buf: String::new(),
last_board: 0,
team,
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));
}
}
}
/// Generate board html
async fn generate_board_res<'a>(
&'a mut self,
board: u32,
turn: Team,
outer_html: bool,
) -> &'a str {
self.res_buf.clear();
if outer_html {
unwrap(self.res_buf.push_str(
"<div \
id=\"game\" \
hx-get=\"/ttt/game\" \
hx-swap=\"innerHTML\" \
hx-trigger=\"every 100ms\" \
hx-target=\"this\"\
>",
))
.await;
}
unwrap(write!(
self.res_buf,
"<h3>Team : <span style=\"color:{}\">{}</span></h3>",
self.team.color(),
self.team.name()
))
.await;
match self.end {
Some((_, Some(t))) => {
unwrap(write!(
self.res_buf,
"<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>",
t.color(),
t.name()
))
.await
}
Some((_, None)) => unwrap(write!(self.res_buf, "<br><h3>Draw!</h3><br>",)).await,
None => {}
}
unwrap(self.res_buf.push_str("<div id=\"grid\">")).await;
for c in 0..=8 {
let picked_by = if board & (1 << c) != 0 {
Some(Team::Zero)
} else if board & (1 << (9 + c)) != 0 {
Some(Team::One)
} else {
None
};
match picked_by {
Some(t) => {
unwrap(write!(
self.res_buf,
"<div class=\"cell\" style=\"background-color:{}\"></div>",
t.color()
))
.await;
}
None => {
if self.team == turn.into() && self.end.is_none() {
unwrap(write!(
self.res_buf,
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>",
c
)).await;
} else {
unwrap(self.res_buf.push_str("<div class=\"cell\"></div>")).await;
}
}
};
}
unwrap(self.res_buf.push_str("</div>")).await;
if outer_html {
unwrap(self.res_buf.push_str("</div>")).await;
}
&self.res_buf
}
}
impl App for TttApp {
fn socket_name(&self) -> &'static str {
self.team.name()
match self.team {
Team::Zero => "ttt0",
Team::One => "ttt1",
}
}
async fn handle_request<'a>(
fn accept_ws(&self, path: &str) -> bool {
(self.team == Team::Zero && path == "/blue") || (self.team == Team::One && path == "/red")
}
async fn handle_ws<'a, const BUF_SIZE: usize>(
&'a mut self,
path: &str,
_req_type: HttpRequestType,
_content: &str,
) -> (HttpResCode, &'static str, &'a str) {
match path {
"/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => {
(HttpResCode::Ok, "html", include_str!("./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::<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, "", "");
_path: &str,
mut ws: Ws<'a, BUF_SIZE>,
) {
Timer::after_millis(500).await;
let r: Result<(), ()> = try {
loop {
Timer::after_millis(1).await;
let Ok(mut game) = GAME.try_lock() else {
info!("game locked");
continue;
};
if self.last_game != *game {
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) };
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?;
}
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? {
if let WsMsg::Bytes([c]) = r {
let c = *c as usize;
if c >= game.board.len() {
warn!("Cell played is too big!");
continue;
}
if game.board[c].is_some() {
warn!("Cell is already taken!");
continue;
}
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!");
continue;
}
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, "", "")
}
}
};
if r.is_err() {
warn!(
"Socket {}: error in ws, terminating connection",
self.socket_name()
);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Team {
#[default]
Zero = 0,
One = 1,
}
@ -237,6 +181,14 @@ impl Not for Team {
}
}
}
impl Serialize for Team {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(*self as u8)
}
}
impl Team {
fn color(self) -> &'static str {
match self {

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>) {
@ -36,9 +36,11 @@ pub async fn dhcp_server(stack: Stack<'static>) {
info!("Starting DHCP server");
loop {
Timer::after_secs(0).await;
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;
@ -53,7 +55,6 @@ pub async fn dhcp_server(stack: Stack<'static>) {
rapid_commit = true;
}
info!("Dhcp: received {:?} message", msg_type);
Timer::after_secs(0).await;
match msg_type {
DhcpMsgType::DISCOVER | DhcpMsgType::REQUEST => {
@ -126,7 +127,6 @@ pub async fn dhcp_server(stack: Stack<'static>) {
)
.await;
info!("Dhcp: offer/ack sent for ip 192.254.0.{}", current_ip);
Timer::after_secs(0).await;
if msg_type == DhcpMsgType::REQUEST || rapid_commit {
current_ip += 1;
@ -160,8 +160,7 @@ async fn write_dhcp_opts<const N: usize>(buf: &mut Vec<u8, N>, op_codes: &[u8])
59 => (4, &3500_u32.to_be_bytes()), // rebinding time
80 => (0, &[]),
_ => {
info!("Dhcp: unhandled requested option {}", o);
Timer::after_secs(0).await;
warn!("Dhcp: unhandled requested option {}", o);
continue;
}
};

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,18 +1,42 @@
#![no_std]
use core::fmt::Debug;
use core::{fmt::Debug, panic::PanicInfo};
use embassy_time::Timer;
use log::info;
use log::error;
pub async fn unwrap<T, E: Debug>(res: Result<T, E>) -> T {
match res {
Ok(v) => v,
Err(e) => {
info!("FATAL ERROR : {:?}", e);
loop {
info!("FATAL ERROR : {:?}", e);
Timer::after_secs(5).await;
}
}
Err(e) => loop {
error!("FATAL ERROR : {:?}", e);
Timer::after_secs(5).await;
},
}
}
pub async fn unwrap_opt<T>(opt: Option<T>) -> T {
expect_opt("option unwraped", opt).await
}
pub async fn assert(condition: bool) {
if !condition {
let err: Result<(), &str> = Err("assert failed");
unwrap(err).await;
}
}
pub async fn expect_opt<T>(msg: &str, opt: Option<T>) -> T {
unwrap(opt.ok_or(msg)).await
}
pub async fn unimplemented() -> ! {
let err: Result<(), &str> = Err("unimplemented");
unwrap(err).await;
loop {}
}
// TODO: make this log work
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
error!("PANIC: {}", info);
loop {}
}

View File

@ -4,6 +4,10 @@
#![feature(impl_trait_in_assoc_type)]
#![feature(slice_split_once)]
#![feature(try_blocks)]
#![feature(impl_trait_in_bindings)]
#![feature(array_repeat)]
#![feature(generic_arg_infer)]
#![feature(async_iterator)]
#[cfg(feature = "wifi-connect")]
use core::net::Ipv4Addr;
@ -22,7 +26,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;
@ -62,13 +65,6 @@ async fn main(spawner: Spawner) {
spawner.spawn(logger_task(driver)).unwrap();
let mut rng = RoscRng;
// let mut i = 0;
// loop {
// info!("test{}", i);
// Timer::after_secs(1).await;
// i += 1;
// }
let fw = include_bytes!("../cyw43-firmware/43439A0.bin");
let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin");
let pwr = Output::new(p.PIN_23, Level::Low);
@ -163,9 +159,6 @@ async fn main(spawner: Spawner) {
// Wait for DHCP, not necessary when using static IP
info!("waiting for DHCP...");
stack.wait_config_up().await;
// while !stack.is_config_up() {
// Timer::after_millis(100).await;
// }
info!("DHCP is now up!");
info!(
"ip : {}",
@ -188,8 +181,8 @@ async fn main(spawner: Spawner) {
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
}
#[cfg(feature = "chat")]
for _ in 0..4 {
unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await;
for i in 0..4 {
unwrap(spawner.spawn(socket::chat_listen_task(stack, i, 9000 + i as u16))).await;
}
info!("All apps lauched!");
}

View File

@ -1,13 +1,15 @@
use base64::{EncodeSliceError, prelude::*};
use base64::prelude::*;
use core::fmt::Write;
use core::str::from_utf8;
use core::{fmt::Write, str::FromStr};
use embassy_net::tcp::TcpSocket;
use embassy_time::{Duration, Timer};
use embedded_io_async::Write as _;
use heapless::{String, Vec};
use log::{info, warn};
use pico_website::unwrap;
use sha1::{Digest, Sha1};
use crate::apps::Content;
use crate::{apps, socket::ws::Ws};
pub mod ws;
@ -15,28 +17,32 @@ pub mod ws;
#[cfg(feature = "ttt")]
#[embassy_executor::task(pool_size = 2)]
pub async fn ttt_listen_task(stack: embassy_net::Stack<'static>, team: apps::ttt::Team, port: u16) {
listen_task(stack, apps::ttt::TttApp::new(team), port).await
listen_task::<32, 32, 1024, 256>(stack, apps::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, apps::index::IndexApp, port).await
listen_task::<64, 0, 2048, 1024>(stack, apps::index::IndexApp, port).await
}
#[cfg(feature = "chat")]
#[embassy_executor::task(pool_size = 4)]
pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, port: u16) {
listen_task(stack, apps::chat::ChatApp::new(), port).await
pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, id: u8, port: u16) {
listen_task::<64, 1024, 1024, 1024>(stack, apps::chat::ChatApp::new(id), port).await
}
pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps::App, port: u16) {
// loop {
// info!("team:{:?}", team);
// Timer::after_millis(0).await;
// }
let mut rx_buffer = [0; 1024];
let mut tx_buffer = [0; 2048];
let mut buf = [0; 1024];
pub async fn listen_task<
const PATH_LEN: usize,
const BUF_LEN: usize,
const RX_LEN: usize,
const TX_LEN: usize,
>(
stack: embassy_net::Stack<'static>,
mut app: impl apps::App,
port: u16,
) {
let mut rx_buffer = [0; RX_LEN];
let mut tx_buffer = [0; TX_LEN];
let mut head_buf = Vec::<u8, 256>::new();
loop {
Timer::after_secs(0).await;
@ -55,155 +61,162 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
socket.remote_endpoint()
);
// let (mut rx, mut tx) = socket.split();
let mut buf = String::<BUF_LEN>::new();
let mut path = String::<PATH_LEN>::new();
let mut request_type = HttpRequestType::Get;
let mut is_ws = false;
loop {
Timer::after_secs(0).await;
let n = match socket.read(&mut buf).await {
Ok(0) => {
warn!("read EOF");
break;
}
Ok(n) => n,
match socket
.read_with(|msg| {
let (headers, content) = match from_utf8(msg) {
Ok(b) => {
info!("{}", b);
match b.split_once("\r\n\r\n") {
Some(t) => t,
None => (b, ""),
}
}
Err(_) => {
warn!("Non utf8 http request");
return (0, Err(()));
}
};
buf.clear();
if let Err(_) = buf.push_str(content) {
warn!("Received content is bigger than maximum content!");
return (0, Err(()));
}
let mut hl = headers.lines();
match hl.next() {
None => {
warn!("Empty request");
return (0, Err(()));
}
Some(l1) => {
let mut l1 = l1.split(' ');
request_type = match l1.next() {
Some("GET") => HttpRequestType::Get,
Some("POST") => HttpRequestType::Post,
Some(t) => {
warn!("Unknown request type : {}", t);
return (0, Err(()));
}
None => {
warn!("No request type");
return (0, Err(()));
}
};
path.clear();
if let Err(_) = path.push_str(match l1.next() {
Some(path) => path,
None => {
warn!("No path");
return (0, Err(()));
}
}) {
warn!("Path is too big!");
return (0, Err(()));
}
}
};
let mut host = None;
let mut ws_key = None;
for h in hl {
let Some((name, val)) = h.split_once(':') else {
continue;
};
let name = name.trim();
let val = val.trim();
match (name, val) {
("Host", _) => host = Some(val),
("Upgrade", "websocket") => is_ws = true,
("Sec-WebSocket-Key", _) => ws_key = Some(val),
_ => {}
}
}
let Some(host) = host else {
warn!("No host!");
return (0, Err(()));
};
info!(
"Socket {}: {:?}{} request for {}{}",
app.socket_name(),
request_type,
if is_ws { " websocket" } else { "" },
host,
path,
);
buf.clear();
if is_ws {
let Some(key) = ws_key else {
warn!("No ws key!");
return (0, Err(()));
};
if let Err(_) = buf.push_str(key) {
warn!("Ws key is too long!");
return (0, Err(()));
}
} else {
if let Err(_) = buf.push_str(content) {
warn!("Content is too long!");
return (0, Err(()));
}
}
(msg.len(), Ok(()))
})
.await
{
Ok(Ok(())) => {}
Ok(Err(())) => break,
Err(e) => {
warn!("Socket {}: read error: {:?}", app.socket_name(), e);
warn!("Error while receiving : {:?}", e);
break;
}
};
head_buf.clear();
let (headers, content) = match from_utf8(&buf[..n]) {
Ok(b) => match b.split_once("\r\n\r\n") {
Some(t) => t,
None => (b, ""),
},
Err(_) => {
warn!("Non utf8 http request");
break;
}
};
info!("\n{:?}\n", headers);
let mut hl = headers.lines();
let (request_type, path) = match hl.next() {
None => {
warn!("Empty request");
break;
}
Some(l1) => {
let mut l1 = l1.split(' ');
(
match l1.next() {
Some("GET") => HttpRequestType::Get,
Some("POST") => HttpRequestType::Post,
Some(t) => {
warn!("Unknown request type : {}", t);
break;
}
None => {
warn!("No request type");
break;
}
},
match l1.next() {
Some(path) => path,
None => {
warn!("No path");
break;
}
},
)
}
};
let mut host = None;
let mut ws_handshake = false;
let mut ws_key = None;
for h in hl {
let Some((name, val)) = h.split_once(':') else {
continue;
};
let name = name.trim();
let val = val.trim();
match (name, val) {
("Host", _) => host = Some(val),
("Upgrade", "websocket") => ws_handshake = true,
("Sec-WebSocket-Key", _) => ws_key = Some(val),
_ => {}
}
}
let Some(host) = host else {
warn!("No host");
break;
};
info!(
"Socket {}: {:?}{} request for {}{}",
app.socket_name(),
request_type,
if ws_handshake { " websocket" } else { "" },
host,
path,
);
Timer::after_secs(0).await;
head_buf.clear();
let res_content: Result<&str, core::fmt::Error> = try {
if ws_handshake {
if !app.accept_ws(path) {
let res_content: Result<Option<Content>, core::fmt::Error> = try {
if is_ws {
if !app.accept_ws(&path) {
warn!("No ws there!");
write!(
&mut head_buf,
"{}\r\n\r\n",
Into::<&str>::into(HttpResCode::NotFound)
)?;
""
None
} else {
if path.len() > 16 {
warn!("Ws socket cannot have path longer than 16 chars!");
let Ok(accept) = compute_ws_accept(&buf) else {
break;
}
let Some(key) = ws_key else {
warn!("No ws key!");
break;
};
let accept = match compute_ws_accept(key).await {
Ok(a) => a,
Err(e) => {
warn!("compute ws accept error : {:?}", e);
break;
}
};
write!(
&mut head_buf,
"{}\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: {}\r\n\r\n",
// Sec-WebSocket-Protocol: chat\r\n
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: {}\r\n\r\n",
Into::<&str>::into(HttpResCode::SwitchingProtocols),
accept
)?;
""
None
}
} else {
let (code, res_type, res_content): (HttpResCode, &str, &str) = match path {
"/htmx.js" => (
HttpResCode::Ok,
"javascript",
#[cfg(debug_assertions)]
include_str!("../static/htmx.js"),
#[cfg(not(debug_assertions))]
include_bytes!("../static/htmx.min.js"),
),
_ => app.handle_request(path, request_type, content).await,
};
let (code, res_type, res_content) =
app.handle_request(&path, request_type, &buf).await;
write!(&mut head_buf, "{}", Into::<&str>::into(code))?;
if res_type.len() > 0 {
if let Some(ref c) = res_content {
write!(
&mut head_buf,
"\r\n\
Content-Type: text/{}\r\n\
Content-Length: {}\r\n",
res_type,
res_content.len()
c.len()
)?;
}
write!(&mut head_buf, "\r\n\r\n")?;
@ -219,33 +232,37 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps:
}
};
info!("\n{}\n", from_utf8(&head_buf).unwrap());
match socket.write_all(&head_buf).await {
Ok(()) => {}
Err(e) => {
warn!("write error: {:?}", e);
break;
}
};
match socket.write_all(res_content.as_bytes()).await {
Ok(()) => {}
Err(e) => {
warn!("write error: {:?}", e);
break;
let w: Result<(), embassy_net::tcp::Error> = try {
socket.write_all(&head_buf).await?;
if let Some(ref c) = res_content {
for s in c.0.iter() {
socket.write_all(s.as_bytes()).await?;
}
}
};
if ws_handshake {
let path: String<16> = String::from_str(path).unwrap();
app.handle_ws(
&path,
Ws::new(&mut socket, &mut buf, &mut head_buf, app.socket_name()),
)
.await;
if let Err(e) = w {
warn!("write error: {:?}", e);
break;
};
if is_ws {
break;
}
}
if is_ws {
let mut buf = buf.into_bytes();
unwrap(buf.resize_default(BUF_LEN)).await;
app.handle_ws::<BUF_LEN>(
&path,
Ws::new(
&mut socket,
&mut unwrap(buf.into_array()).await,
app.socket_name(),
),
)
.await;
}
}
}
@ -285,13 +302,22 @@ impl Into<&str> for HttpResCode {
}
}
async fn compute_ws_accept(key: &str) -> Result<String<28>, EncodeSliceError> {
fn compute_ws_accept(key: &str) -> Result<String<28>, ()> {
let mut res = Vec::<u8, 28>::new();
res.extend_from_slice(&[0; 28]).unwrap();
res.resize_default(28)?;
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())
if let Err(e) = BASE64_STANDARD.encode_slice(hash, &mut res) {
warn!("Error while base64 encoding : {}\nkey: {}", e, key);
return Err(());
};
match String::from_utf8(res) {
Ok(r) => Ok(r),
Err(e) => {
warn!("Ws accept is not utf8! ({})", e);
Err(())
}
}
}

View File

@ -2,11 +2,11 @@ use core::str::from_utf8;
use embassy_net::tcp::{TcpReader, TcpSocket, TcpWriter};
use embassy_time::Instant;
use embedded_io_async::{ErrorType, ReadReady, Write};
use heapless::Vec;
use log::{info, warn};
use embedded_io_async::ReadReady;
use log::warn;
use pico_website::{assert, unwrap, unwrap_opt};
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum WsMsg<'a> {
Ping(&'a [u8]),
Pong(&'a [u8]),
@ -15,6 +15,10 @@ pub enum WsMsg<'a> {
Unknown(u8, &'a [u8]),
}
impl WsMsg<'_> {
const TEXT: u8 = 1;
const BYTES: u8 = 2;
const PING: u8 = 9;
const PONG: u8 = 10;
pub const fn len(&self) -> usize {
self.as_bytes().len()
}
@ -38,78 +42,132 @@ 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> {
struct WsTx<'a> {
socket: TcpWriter<'a>,
head_buf: &'a mut Vec<u8, HEAD_BUF_SIZE>,
}
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();
if msg.len() < 126 {
self.head_buf.push(msg.len() as u8).unwrap();
impl<'a> WsTx<'a> {
pub async fn send_with<F: Fn(&mut [u8]) -> Result<usize, ()>>(
&mut self,
msg_code: u8,
f: F,
) -> Result<(), ()> {
if self.send_with_no_flush(msg_code, &f).await.is_err() {
self.socket.flush().await.map_err(|_| ())?;
self.send_with_no_flush(msg_code, f).await
} else {
self.head_buf.push(0b0111_1110).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();
Ok(())
}
let w: Result<(), <TcpSocket<'_> as ErrorType>::Error> = try {
self.socket.write_all(&self.head_buf).await?;
self.socket.write_all(msg.as_bytes()).await?;
};
w.map_err(|e| {
warn!("write error: {:?}", e);
()
}
pub async fn send_with_no_flush<F: FnOnce(&mut [u8]) -> Result<usize, ()>>(
&mut self,
msg_code: u8,
f: F,
) -> Result<(), ()> {
self.socket
.write_with(|buf| {
if buf.len() < 6 {
return (0, Err(()));
}
buf[0] = 0b1000_0000 | msg_code;
let Ok(n) = f(&mut buf[4..]) else {
return (0, Err(()));
};
if n < 126 {
buf[1] = n as u8;
buf.copy_within(4..4 + n, 2);
(n + 2, Ok(()))
} else {
buf[1..=2].copy_from_slice(&(n as u16).to_le_bytes());
(n + 4, Ok(()))
}
})
.await
.map_err(|_| ())?
}
pub async fn send<'m>(&mut self, msg: WsMsg<'m>) -> Result<(), ()> {
self.send_with(msg.code(), |buf| {
let msg = msg.as_bytes();
if buf.len() < msg.len() {
Err(())
} else {
buf[..msg.len()].copy_from_slice(msg);
Ok(msg.len())
}
})
.await
}
pub async fn send_json<T: serde::Serialize>(&mut self, msg: &T) -> Result<(), ()> {
self.send_with(WsMsg::TEXT, |buf| {
serde_json_core::to_slice(msg, buf).map_err(|_| ())
})
.await
}
}
pub struct Ws<'a, const BUF_SIZE: usize = 1024, const RES_HEAD_BUF_SIZE: usize = 256> {
pub struct Ws<'a, const BUF_SIZE: usize = 1024> {
rx: WsRx<'a, BUF_SIZE>,
tx: WsTx<'a, RES_HEAD_BUF_SIZE>,
tx: WsTx<'a>,
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> {
pub fn new(
socket: &'a mut TcpSocket,
buf: &'a mut [u8; BUF_SIZE],
head_buf: &'a mut Vec<u8, HEAD_BUF_SIZE>,
name: &'a str,
) -> Self {
impl<'a, const BUF_SIZE: usize> Ws<'a, BUF_SIZE> {
pub fn new(socket: &'a mut TcpSocket, buf: &'a mut [u8; BUF_SIZE], name: &'a str) -> Self {
let (rx, tx) = socket.split();
Self {
rx: WsRx {
socket: rx,
buf,
last_msg: Instant::MIN,
},
tx: WsTx {
socket: tx,
head_buf,
msg_in_buf: None,
},
tx: WsTx { socket: tx },
last_msg: Instant::MIN,
name,
}
}
// Do this often to respond to pings
async fn rcv(&mut self) -> Result<Option<WsMsg>, ()> {
if !self.rx.socket.read_ready().unwrap() {
return Ok(None);
}
let n = match self.rx.socket.read(self.rx.buf).await {
Ok(0) => {
warn!("read EOF");
return Err(());
/// Do this often to respond to pings
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 unwrap(self.rx.socket.read_ready()).await {
let n_rcv = match self.rx.socket.read(&mut self.rx.buf[n.1..]).await {
Ok(0) => {
warn!("read EOF");
return Err(());
}
Ok(n) => n,
Err(e) => {
warn!("Socket {}: read error: {:?}", self.name, e);
return Err(());
}
};
n.1 + n_rcv
} else {
n.1
}
}
Ok(n) => n,
Err(e) => {
warn!("Socket {}: read error: {:?}", self.name, e);
return Err(());
None => {
if unwrap(self.rx.socket.read_ready()).await {
match self.rx.socket.read(self.rx.buf).await {
Ok(0) => {
warn!("read EOF");
return Err(());
}
Ok(n) => n,
Err(e) => {
warn!("Socket {}: read error: {:?}", self.name, e);
return Err(());
}
}
} else {
return Ok(None);
}
}
};
if self.rx.buf[0] & 0b1000_0000 == 0 {
warn!("Fragmented ws messages are not supported!");
return Err(());
@ -150,7 +208,13 @@ 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 % 4)).await;
}
if n_after_length + 4 + (length as usize) < n {
self.rx.msg_in_buf = Some((
n_after_length + 4 + (length as usize),
(n - (n_after_length + 4 + (length as usize))),
));
}
&self.rx.buf[n_after_length + 4..n_after_length + 4 + length as usize]
} else {
@ -158,16 +222,23 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
warn!("ws payload smaller than length");
return Err(());
}
if n_after_length + (length as usize) < n {
self.rx.msg_in_buf = Some((
n_after_length + (length as usize),
(n - (n_after_length + (length as usize))),
));
}
&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 => {
let content = from_utf8(&content).map_err(|_| ())?;
info!("Received text : {:?}", content);
Ok(Some(WsMsg::Text(content)))
}
// Bytes
2 => Ok(Some(WsMsg::Bytes(content))),
// Ping
9 => {
self.tx.send(WsMsg::Pong(&content)).await?;
@ -176,12 +247,37 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
// Pong
10 => Ok(Some(WsMsg::Pong(&content))),
c => {
info!("Unknown ws op code (ignoring) : {}", c);
warn!("Unknown ws op code (ignoring) : {}", c);
Ok(Some(WsMsg::Unknown(c, &content)))
}
}
}
pub async fn send(&mut self, msg: WsMsg<'a>) -> Result<(), ()> {
self.tx.send(msg).await
pub async fn send(&mut self, msg: WsMsg<'_>) -> Result<(), ()> {
self.tx.send(msg).await?;
self.last_msg = Instant::now();
Ok(())
}
pub async fn send_json<T: serde::Serialize>(&mut self, msg: &T) -> Result<(), ()> {
self.tx.send_json(msg).await?;
self.last_msg = Instant::now();
Ok(())
}
pub async fn send_with<F: Fn(&mut [u8]) -> Result<usize, ()>>(
&mut self,
msg_code: u8,
f: F,
) -> Result<(), ()> {
self.tx.send_with(msg_code, f).await?;
self.last_msg = Instant::now();
Ok(())
}
pub async fn send_with_no_flush<F: FnOnce(&mut [u8]) -> Result<usize, ()>>(
&mut self,
msg_code: u8,
f: F,
) -> Result<(), ()> {
self.tx.send_with_no_flush(msg_code, f).await?;
self.last_msg = Instant::now();
Ok(())
}
}

1
static/index.min.html Normal file
View File

@ -0,0 +1 @@
<!doctype html><html><body><h1>Apps</h1><ul><li><a href="/ttt?team=0">Tic Tac Toe</a>(team blue)</li><li><a href="/ttt?team=1">Tic Tac Toe</a>(team red)</li></ul></body></html>

1
static/ttt.min.html Normal file
View File

@ -0,0 +1 @@
<!doctype html><head><style type="text/css"> body { #grid { .cell { border: 1px dotted black; padding: 33%; } .cell[team="0"] { background-color: dodgerblue; } .cell[team="1"] { background-color: firebrick; } display: grid; border: 1px solid black; grid-template-rows: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr; } } </style><script src="/ttt.js" defer></script></head><html><body><h1>TicTacToe</h1><h3 id="team"></h3><h3 id="winner"></h3><div id="grid"></div></body></html>

1
static/ttt.min.js vendored Normal file
View File

@ -0,0 +1 @@
if(0!=team&&1!=team)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:'+teams[team].color+'">'+teams[team].name+"</span>";const ws=new WebSocket("ws://192.254.0.2:"+teams[team].port+"/"+teams[team].name);ws.onmessage=e=>{if("string"==typeof e.data){let t=JSON.parse(e.data),n=[];for(let e=0;e<9;e++){let a,r=t.board[e];a=t.turn==team&&null===r?"button":"div";let m=document.createElement(a);m.classList.add("cell"),"button"===a&&m.addEventListener("click",(t=>{ws.send(new Uint8Array([e]))})),m.setAttribute("team",r),n.push(m)}document.getElementById("grid").replaceChildren(...n),null==t.turn?null==t.winner?document.getElementById("winner").innerHTML="Draw!":document.getElementById("winner").innerHTML='Winner : <span style="color:'+teams[t.winner].color+'">'+teams[t.winner].name+"</span>":document.getElementById("winner").innerHTML=""}};