Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

22 changed files with 1016 additions and 1372 deletions

524
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,44 +4,48 @@ 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:ringbuf", "json"]
ttt = ["json"]
default = ["dhcp", "dns", "chat"]
chat = ["dep:ringbuffer"]
ttt = []
default = ["dhcp", "dns"]
[dependencies]
# git = "https://github.com/embassy-rs/embassy",
embassy-executor = { version = "*", features = [
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt",
"nightly",
"arch-cortex-m",
"executor-thread",
"executor-interrupt",
] }
embassy-rp = { version = "*", features = [
embassy-rp = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt",
"unstable-pac",
"rp2040",
"time-driver",
"critical-section-impl",
] }
embassy-time = { version = "*" }
embassy-usb-logger = { version = "*" }
embassy-net = { version = "*", features = [
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",
"proto-ipv4",
"tcp",
"udp",
"dhcpv4",
] }
embassy-sync = { version = "*" }
cyw43-pio = { version = "*" }
cyw43 = { version = "*" }
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" }
embedded-io-async = "*"
defmt = "*"
defmt-rtt = "*"
panic-probe = "*"
cortex-m = { version = "*", features = ["inline-asm"] }
cortex-m-rt = "*"
static_cell = "*"
@ -56,9 +60,7 @@ serde = { version = "*", optional = true, default-features = false, features = [
] }
dhcparse = { version = "*", default-features = false, optional = true }
dnsparse = { version = "*", optional = true }
ringbuf = { version = "*", default-features = false, features = [
"portable-atomic",
], optional = true }
ringbuffer = { version = "*", default-features = false, 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-2025-03-18"
channel = "nightly"

View File

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

View File

@ -1,47 +0,0 @@
// 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,176 +1,30 @@
use core::sync::atomic::Ordering;
use core::fmt::Write;
use dhcparse::dhcpv4::MAX_MESSAGE_SIZE;
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
use embassy_time::{Duration, Timer};
use heapless::String;
use heapless::{String, Vec};
use log::{info, warn};
use pico_website::unimplemented;
use portable_atomic::AtomicUsize;
use serde::Serialize;
use percent_encoding::percent_decode_str;
use pico_website::unwrap;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use crate::{apps::App, socket::ws::WsMsg};
use crate::socket::{HttpRequestType, HttpResCode};
// Must be <= u8::MAX-1;
pub const USERS_LEN: u8 = 4;
use super::App;
const MSG_MAX_SIZE: usize = 500;
#[derive(Debug, Serialize)]
struct Msg<'a> {
id: usize,
author: u8,
content: &'a str,
}
const MEMORY_SIZE: usize = 16;
const USERNAME_MIN_SIZE: usize = 3;
const USERNAME_SIZE: usize = 16;
const MSG_SIZE: usize = 128;
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);
static MESSAGES: Mutex<ThreadModeRawMutex, Messages> = Mutex::new(Messages::new());
pub struct ChatApp {
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,
res_buf: String<1100>,
}
impl ChatApp {
pub fn new(id: u8) -> Self {
pub fn new() -> Self {
Self {
id,
next_msg: 0,
usernames_version: 0,
res_buf: String::new(),
}
}
}
@ -178,81 +32,245 @@ impl App for ChatApp {
fn socket_name(&self) -> &'static str {
"chat"
}
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?;
}
}
{
let msgs = MSGS.lock().await;
for m in msgs.iter() {
if m.id >= self.next_msg {
ws.send_json(&m).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;
}
{
MSGS.lock()
.await
.push(self.id, r.get(5..).unwrap_or_default());
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,
_ => {}
}
}
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, "", "")
}
}
};
if r.is_err() {
warn!(
"Socket {}: error in ws, terminating connection",
self.socket_name()
);
}
}
}
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,
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;
}
}

View File

@ -1,10 +1,15 @@
<!doctype html>
<head>
<script>
var ws = new WebSocket("/chat");
</script>
</head>
<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>
<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>

