Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
f136f55266 | |||
b50300fbbb | |||
![]() |
6c57c3aaaf | ||
![]() |
bfdf18c1da | ||
![]() |
21e6a50e36 | ||
![]() |
4521697f48 | ||
![]() |
f523ec812c | ||
![]() |
82c86bba16 | ||
![]() |
c629ae5ce3 | ||
![]() |
1c0108cefa | ||
![]() |
e6bc8561d4 | ||
![]() |
b90c978a38 | ||
![]() |
f085ceba1e | ||
![]() |
6bc4d823b7 | ||
![]() |
937aafb099 | ||
![]() |
c0402e0dca |
@ -1,8 +1,8 @@
|
|||||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||||
runner = "elf2uf2-rs -d -s -t"
|
runner = "sudo /home/arkitu/.cargo/bin/elf2uf2-rs -d -s"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
DEFMT_LOG = "debug"
|
DEFMT_LOG = "debug"
|
||||||
|
41
Cargo.lock
generated
41
Cargo.lock
generated
@ -68,6 +68,12 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -348,6 +354,12 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dnsparse"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18b5892f4beae62ac9681eb96df926ebcebca009e6f2e23a216acf7d3f5c5a97"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@ -1057,6 +1069,12 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1086,6 +1104,7 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
|||||||
name = "pico-website"
|
name = "pico-website"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
"cortex-m-rt",
|
"cortex-m-rt",
|
||||||
"cyw43",
|
"cyw43",
|
||||||
@ -1093,19 +1112,24 @@ dependencies = [
|
|||||||
"defmt",
|
"defmt",
|
||||||
"defmt-rtt",
|
"defmt-rtt",
|
||||||
"dhcparse",
|
"dhcparse",
|
||||||
|
"dnsparse",
|
||||||
"embassy-executor",
|
"embassy-executor",
|
||||||
"embassy-net",
|
"embassy-net",
|
||||||
"embassy-rp",
|
"embassy-rp",
|
||||||
|
"embassy-sync",
|
||||||
"embassy-time",
|
"embassy-time",
|
||||||
"embassy-usb-logger",
|
"embassy-usb-logger",
|
||||||
"embedded-io-async",
|
"embedded-io-async",
|
||||||
"heapless",
|
"heapless",
|
||||||
"log",
|
"log",
|
||||||
"panic-probe",
|
"panic-probe",
|
||||||
|
"percent-encoding",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
"ringbuffer",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-json-core",
|
"serde-json-core",
|
||||||
|
"sha1",
|
||||||
"static_cell",
|
"static_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1297,6 +1321,12 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ringbuffer"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rp-pac"
|
name = "rp-pac"
|
||||||
version = "7.0.0"
|
version = "7.0.0"
|
||||||
@ -1398,6 +1428,17 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2-const-stable"
|
name = "sha2-const-stable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
15
Cargo.toml
15
Cargo.toml
@ -7,9 +7,12 @@ edition = "2024"
|
|||||||
wifi-connect = [
|
wifi-connect = [
|
||||||
"dep:serde-json-core",
|
"dep:serde-json-core",
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
] # you need to add a wifi.conf file for this to work
|
] # you need to add a wifi.json file for this to work
|
||||||
dhcp = ["dep:dhcparse"]
|
dhcp = ["dep:dhcparse"]
|
||||||
default = ["dhcp"]
|
dns = ["dep:dnsparse"]
|
||||||
|
chat = ["dep:ringbuffer"]
|
||||||
|
ttt = []
|
||||||
|
default = ["dhcp", "dns"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
|
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
|
||||||
@ -35,6 +38,7 @@ embassy-net = { git = "https://github.com/embassy-rs/embassy", features = [
|
|||||||
"udp",
|
"udp",
|
||||||
"dhcpv4",
|
"dhcpv4",
|
||||||
] }
|
] }
|
||||||
|
embassy-sync = { git = "https://github.com/embassy-rs/embassy" }
|
||||||
cyw43-pio = { git = "https://github.com/embassy-rs/embassy" }
|
cyw43-pio = { git = "https://github.com/embassy-rs/embassy" }
|
||||||
cyw43 = { git = "https://github.com/embassy-rs/embassy" }
|
cyw43 = { git = "https://github.com/embassy-rs/embassy" }
|
||||||
|
|
||||||
@ -54,4 +58,9 @@ serde-json-core = { version = "*", optional = true }
|
|||||||
serde = { version = "*", optional = true, default-features = false, features = [
|
serde = { version = "*", optional = true, default-features = false, features = [
|
||||||
"derive",
|
"derive",
|
||||||
] }
|
] }
|
||||||
dhcparse = { version = "*", default-features = false, optional = true }
|
dhcparse = { version = "*", default-features = false, optional = true }
|
||||||
|
dnsparse = { version = "*", optional = true }
|
||||||
|
ringbuffer = { version = "*", default-features = false, optional = true }
|
||||||
|
percent-encoding = { version = "*", default-features = false }
|
||||||
|
sha1 = { version = "*", default-features = false }
|
||||||
|
base64 = { version = "*", default-features = false }
|
||||||
|
41
src/apps/chat.html
Normal file
41
src/apps/chat.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<head>
|
||||||
|
<script src="./htmx.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
/* #grid {
|
||||||
|
.cell {
|
||||||
|
border: 1px dotted black;
|
||||||
|
padding: 33%;
|
||||||
|
}
|
||||||
|
display: grid;
|
||||||
|
border: 1px solid black;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Chat</h1>
|
||||||
|
<form id="login-page">
|
||||||
|
Enter your name :
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
minlength="3"
|
||||||
|
maxlength="16"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
hx-post="/chat/connect"
|
||||||
|
hx-target="#login-page"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
276
src/apps/chat.rs
Normal file
276
src/apps/chat.rs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
use core::fmt::Write;
|
||||||
|
use dhcparse::dhcpv4::MAX_MESSAGE_SIZE;
|
||||||
|
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
|
||||||
|
use heapless::{String, Vec};
|
||||||
|
use log::{info, warn};
|
||||||
|
use percent_encoding::percent_decode_str;
|
||||||
|
use pico_website::unwrap;
|
||||||
|
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
|
||||||
|
|
||||||
|
use crate::socket::{HttpRequestType, HttpResCode};
|
||||||
|
|
||||||
|
use super::App;
|
||||||
|
|
||||||
|
const MEMORY_SIZE: usize = 16;
|
||||||
|
const USERNAME_MIN_SIZE: usize = 3;
|
||||||
|
const USERNAME_SIZE: usize = 16;
|
||||||
|
const MSG_SIZE: usize = 128;
|
||||||
|
|
||||||
|
static MESSAGES: Mutex<ThreadModeRawMutex, Messages> = Mutex::new(Messages::new());
|
||||||
|
|
||||||
|
pub struct ChatApp {
|
||||||
|
res_buf: String<1100>,
|
||||||
|
}
|
||||||
|
impl ChatApp {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
res_buf: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl App for ChatApp {
|
||||||
|
fn socket_name(&self) -> &'static str {
|
||||||
|
"chat"
|
||||||
|
}
|
||||||
|
async fn handle_request<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
path: &str,
|
||||||
|
req_type: HttpRequestType,
|
||||||
|
content: &str,
|
||||||
|
) -> (HttpResCode, &'static str, &'a 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, "", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
15
src/apps/index.html
Normal file
15
src/apps/index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!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>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
src/apps/index.rs
Normal file
23
src/apps/index.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use crate::socket::{HttpRequestType, HttpResCode};
|
||||||
|
|
||||||
|
use super::App;
|
||||||
|
|
||||||
|
pub struct IndexApp;
|
||||||
|
impl App for IndexApp {
|
||||||
|
fn socket_name(&self) -> &'static str {
|
||||||
|
"index"
|
||||||
|
}
|
||||||
|
async fn handle_request<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
path: &str,
|
||||||
|
_req_type: HttpRequestType,
|
||||||
|
_content: &str,
|
||||||
|
) -> (HttpResCode, &'static str, &'a str) {
|
||||||
|
match path {
|
||||||
|
"/" | "/index" | "/index.html" => {
|
||||||
|
(HttpResCode::Ok, "html", include_str!("./index.html"))
|
||||||
|
}
|
||||||
|
_ => (HttpResCode::NotFound, "", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/apps/mod.rs
Normal file
28
src/apps/mod.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use crate::socket::{HttpRequestType, HttpResCode, ws::Ws};
|
||||||
|
|
||||||
|
#[cfg(feature = "chat")]
|
||||||
|
pub mod chat;
|
||||||
|
pub mod index;
|
||||||
|
#[cfg(feature = "ttt")]
|
||||||
|
pub mod ttt;
|
||||||
|
|
||||||
|
pub trait App {
|
||||||
|
fn socket_name(&self) -> &'static str;
|
||||||
|
async fn handle_request<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
_path: &str,
|
||||||
|
_req_type: HttpRequestType,
|
||||||
|
_content: &str,
|
||||||
|
) -> (HttpResCode, &'static str, &'a str) {
|
||||||
|
(HttpResCode::NotFound, "", "")
|
||||||
|
}
|
||||||
|
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>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,32 @@
|
|||||||
|
use core::fmt::Write;
|
||||||
use core::{ops::Not, sync::atomic::Ordering};
|
use core::{ops::Not, sync::atomic::Ordering};
|
||||||
use embassy_time::{Duration, Instant};
|
use embassy_time::{Duration, Instant};
|
||||||
use heapless::Vec;
|
use heapless::String;
|
||||||
use pico_website::unwrap;
|
use pico_website::unwrap;
|
||||||
use portable_atomic::{AtomicBool, AtomicU32};
|
use portable_atomic::{AtomicBool, AtomicU32};
|
||||||
use core::fmt::Write;
|
|
||||||
|
|
||||||
use crate::socket::HttpResCode;
|
use crate::socket::{HttpRequestType, HttpResCode};
|
||||||
|
|
||||||
|
use super::App;
|
||||||
|
|
||||||
static TURN: AtomicBool = AtomicBool::new(false);
|
static TURN: AtomicBool = AtomicBool::new(false);
|
||||||
// bits [0; 8] : player zero board / bits [9; 17] : player one board
|
// bits [0; 8] : player zero board / bits [9; 17] : player one board
|
||||||
static BOARD: AtomicU32 = AtomicU32::new(0);
|
static BOARD: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
pub struct GameClient {
|
pub struct TttApp {
|
||||||
res_buf: Vec<u8, 4096>,
|
res_buf: String<2048>,
|
||||||
/// State of the board last time it has been sent
|
/// State of the board last time it has been sent
|
||||||
last_board: u32,
|
last_board: u32,
|
||||||
team: Team,
|
team: Team,
|
||||||
end: Option<(Instant, Option<Team>)>
|
end: Option<(Instant, Option<Team>)>,
|
||||||
}
|
}
|
||||||
impl GameClient {
|
impl TttApp {
|
||||||
pub fn new(team: Team) -> Self {
|
pub fn new(team: Team) -> Self {
|
||||||
Self {
|
Self {
|
||||||
res_buf: Vec::new(),
|
res_buf: String::new(),
|
||||||
last_board: 0,
|
last_board: 0,
|
||||||
team,
|
team,
|
||||||
end: None
|
end: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) {
|
pub fn is_ended(&self, board: u32) -> (bool, Option<Team>) {
|
||||||
@ -40,21 +42,21 @@ impl GameClient {
|
|||||||
0b010010010,
|
0b010010010,
|
||||||
0b001001001,
|
0b001001001,
|
||||||
0b100010001,
|
0b100010001,
|
||||||
0b001010100
|
0b001010100,
|
||||||
] {
|
] {
|
||||||
if board & (w<<m) == (w<<m) {
|
if board & (w << m) == (w << m) {
|
||||||
return (true, Some(t))
|
return (true, Some(t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((board | (board>>9)) & 0b111111111) == 0b111111111 {
|
if ((board | (board >> 9)) & 0b111111111) == 0b111111111 {
|
||||||
return (true, None)
|
return (true, None);
|
||||||
}
|
}
|
||||||
(false, None)
|
(false, None)
|
||||||
}
|
}
|
||||||
pub fn update_end_state(&mut self, board: &mut u32) {
|
pub fn update_end_state(&mut self, board: &mut u32) {
|
||||||
if let Some((i, _)) = self.end {
|
if let Some((i, _)) = self.end {
|
||||||
if i+Duration::from_secs(7) < Instant::now() {
|
if i + Duration::from_secs(7) < Instant::now() {
|
||||||
self.end = None;
|
self.end = None;
|
||||||
BOARD.store(0, Ordering::Release);
|
BOARD.store(0, Ordering::Release);
|
||||||
*board = 0;
|
*board = 0;
|
||||||
@ -66,43 +68,50 @@ impl GameClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Generate board html
|
/// Generate board html
|
||||||
pub async fn generate_board_res<'a>(&'a mut self, board: u32, turn: Team, outer_html: bool) -> &'a [u8] {
|
async fn generate_board_res<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
board: u32,
|
||||||
|
turn: Team,
|
||||||
|
outer_html: bool,
|
||||||
|
) -> &'a str {
|
||||||
self.res_buf.clear();
|
self.res_buf.clear();
|
||||||
if outer_html {
|
if outer_html {
|
||||||
unwrap(self.res_buf.extend_from_slice(
|
unwrap(self.res_buf.push_str(
|
||||||
b"<div \
|
"<div \
|
||||||
id=\"game\" \
|
id=\"game\" \
|
||||||
hx-get=\"/ttt/game\" \
|
hx-get=\"/ttt/game\" \
|
||||||
hx-swap=\"innerHTML\" \
|
hx-swap=\"innerHTML\" \
|
||||||
hx-trigger=\"every 100ms\" \
|
hx-trigger=\"every 100ms\" \
|
||||||
hx-target=\"this\"\
|
hx-target=\"this\"\
|
||||||
>"
|
>",
|
||||||
)).await;
|
))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
unwrap(write!(
|
unwrap(write!(
|
||||||
self.res_buf,
|
self.res_buf,
|
||||||
"<h3>Team : <span style=\"color:{}\">{}</span></h3>",
|
"<h3>Team : <span style=\"color:{}\">{}</span></h3>",
|
||||||
self.team.color(),
|
self.team.color(),
|
||||||
self.team.name()
|
self.team.name()
|
||||||
)).await;
|
))
|
||||||
|
.await;
|
||||||
match self.end {
|
match self.end {
|
||||||
Some((_, Some(t))) => unwrap(write!(
|
Some((_, Some(t))) => {
|
||||||
|
unwrap(write!(
|
||||||
self.res_buf,
|
self.res_buf,
|
||||||
"<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>",
|
"<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>",
|
||||||
t.color(),
|
t.color(),
|
||||||
t.name()
|
t.name()
|
||||||
)).await,
|
))
|
||||||
Some((_, None)) => unwrap(write!(
|
.await
|
||||||
self.res_buf,
|
}
|
||||||
"<br><h3>Draw!</h3><br>",
|
Some((_, None)) => unwrap(write!(self.res_buf, "<br><h3>Draw!</h3><br>",)).await,
|
||||||
)).await,
|
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
unwrap(self.res_buf.extend_from_slice(b"<div id=\"grid\">")).await;
|
unwrap(self.res_buf.push_str("<div id=\"grid\">")).await;
|
||||||
for c in 0..=8 {
|
for c in 0..=8 {
|
||||||
let picked_by = if board & (1<<c) != 0 {
|
let picked_by = if board & (1 << c) != 0 {
|
||||||
Some(Team::Zero)
|
Some(Team::Zero)
|
||||||
} else if board & (1<<(9 + c)) != 0 {
|
} else if board & (1 << (9 + c)) != 0 {
|
||||||
Some(Team::One)
|
Some(Team::One)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -113,66 +122,90 @@ impl GameClient {
|
|||||||
self.res_buf,
|
self.res_buf,
|
||||||
"<div class=\"cell\" style=\"background-color:{}\"></div>",
|
"<div class=\"cell\" style=\"background-color:{}\"></div>",
|
||||||
t.color()
|
t.color()
|
||||||
)).await;
|
))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
None => if self.team == turn.into() && self.end.is_none() {
|
None => {
|
||||||
unwrap(write!(
|
if self.team == turn.into() && self.end.is_none() {
|
||||||
|
unwrap(write!(
|
||||||
self.res_buf,
|
self.res_buf,
|
||||||
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>",
|
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>",
|
||||||
c
|
c
|
||||||
)).await;
|
)).await;
|
||||||
} else {
|
} else {
|
||||||
unwrap(self.res_buf.extend_from_slice(
|
unwrap(self.res_buf.push_str("<div class=\"cell\"></div>")).await;
|
||||||
b"<div class=\"cell\"></div>",
|
}
|
||||||
)).await;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
unwrap(self.res_buf.extend_from_slice(b"</div>")).await;
|
unwrap(self.res_buf.push_str("</div>")).await;
|
||||||
if outer_html {
|
if outer_html {
|
||||||
unwrap(self.res_buf.extend_from_slice(b"</div>")).await;
|
unwrap(self.res_buf.push_str("</div>")).await;
|
||||||
}
|
}
|
||||||
&self.res_buf
|
&self.res_buf
|
||||||
}
|
}
|
||||||
pub async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) {
|
}
|
||||||
if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/game" {
|
|
||||||
let mut board = BOARD.load(Ordering::Acquire);
|
|
||||||
let mut turn = TURN.load(Ordering::Acquire);
|
|
||||||
|
|
||||||
// just return correct board in case of unauthorized move
|
impl App for TttApp {
|
||||||
if path.starts_with("/ttt/cell") && self.team == turn.into() {
|
fn socket_name(&self) -> &'static str {
|
||||||
let clicked_c: Cell = match TryInto::<Cell>::try_into(
|
self.team.name()
|
||||||
unwrap(path.chars().nth(9).ok_or("no 9th char")).await,
|
}
|
||||||
) {
|
async fn handle_request<'a>(
|
||||||
Ok(c) => c,
|
&'a mut self,
|
||||||
Err(_) => return (HttpResCode::NotFound, "", &[]),
|
path: &str,
|
||||||
};
|
_req_type: HttpRequestType,
|
||||||
if board
|
_content: &str,
|
||||||
& (
|
) -> (HttpResCode, &'static str, &'a str) {
|
||||||
(1<<(clicked_c as u32)) + (1<<(9 + clicked_c as u32))
|
match path {
|
||||||
)
|
"/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => {
|
||||||
!= 0
|
(HttpResCode::Ok, "html", include_str!("./ttt.html"))
|
||||||
{
|
}
|
||||||
return (HttpResCode::Forbidden, "", &[]);
|
"/ttt/initial_game" => {
|
||||||
|
let board = BOARD.load(Ordering::Acquire);
|
||||||
|
let turn = TURN.load(Ordering::Acquire);
|
||||||
|
(
|
||||||
|
HttpResCode::Ok,
|
||||||
|
"html",
|
||||||
|
self.generate_board_res(board, turn.into(), true).await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
path => {
|
||||||
|
if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/game" {
|
||||||
|
let mut board = BOARD.load(Ordering::Acquire);
|
||||||
|
let mut turn = TURN.load(Ordering::Acquire);
|
||||||
|
|
||||||
|
// just return correct board in case of unauthorized move
|
||||||
|
if path.starts_with("/ttt/cell") && self.team == turn.into() {
|
||||||
|
let clicked_c: Cell = match TryInto::<Cell>::try_into(
|
||||||
|
unwrap(path.chars().nth(9).ok_or("no 9th char")).await,
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return (HttpResCode::NotFound, "", ""),
|
||||||
|
};
|
||||||
|
if board & ((1 << (clicked_c as u32)) + (1 << (9 + clicked_c as u32))) != 0
|
||||||
|
{
|
||||||
|
return (HttpResCode::Forbidden, "", "");
|
||||||
|
}
|
||||||
|
board = board | (1 << ((self.team as u32 * 9) + clicked_c as u32));
|
||||||
|
turn = (!self.team).into();
|
||||||
|
BOARD.store(board, Ordering::Release);
|
||||||
|
TURN.store(turn, Ordering::Release);
|
||||||
|
}
|
||||||
|
self.update_end_state(&mut board);
|
||||||
|
if self.last_board != board {
|
||||||
|
self.last_board = board;
|
||||||
|
(
|
||||||
|
HttpResCode::Ok,
|
||||||
|
"html",
|
||||||
|
self.generate_board_res(board, turn.into(), false).await,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(HttpResCode::NoContent, "", "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(HttpResCode::NotFound, "", "")
|
||||||
}
|
}
|
||||||
board = board | (1<<((self.team as u32 * 9) + clicked_c as u32));
|
|
||||||
turn = (!self.team).into();
|
|
||||||
BOARD.store(board, Ordering::Release);
|
|
||||||
TURN.store(turn, Ordering::Release);
|
|
||||||
}
|
}
|
||||||
self.update_end_state(&mut board);
|
|
||||||
if self.last_board != board {
|
|
||||||
self.last_board = board;
|
|
||||||
(HttpResCode::Ok, "html", self.generate_board_res(board, turn.into(), false).await)
|
|
||||||
} else {
|
|
||||||
(HttpResCode::NoContent, "", &[])
|
|
||||||
}
|
|
||||||
} else if path == "/ttt/initial_game" {
|
|
||||||
let board = BOARD.load(Ordering::Acquire);
|
|
||||||
let turn = TURN.load(Ordering::Acquire);
|
|
||||||
(HttpResCode::Ok, "html", self.generate_board_res(board, turn.into(), true).await)
|
|
||||||
} else {
|
|
||||||
(HttpResCode::NotFound, "", &[])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,7 +224,7 @@ impl Into<bool> for Team {
|
|||||||
fn into(self) -> bool {
|
fn into(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Team::Zero => false,
|
Team::Zero => false,
|
||||||
Team::One => true
|
Team::One => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,7 +233,7 @@ impl Not for Team {
|
|||||||
fn not(self) -> Self::Output {
|
fn not(self) -> Self::Output {
|
||||||
match self {
|
match self {
|
||||||
Team::Zero => Team::One,
|
Team::Zero => Team::One,
|
||||||
Team::One => Team::Zero
|
Team::One => Team::Zero,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,13 +241,13 @@ impl Team {
|
|||||||
fn color(self) -> &'static str {
|
fn color(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Team::Zero => "dodgerblue",
|
Team::Zero => "dodgerblue",
|
||||||
Team::One => "firebrick"
|
Team::One => "firebrick",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn name(self) -> &'static str {
|
fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Team::Zero => "blue",
|
Team::Zero => "blue",
|
||||||
Team::One => "red"
|
Team::One => "red",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,4 +297,4 @@ impl TryFrom<u8> for Cell {
|
|||||||
_ => return Err(()),
|
_ => return Err(()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/dhcp.rs
16
src/dhcp.rs
@ -149,15 +149,15 @@ async fn write_dhcp_opts<const N: usize>(buf: &mut Vec<u8, N>, op_codes: &[u8])
|
|||||||
1 => (4, &[255, 255, 255, 0]), // DhcpOption::SubnetMask(&dhcpv4::Addr([255, 255, 255, 0])),
|
1 => (4, &[255, 255, 255, 0]), // DhcpOption::SubnetMask(&dhcpv4::Addr([255, 255, 255, 0])),
|
||||||
2 => (4, &3600_i32.to_be_bytes()), // DhcpOption::TimeOffset(3600),
|
2 => (4, &3600_i32.to_be_bytes()), // DhcpOption::TimeOffset(3600),
|
||||||
3 => (4, &[192, 254, 0, 2]), // DhcpOption::Router(&[dhcpv4::Addr([192, 254, 0, 2])]),
|
3 => (4, &[192, 254, 0, 2]), // DhcpOption::Router(&[dhcpv4::Addr([192, 254, 0, 2])]),
|
||||||
6 => (4, &[0, 0, 0, 0]), // DhcpOption::DomainNameServer(&[dhcpv4::Addr([0, 0, 0, 0])]),
|
6 => (4, &[192, 254, 0, 2]), // DhcpOption::DomainNameServer(&[dhcpv4::Addr([0, 0, 0, 0])]),
|
||||||
12 => (4, b"blue"), // DhcpOption::HostName(b"blue"),
|
12 => (4, b"blue"), // DhcpOption::HostName(b"blue"),
|
||||||
15 => (11, b"LocalDomain"), // DhcpOption::DomainName(b"LocalDomain"),
|
15 => (4, b"wifi"), // DhcpOption::DomainName(b"LocalDomain"),
|
||||||
26 => (2, &1514_u16.to_be_bytes()), // DhcpOption::Unknown(26, &[0x5, 0xEA]), // mtu
|
26 => (2, &1514_u16.to_be_bytes()), // mtu
|
||||||
28 => (4, &[192, 254, 0, 255]), // DhcpOption::Unknown(28, &[192, 254, 0, 255]), // broadcast
|
28 => (4, &[192, 254, 0, 255]), // broadcast
|
||||||
51 => (4, &700_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(700),
|
51 => (4, &3600_u32.to_be_bytes()), // DhcpOption::AddressLeaseTime(3600),
|
||||||
54 => (4, &[192, 254, 0, 2]), // DhcpOption::ServerIdentifier(&dhcpv4::Addr([192, 254, 0, 2])),
|
54 => (4, &[192, 254, 0, 2]), // DhcpOption::ServerIdentifier(&dhcpv4::Addr([192, 254, 0, 2])),
|
||||||
58 => (4, &500_u32.to_be_bytes()), // DhcpOption::Unknown(58, &[0, 0, 0x1, 0xF4]), // renewal time = 500s
|
58 => (4, &3400_u32.to_be_bytes()), // renewal time
|
||||||
59 => (4, &600_u32.to_be_bytes()), // DhcpOption::Unknown(59, &[0, 0, 0x2, 0x58]), // rebinding time = 600s
|
59 => (4, &3500_u32.to_be_bytes()), // rebinding time
|
||||||
80 => (0, &[]),
|
80 => (0, &[]),
|
||||||
_ => {
|
_ => {
|
||||||
info!("Dhcp: unhandled requested option {}", o);
|
info!("Dhcp: unhandled requested option {}", o);
|
||||||
|
88
src/dns.rs
Normal file
88
src/dns.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use dnsparse::{Answer, Header, HeaderKind, Message, OpCode, QueryClass, QueryKind, ResponseCode};
|
||||||
|
use embassy_net::{
|
||||||
|
Stack,
|
||||||
|
udp::{PacketMetadata, UdpSocket},
|
||||||
|
};
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use log::{info, warn};
|
||||||
|
use pico_website::unwrap;
|
||||||
|
|
||||||
|
#[embassy_executor::task(pool_size = 1)]
|
||||||
|
pub async fn dns_server(stack: Stack<'static>) {
|
||||||
|
let mut rx_buffer = [0; 4096];
|
||||||
|
let mut tx_buffer = [0; 4096];
|
||||||
|
let mut rx_meta = [PacketMetadata::EMPTY; 16];
|
||||||
|
let mut tx_meta = [PacketMetadata::EMPTY; 16];
|
||||||
|
let mut buf = [0; 4096];
|
||||||
|
let mut res_buf = [0; 2048];
|
||||||
|
loop {
|
||||||
|
let mut socket = UdpSocket::new(
|
||||||
|
stack,
|
||||||
|
&mut rx_meta,
|
||||||
|
&mut rx_buffer,
|
||||||
|
&mut tx_meta,
|
||||||
|
&mut tx_buffer,
|
||||||
|
);
|
||||||
|
unwrap(socket.bind(53)).await;
|
||||||
|
|
||||||
|
info!("Starting DNS server");
|
||||||
|
loop {
|
||||||
|
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]) {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Dns: Error while parsing DNS message : {:#?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if msg.header().opcode() != OpCode::Query {
|
||||||
|
info!(
|
||||||
|
"Dns: Received unknown dns opcode ({:?}), ignoring",
|
||||||
|
msg.header().opcode()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut res = Message::builder(&mut res_buf)
|
||||||
|
.header(
|
||||||
|
Header::builder()
|
||||||
|
.id(msg.header().id())
|
||||||
|
.kind(HeaderKind::Response)
|
||||||
|
.recursion_available(false)
|
||||||
|
.recursion_desired(msg.header().recursion_desired())
|
||||||
|
.response_code(ResponseCode::NoError)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
for q in msg.questions() {
|
||||||
|
match q.kind() {
|
||||||
|
QueryKind::A => {
|
||||||
|
if q.name() == "pico.wifi" || q.name() == "www.pico.wifi" {
|
||||||
|
res.add_question(&q);
|
||||||
|
res.add_answer(&Answer {
|
||||||
|
name: q.name().clone(),
|
||||||
|
kind: QueryKind::A,
|
||||||
|
class: QueryClass::IN,
|
||||||
|
ttl: 600,
|
||||||
|
rdata: &[192, 254, 0, 2],
|
||||||
|
});
|
||||||
|
info!("Dns: Giving {}", q.name());
|
||||||
|
} else {
|
||||||
|
info!("Dns: Unknown uri, ignoring ({})", q.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = socket.send_to(res.as_bytes(), meta).await {
|
||||||
|
warn!("Dns: Error while sending dns response : {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/main.rs
41
src/main.rs
@ -3,6 +3,10 @@
|
|||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
#![feature(slice_split_once)]
|
#![feature(slice_split_once)]
|
||||||
|
#![feature(try_blocks)]
|
||||||
|
|
||||||
|
#[cfg(feature = "wifi-connect")]
|
||||||
|
use core::net::Ipv4Addr;
|
||||||
|
|
||||||
use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi};
|
use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi};
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
@ -14,6 +18,7 @@ use embassy_rp::peripherals::USB;
|
|||||||
use embassy_rp::peripherals::{DMA_CH0, PIO0};
|
use embassy_rp::peripherals::{DMA_CH0, PIO0};
|
||||||
use embassy_rp::pio::{InterruptHandler as PioInterruptHandler, Pio};
|
use embassy_rp::pio::{InterruptHandler as PioInterruptHandler, Pio};
|
||||||
use embassy_rp::usb::{Driver, InterruptHandler as UsbInterruptHandler};
|
use embassy_rp::usb::{Driver, InterruptHandler as UsbInterruptHandler};
|
||||||
|
use log::info;
|
||||||
use pico_website::unwrap;
|
use pico_website::unwrap;
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
@ -22,7 +27,10 @@ use {defmt_rtt as _, panic_probe as _};
|
|||||||
#[cfg(feature = "dhcp")]
|
#[cfg(feature = "dhcp")]
|
||||||
mod dhcp;
|
mod dhcp;
|
||||||
|
|
||||||
mod game;
|
#[cfg(feature = "dns")]
|
||||||
|
mod dns;
|
||||||
|
|
||||||
|
mod apps;
|
||||||
mod socket;
|
mod socket;
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
@ -54,6 +62,13 @@ async fn main(spawner: Spawner) {
|
|||||||
spawner.spawn(logger_task(driver)).unwrap();
|
spawner.spawn(logger_task(driver)).unwrap();
|
||||||
let mut rng = RoscRng;
|
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 fw = include_bytes!("../cyw43-firmware/43439A0.bin");
|
||||||
let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin");
|
let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin");
|
||||||
let pwr = Output::new(p.PIN_23, Level::Low);
|
let pwr = Output::new(p.PIN_23, Level::Low);
|
||||||
@ -103,7 +118,7 @@ async fn main(spawner: Spawner) {
|
|||||||
dns_servers: heapless::Vec::new(),
|
dns_servers: heapless::Vec::new(),
|
||||||
gateway: None,
|
gateway: None,
|
||||||
}),
|
}),
|
||||||
None => Config::dhcpv4(DhcpConfig::default()),
|
None => Config::dhcpv4(embassy_net::DhcpConfig::default()),
|
||||||
};
|
};
|
||||||
(wifi_conf, config)
|
(wifi_conf, config)
|
||||||
};
|
};
|
||||||
@ -112,7 +127,7 @@ async fn main(spawner: Spawner) {
|
|||||||
let seed = rng.next_u64();
|
let seed = rng.next_u64();
|
||||||
|
|
||||||
// Init network stack
|
// Init network stack
|
||||||
static RESOURCES: StaticCell<StackResources<4>> = StaticCell::new();
|
static RESOURCES: StaticCell<StackResources<20>> = StaticCell::new();
|
||||||
let (stack, runner) = embassy_net::new(
|
let (stack, runner) = embassy_net::new(
|
||||||
net_device,
|
net_device,
|
||||||
config,
|
config,
|
||||||
@ -123,8 +138,8 @@ async fn main(spawner: Spawner) {
|
|||||||
unwrap(spawner.spawn(net_task(runner))).await;
|
unwrap(spawner.spawn(net_task(runner))).await;
|
||||||
|
|
||||||
#[cfg(not(feature = "wifi-connect"))]
|
#[cfg(not(feature = "wifi-connect"))]
|
||||||
//control.start_ap_open("cyw43", 5).await;
|
control.start_ap_open("pico", 5).await;
|
||||||
control.start_ap_wpa2("cyw43", "password", 5).await;
|
// control.start_ap_wpa2("pico", "password", 5).await;
|
||||||
|
|
||||||
#[cfg(feature = "wifi-connect")]
|
#[cfg(feature = "wifi-connect")]
|
||||||
{
|
{
|
||||||
@ -163,8 +178,20 @@ async fn main(spawner: Spawner) {
|
|||||||
#[cfg(feature = "dhcp")]
|
#[cfg(feature = "dhcp")]
|
||||||
unwrap(spawner.spawn(dhcp::dhcp_server(stack))).await;
|
unwrap(spawner.spawn(dhcp::dhcp_server(stack))).await;
|
||||||
|
|
||||||
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::Zero, 80))).await;
|
#[cfg(feature = "dns")]
|
||||||
unwrap(spawner.spawn(socket::listen_task(stack, game::Team::One, 81))).await;
|
unwrap(spawner.spawn(dns::dns_server(stack))).await;
|
||||||
|
|
||||||
|
unwrap(spawner.spawn(socket::index_listen_task(stack, 80))).await;
|
||||||
|
#[cfg(feature = "ttt")]
|
||||||
|
{
|
||||||
|
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::Zero, 8080))).await;
|
||||||
|
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "chat")]
|
||||||
|
for _ in 0..4 {
|
||||||
|
unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await;
|
||||||
|
}
|
||||||
|
info!("All apps lauched!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wifi-connect")]
|
#[cfg(feature = "wifi-connect")]
|
||||||
|
271
src/socket.rs
271
src/socket.rs
@ -1,41 +1,57 @@
|
|||||||
|
use base64::{EncodeSliceError, prelude::*};
|
||||||
use core::str::from_utf8;
|
use core::str::from_utf8;
|
||||||
|
use core::{fmt::Write, str::FromStr};
|
||||||
use embassy_net::tcp::TcpSocket;
|
use embassy_net::tcp::TcpSocket;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use heapless::Vec;
|
|
||||||
use log::{info, warn};
|
|
||||||
use pico_website::unwrap;
|
|
||||||
use embedded_io_async::Write as _;
|
use embedded_io_async::Write as _;
|
||||||
use core::fmt::Write;
|
use heapless::{String, Vec};
|
||||||
|
use log::{info, warn};
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
|
||||||
use crate::game::{GameClient, Team};
|
use crate::{apps, socket::ws::Ws};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
#[embassy_executor::task(pool_size = 2)]
|
#[embassy_executor::task(pool_size = 2)]
|
||||||
pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u16) {
|
pub async fn index_listen_task(stack: embassy_net::Stack<'static>, port: u16) {
|
||||||
|
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>, port: u16) {
|
||||||
|
listen_task(stack, apps::chat::ChatApp::new(), port).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps::App, port: u16) {
|
||||||
// loop {
|
// loop {
|
||||||
// info!("team:{:?}", team);
|
// info!("team:{:?}", team);
|
||||||
// Timer::after_millis(0).await;
|
// Timer::after_millis(0).await;
|
||||||
// }
|
// }
|
||||||
let mut rx_buffer = [0; 4096];
|
let mut rx_buffer = [0; 1024];
|
||||||
let mut tx_buffer = [0; 4096];
|
let mut tx_buffer = [0; 2048];
|
||||||
let mut buf = [0; 4096];
|
let mut buf = [0; 1024];
|
||||||
let mut res_head_buf = Vec::<u8, 4096>::new();
|
let mut head_buf = Vec::<u8, 256>::new();
|
||||||
|
|
||||||
let mut game_client = GameClient::new(team);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
Timer::after_secs(0).await;
|
Timer::after_secs(0).await;
|
||||||
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
||||||
socket.set_timeout(Some(Duration::from_secs(30)));
|
socket.set_timeout(Some(Duration::from_secs(30)));
|
||||||
|
|
||||||
info!("Socket {:?}: Listening on TCP:{}...", team, port);
|
info!("Socket {}: Listening on TCP:{}...", app.socket_name(), port);
|
||||||
if let Err(e) = socket.accept(port).await {
|
if let Err(e) = socket.accept(port).await {
|
||||||
warn!("accept error: {:?}", e);
|
warn!("accept error: {:?}", e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Socket {:?}: Received connection from {:?}",
|
"Socket {}: Received connection from {:?}",
|
||||||
team,
|
app.socket_name(),
|
||||||
socket.remote_endpoint()
|
socket.remote_endpoint()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -48,36 +64,38 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
|
|||||||
}
|
}
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Socket {:?}: read error: {:?}", team, e);
|
warn!("Socket {}: read error: {:?}", app.socket_name(), e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut headers: &[u8] = &buf[..n];
|
info!("\n{:?}\n", headers);
|
||||||
let mut _content: &[u8] = &[];
|
|
||||||
for i in 0..(n - 1) {
|
|
||||||
if &buf[i..i + 1] == b"\r\n" {
|
|
||||||
headers = &buf[0..i];
|
|
||||||
if i + 2 < n {
|
|
||||||
_content = &buf[i + 2..n];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut headers = headers.split(|x| *x == b'\n');
|
let mut hl = headers.lines();
|
||||||
let (request_type, path) = match headers.next() {
|
let (request_type, path) = match hl.next() {
|
||||||
None => {
|
None => {
|
||||||
warn!("Empty request");
|
warn!("Empty request");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Some(l1) => {
|
Some(l1) => {
|
||||||
let mut l1 = l1.split(|x| *x == b' ');
|
let mut l1 = l1.split(' ');
|
||||||
(
|
(
|
||||||
match l1.next() {
|
match l1.next() {
|
||||||
Some(b"GET") => HttpRequestType::Get,
|
Some("GET") => HttpRequestType::Get,
|
||||||
Some(b"POST") => HttpRequestType::Post,
|
Some("POST") => HttpRequestType::Post,
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
warn!("Unknown request type : {}", unwrap(from_utf8(t)).await);
|
warn!("Unknown request type : {}", t);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@ -86,7 +104,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
match l1.next() {
|
match l1.next() {
|
||||||
Some(path) => unwrap(from_utf8(path)).await,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
warn!("No path");
|
warn!("No path");
|
||||||
break;
|
break;
|
||||||
@ -95,64 +113,144 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
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!(
|
info!(
|
||||||
"Socket {:?}: {:?} request for {}",
|
"Socket {}: {:?}{} request for {}{}",
|
||||||
team,
|
app.socket_name(),
|
||||||
request_type,
|
request_type,
|
||||||
path
|
if ws_handshake { " websocket" } else { "" },
|
||||||
|
host,
|
||||||
|
path,
|
||||||
);
|
);
|
||||||
Timer::after_secs(0).await;
|
Timer::after_secs(0).await;
|
||||||
|
|
||||||
let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path {
|
head_buf.clear();
|
||||||
"/" => (HttpResCode::Ok, "html", include_bytes!("../static/index.html")),
|
let res_content: Result<&str, core::fmt::Error> = try {
|
||||||
"/htmx.min.js" => (
|
if ws_handshake {
|
||||||
HttpResCode::Ok,
|
if !app.accept_ws(path) {
|
||||||
"javascript",
|
write!(
|
||||||
include_bytes!("../static/htmx.min.js"),
|
&mut head_buf,
|
||||||
),
|
"{}\r\n\r\n",
|
||||||
"/htmx.js" => (
|
Into::<&str>::into(HttpResCode::NotFound)
|
||||||
HttpResCode::Ok,
|
)?;
|
||||||
"javascript",
|
""
|
||||||
include_bytes!("../static/htmx.js"),
|
} else {
|
||||||
),
|
if path.len() > 16 {
|
||||||
p => game_client.handle_request(p).await
|
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",
|
||||||
|
// Sec-WebSocket-Protocol: chat\r\n
|
||||||
|
Into::<&str>::into(HttpResCode::SwitchingProtocols),
|
||||||
|
accept
|
||||||
|
)?;
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(&mut head_buf, "{}", Into::<&str>::into(code))?;
|
||||||
|
if res_type.len() > 0 {
|
||||||
|
write!(
|
||||||
|
&mut head_buf,
|
||||||
|
"\r\n\
|
||||||
|
Content-Type: text/{}\r\n\
|
||||||
|
Content-Length: {}\r\n",
|
||||||
|
res_type,
|
||||||
|
res_content.len()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
write!(&mut head_buf, "\r\n\r\n")?;
|
||||||
|
res_content
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res_head_buf.clear();
|
let res_content = match res_content {
|
||||||
if let Err(e) = write!(
|
Ok(rc) => rc,
|
||||||
&mut res_head_buf,
|
Err(e) => {
|
||||||
"{}\r\n\
|
warn!("res buffer write error: {:?}", e);
|
||||||
Content-Type: text/{}\r\n\
|
break;
|
||||||
Content-Length: {}\r\n\r\n",
|
}
|
||||||
Into::<&str>::into(code),
|
};
|
||||||
res_type,
|
|
||||||
res_content.len()
|
info!("\n{}\n", from_utf8(&head_buf).unwrap());
|
||||||
) {
|
|
||||||
warn!("res buffer write error: {:?}", e);
|
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 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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
match socket.write_all(&res_head_buf).await {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("write error: {:?}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match socket.write_all(&res_content).await {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("write error: {:?}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
enum HttpRequestType {
|
pub enum HttpRequestType {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
}
|
}
|
||||||
@ -160,25 +258,40 @@ impl Into<&str> for HttpRequestType {
|
|||||||
fn into(self) -> &'static str {
|
fn into(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Get => "GET",
|
Self::Get => "GET",
|
||||||
Self::Post => "POST"
|
Self::Post => "POST",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum HttpResCode {
|
pub enum HttpResCode {
|
||||||
|
SwitchingProtocols,
|
||||||
Ok,
|
Ok,
|
||||||
NoContent,
|
NoContent,
|
||||||
|
BadRequest,
|
||||||
NotFound,
|
NotFound,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
}
|
}
|
||||||
impl Into<&str> for HttpResCode {
|
impl Into<&str> for HttpResCode {
|
||||||
fn into(self) -> &'static str {
|
fn into(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
HttpResCode::SwitchingProtocols => "HTTP/1.1 101 Switching Protocols",
|
||||||
HttpResCode::Ok => "HTTP/1.1 200 OK",
|
HttpResCode::Ok => "HTTP/1.1 200 OK",
|
||||||
HttpResCode::NoContent => "HTTP/1.1 204 NO CONTENT",
|
HttpResCode::NoContent => "HTTP/1.1 204 NO CONTENT",
|
||||||
|
HttpResCode::BadRequest => "HTTP/1.1 400 BAD REQUEST",
|
||||||
HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND",
|
HttpResCode::NotFound => "HTTP/1.1 404 NOT FOUND",
|
||||||
HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN",
|
HttpResCode::Forbidden => "HTTP/1.1 403 FORBIDDEN",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn compute_ws_accept(key: &str) -> Result<String<28>, EncodeSliceError> {
|
||||||
|
let mut res = Vec::<u8, 28>::new();
|
||||||
|
res.extend_from_slice(&[0; 28]).unwrap();
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(key.as_bytes());
|
||||||
|
hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
BASE64_STANDARD.encode_slice(hash, &mut res)?;
|
||||||
|
Ok(String::from_utf8(res).unwrap())
|
||||||
|
}
|
||||||
|
187
src/socket/ws.rs
Normal file
187
src/socket/ws.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
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};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum WsMsg<'a> {
|
||||||
|
Ping(&'a [u8]),
|
||||||
|
Pong(&'a [u8]),
|
||||||
|
Text(&'a str),
|
||||||
|
Bytes(&'a [u8]),
|
||||||
|
Unknown(u8, &'a [u8]),
|
||||||
|
}
|
||||||
|
impl WsMsg<'_> {
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
self.as_bytes().len()
|
||||||
|
}
|
||||||
|
pub const fn as_bytes(&self) -> &[u8] {
|
||||||
|
match self {
|
||||||
|
WsMsg::Text(t) => t.as_bytes(),
|
||||||
|
WsMsg::Bytes(b) | WsMsg::Pong(b) | WsMsg::Ping(b) | WsMsg::Unknown(_, b) => b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn code(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
WsMsg::Text(_) => 1,
|
||||||
|
WsMsg::Bytes(_) => 2,
|
||||||
|
WsMsg::Ping(_) => 9,
|
||||||
|
WsMsg::Pong(_) => 10,
|
||||||
|
WsMsg::Unknown(c, _) => *c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WsRx<'a, const BUF_SIZE: usize> {
|
||||||
|
socket: TcpReader<'a>,
|
||||||
|
buf: &'a mut [u8; BUF_SIZE],
|
||||||
|
last_msg: Instant,
|
||||||
|
}
|
||||||
|
struct WsTx<'a, const HEAD_BUF_SIZE: usize> {
|
||||||
|
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();
|
||||||
|
} 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);
|
||||||
|
()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Ws<'a, const BUF_SIZE: usize = 1024, const RES_HEAD_BUF_SIZE: usize = 256> {
|
||||||
|
rx: WsRx<'a, BUF_SIZE>,
|
||||||
|
tx: WsTx<'a, RES_HEAD_BUF_SIZE>,
|
||||||
|
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 {
|
||||||
|
let (rx, tx) = socket.split();
|
||||||
|
Self {
|
||||||
|
rx: WsRx {
|
||||||
|
socket: rx,
|
||||||
|
buf,
|
||||||
|
last_msg: Instant::MIN,
|
||||||
|
},
|
||||||
|
tx: WsTx {
|
||||||
|
socket: tx,
|
||||||
|
head_buf,
|
||||||
|
},
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
if self.rx.buf[0] & 0b0111_0000 != 0 {
|
||||||
|
warn!(
|
||||||
|
"Reserved ws bits are set : {}",
|
||||||
|
(self.rx.buf[0] >> 4) & 0b0111
|
||||||
|
);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let (length, n_after_length) = match self.rx.buf[1] & 0b0111_1111 {
|
||||||
|
126 => (
|
||||||
|
u64::from_le_bytes([0, 0, 0, 0, 0, 0, self.rx.buf[2], self.rx.buf[3]]),
|
||||||
|
4,
|
||||||
|
),
|
||||||
|
127 => (
|
||||||
|
u64::from_le_bytes(self.rx.buf[2..10].try_into().unwrap()),
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
l => (l as u64, 2),
|
||||||
|
};
|
||||||
|
if length > 512 {
|
||||||
|
warn!("ws payload bigger than 512!");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = if self.rx.buf[1] & 0b1000_0000 != 0 {
|
||||||
|
// masked message
|
||||||
|
if n_after_length + 4 + length as usize > n {
|
||||||
|
warn!("ws payload smaller than length");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let mask_key: [u8; 4] = self.rx.buf[n_after_length..n_after_length + 4]
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
for (i, x) in self.rx.buf[n_after_length + 4..n_after_length + 4 + length as usize]
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*x ^= mask_key[i & 0xff];
|
||||||
|
}
|
||||||
|
&self.rx.buf[n_after_length + 4..n_after_length + 4 + length as usize]
|
||||||
|
} else {
|
||||||
|
if n_after_length + length as usize > n {
|
||||||
|
warn!("ws payload smaller than length");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
&self.rx.buf[n_after_length..n_after_length + length as usize]
|
||||||
|
};
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
// Ping
|
||||||
|
9 => {
|
||||||
|
self.tx.send(WsMsg::Pong(&content)).await?;
|
||||||
|
Ok(Some(WsMsg::Ping(&content)))
|
||||||
|
}
|
||||||
|
// Pong
|
||||||
|
10 => Ok(Some(WsMsg::Pong(&content))),
|
||||||
|
c => {
|
||||||
|
info!("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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user