View File

@ -1,10 +1,4 @@
use heapless::Vec;
use pico_website::unwrap;
use crate::{
apps::{self, Content, chat::id_to_static_str},
socket::{HttpRequestType, HttpResCode},
};
use crate::socket::{HttpRequestType, HttpResCode};
use super::App;
@ -13,138 +7,17 @@ impl App for IndexApp {
fn socket_name(&self) -> &'static str {
"index"
}
async fn handle_request(
&mut self,
async fn handle_request<'a>(
&'a mut self,
path: &str,
_req_type: HttpRequestType,
_content: &str,
) -> (HttpResCode, &'static str, Option<Content<'_>>) {
) -> (HttpResCode, &'static str, &'a str) {
match path {
"/" | "/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),
}
"/" | "/index" | "/index.html" => {
(HttpResCode::Ok, "html", include_str!("./index.html"))
}
_ => (HttpResCode::NotFound, "", ""),
}
}
}

View File

@ -1,5 +1,3 @@
use heapless::Vec;
use crate::socket::{HttpRequestType, HttpResCode, ws::Ws};
#[cfg(feature = "chat")]
@ -12,29 +10,19 @@ pub trait App {
fn socket_name(&self) -> &'static str;
async fn handle_request<'a>(
&'a mut self,
_path: &'a str,
_path: &str,
_req_type: HttpRequestType,
_content: &'a str,
) -> (HttpResCode, &'static str, Option<Content<'a>>) {
(HttpResCode::NotFound, "", None)
_content: &str,
) -> (HttpResCode, &'static str, &'a str) {
(HttpResCode::NotFound, "", "")
}
fn accept_ws(&self, _path: &str) -> bool {
false
}
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())
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>,
) {
}
}

View File

@ -1,32 +1,30 @@
<!doctype html>
<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;
<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>
<script src="/ttt.js" defer></script>
</head>
}
</style>
</head>
<html>
<body>
<h1>TicTacToe</h1>
<h3 id="team"></h3>
<h3 id="winner"></h3>
<div id="grid"></div>
<div
id="game"
hx-get="/ttt/initial_game"
hx-swap="outerHTML"
hx-trigger="load"
hx-target="this"
></div>
</body>
</html>

View File

@ -1,72 +0,0 @@
//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,161 +1,217 @@
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 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 crate::apps::Content;
use crate::socket::ws::{Ws, WsMsg};
use crate::socket::{HttpRequestType, HttpResCode};
use super::App;
#[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());
static TURN: AtomicBool = AtomicBool::new(false);
// bits [0; 8] : player zero board / bits [9; 17] : player one board
static BOARD: AtomicU32 = AtomicU32::new(0);
pub struct TttApp {
res_buf: String<2048>,
/// State of the board last time it has been sent
last_board: u32,
team: 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],
end: Option<(Instant, Option<Team>)>,
}
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 {
match self.team {
Team::Zero => "ttt0",
Team::One => "ttt1",
}
self.team.name()
}
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>(
async fn handle_request<'a>(
&'a mut self,
_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;
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, "", "");
}
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, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Team {
#[default]
Zero = 0,
One = 1,
}
@ -181,14 +237,6 @@ 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, unwrap_opt};
use pico_website::unwrap;
#[embassy_executor::task(pool_size = 1)]
pub async fn dhcp_server(stack: Stack<'static>) {
@ -36,11 +36,9 @@ 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_opt(buf.get(..n)).await;
let msg = unwrap(dhcpv4::Message::new(&msg)).await;
let msg = unwrap(dhcpv4::Message::new(&buf[..n])).await;
let msg_type = unwrap(v4_options!(msg; MessageType required)).await;
let mut rapid_commit = false;
@ -55,6 +53,7 @@ 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 => {
@ -127,6 +126,7 @@ 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,7 +160,8 @@ 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, &[]),
_ => {
warn!("Dhcp: unhandled requested option {}", o);
info!("Dhcp: unhandled requested option {}", o);
Timer::after_secs(0).await;
continue;
}
};

View File

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

View File

@ -1,42 +1,18 @@
#![no_std]
use core::{fmt::Debug, panic::PanicInfo};
use core::fmt::Debug;
use embassy_time::Timer;
use log::error;
use log::info;
pub async fn unwrap<T, E: Debug>(res: Result<T, E>) -> T {
match res {
Ok(v) => v,
Err(e) => loop {
error!("FATAL ERROR : {:?}", e);
Timer::after_secs(5).await;
},
Err(e) => {
info!("FATAL ERROR : {:?}", e);
loop {
info!("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,10 +4,6 @@
#![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;
@ -26,6 +22,7 @@ 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;
@ -65,6 +62,13 @@ 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);
@ -159,6 +163,9 @@ 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 : {}",
@ -181,8 +188,8 @@ async fn main(spawner: Spawner) {
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
}
#[cfg(feature = "chat")]
for i in 0..4 {
unwrap(spawner.spawn(socket::chat_listen_task(stack, i, 9000 + i as u16))).await;
for _ in 0..4 {
unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await;
}
info!("All apps lauched!");
}

View File

@ -1,15 +1,13 @@
use base64::prelude::*;
use core::fmt::Write;
use base64::{EncodeSliceError, prelude::*};
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;
@ -17,32 +15,28 @@ 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::<32, 32, 1024, 256>(stack, apps::ttt::TttApp::new(team), port).await
listen_task(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::<64, 0, 2048, 1024>(stack, apps::index::IndexApp, port).await
listen_task(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>, id: u8, port: u16) {
listen_task::<64, 1024, 1024, 1024>(stack, apps::chat::ChatApp::new(id), port).await
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 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];
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];
let mut head_buf = Vec::<u8, 256>::new();
loop {
Timer::after_secs(0).await;
@ -61,162 +55,155 @@ pub async fn listen_task<
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;
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,
let n = match socket.read(&mut buf).await {
Ok(0) => {
warn!("read EOF");
break;
}
Ok(n) => n,
Err(e) => {
warn!("Error while receiving : {:?}", e);
warn!("Socket {}: read error: {:?}", app.socket_name(), 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<Option<Content>, core::fmt::Error> = try {
if is_ws {
if !app.accept_ws(&path) {
warn!("No ws there!");
let res_content: Result<&str, core::fmt::Error> = try {
if ws_handshake {
if !app.accept_ws(path) {
write!(
&mut head_buf,
"{}\r\n\r\n",
Into::<&str>::into(HttpResCode::NotFound)
)?;
None
""
} else {
let Ok(accept) = compute_ws_accept(&buf) else {
if path.len() > 16 {
warn!("Ws socket cannot have path longer than 16 chars!");
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",
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: {}\r\n\r\n",
// Sec-WebSocket-Protocol: chat\r\n
Into::<&str>::into(HttpResCode::SwitchingProtocols),
accept
)?;
None
""
}
} else {
let (code, res_type, res_content) =
app.handle_request(&path, request_type, &buf).await;
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,
};
write!(&mut head_buf, "{}", Into::<&str>::into(code))?;
if let Some(ref c) = res_content {
if res_type.len() > 0 {
write!(
&mut head_buf,
"\r\n\
Content-Type: text/{}\r\n\
Content-Length: {}\r\n",
res_type,
c.len()
res_content.len()
)?;
}
write!(&mut head_buf, "\r\n\r\n")?;
@ -232,37 +219,33 @@ pub async fn listen_task<
}
};
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?;
}
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;
}
};
if let Err(e) = w {
warn!("write error: {:?}", e);
break;
};
if is_ws {
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;
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;
}
}
}
@ -302,22 +285,13 @@ impl Into<&str> for HttpResCode {
}
}
fn compute_ws_accept(key: &str) -> Result<String<28>, ()> {
async fn compute_ws_accept(key: &str) -> Result<String<28>, EncodeSliceError> {
let mut res = Vec::<u8, 28>::new();
res.resize_default(28)?;
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();
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(())
}
}
BASE64_STANDARD.encode_slice(hash, &mut res)?;
Ok(String::from_utf8(res).unwrap())
}

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::ReadReady;
use log::warn;
use pico_website::{assert, unwrap, unwrap_opt};
use embedded_io_async::{ErrorType, ReadReady, Write};
use heapless::Vec;
use log::{info, warn};
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy)]
pub enum WsMsg<'a> {
Ping(&'a [u8]),
Pong(&'a [u8]),
@ -15,10 +15,6 @@ 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()
}
@ -42,132 +38,78 @@ impl WsMsg<'_> {
struct WsRx<'a, const BUF_SIZE: usize> {
socket: TcpReader<'a>,
buf: &'a mut [u8; BUF_SIZE],
msg_in_buf: Option<(usize, usize)>, // (start, length)
last_msg: Instant,
}
struct WsTx<'a> {
struct WsTx<'a, const HEAD_BUF_SIZE: usize> {
socket: TcpWriter<'a>,
head_buf: &'a mut Vec<u8, HEAD_BUF_SIZE>,
}
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 {
Ok(())
}
}
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(|_| ())?
}
impl<'a, const HEAD_BUF_SIZE: usize> WsTx<'a, HEAD_BUF_SIZE> {
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())
}
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();
} 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();
}
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);
()
})
.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> {
pub struct Ws<'a, const BUF_SIZE: usize = 1024, const RES_HEAD_BUF_SIZE: usize = 256> {
rx: WsRx<'a, BUF_SIZE>,
tx: WsTx<'a>,
pub last_msg: Instant,
tx: WsTx<'a, RES_HEAD_BUF_SIZE>,
name: &'a str,
}
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 {
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 {
let (rx, tx) = socket.split();
Self {
rx: WsRx {
socket: rx,
buf,
msg_in_buf: None,
last_msg: Instant::MIN,
},
tx: WsTx {
socket: tx,
head_buf,
},
tx: WsTx { socket: tx },
last_msg: Instant::MIN,
name,
}
}
/// 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
}
// 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(());
}
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);
}
Ok(n) => n,
Err(e) => {
warn!("Socket {}: read error: {:?}", self.name, e);
return Err(());
}
};
if self.rx.buf[0] & 0b1000_0000 == 0 {
warn!("Fragmented ws messages are not supported!");
return Err(());
@ -208,13 +150,7 @@ impl<'a, const BUF_SIZE: usize> Ws<'a, BUF_SIZE> {
.iter_mut()
.enumerate()
{
*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))),
));
*x ^= mask_key[i & 0xff];
}
&self.rx.buf[n_after_length + 4..n_after_length + 4 + length as usize]
} else {
@ -222,23 +158,16 @@ impl<'a, const BUF_SIZE: usize> Ws<'a, BUF_SIZE> {
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.last_msg = Instant::now();
self.rx.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?;
@ -247,37 +176,12 @@ impl<'a, const BUF_SIZE: usize> Ws<'a, BUF_SIZE> {
// Pong
10 => Ok(Some(WsMsg::Pong(&content))),
c => {
warn!("Unknown ws op code (ignoring) : {}", c);
info!("Unknown ws op code (ignoring) : {}", c);
Ok(Some(WsMsg::Unknown(c, &content)))
}
}
}
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(())
pub async fn send(&mut self, msg: WsMsg<'a>) -> Result<(), ()> {
self.tx.send(msg).await
}
}

View File

@ -1 +0,0 @@
<!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>

View File

@ -1 +0,0 @@
<!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
View File

@ -1 +0,0 @@
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=""}};