save
This commit is contained in:
parent
fc6423ad44
commit
2af8cacc76
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -206,6 +206,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -1061,7 +1067,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"ringbuffer",
|
"ringbuf",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-json-core",
|
"serde-json-core",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -1137,6 +1143,15 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -1257,10 +1272,15 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ringbuffer"
|
name = "ringbuf"
|
||||||
version = "0.15.0"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53"
|
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rp-pac"
|
name = "rp-pac"
|
||||||
|
11
Cargo.toml
11
Cargo.toml
@ -4,15 +4,16 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
json = ["dep:serde-json-core", "dep:serde"]
|
||||||
wifi-connect = [
|
wifi-connect = [
|
||||||
"dep:serde-json-core",
|
"dep:serde-json-core",
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
] # you need to add a wifi.json file for this to work
|
] # you need to add a wifi.json file for this to work
|
||||||
dhcp = ["dep:dhcparse"]
|
dhcp = ["dep:dhcparse"]
|
||||||
dns = ["dep:dnsparse"]
|
dns = ["dep:dnsparse"]
|
||||||
chat = ["dep:ringbuffer"]
|
chat = ["dep:ringbuf", "json"]
|
||||||
ttt = ["dep:serde-json-core", "dep:serde"]
|
ttt = ["json"]
|
||||||
default = ["dhcp", "dns", "ttt"]
|
default = ["dhcp", "dns", "chat"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
|
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
|
||||||
@ -54,7 +55,9 @@ serde = { version = "*", optional = true, default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
dhcparse = { version = "*", default-features = false, optional = true }
|
dhcparse = { version = "*", default-features = false, optional = true }
|
||||||
dnsparse = { version = "*", optional = true }
|
dnsparse = { version = "*", optional = true }
|
||||||
ringbuffer = { version = "*", default-features = false, optional = true }
|
ringbuf = { version = "*", default-features = false, features = [
|
||||||
|
"portable-atomic",
|
||||||
|
], optional = true }
|
||||||
percent-encoding = { version = "*", default-features = false }
|
percent-encoding = { version = "*", default-features = false }
|
||||||
sha1 = { version = "*", default-features = false }
|
sha1 = { version = "*", default-features = false }
|
||||||
base64 = { version = "*", default-features = false }
|
base64 = { version = "*", default-features = false }
|
||||||
|
@ -1,41 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<head>
|
<head>
|
||||||
<script src="./htmx.js"></script>
|
<script src="/chat.js" defer></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>
|
</head>
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<h1>Chat</h1>
|
<h1>Chat</h1>
|
||||||
<form id="login-page">
|
<div id="users"></div>
|
||||||
Enter your name :
|
<div id="msgs"></div>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
26
src/apps/chat.js
Normal file
26
src/apps/chat.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const id = 0;
|
||||||
|
const username = "testName";
|
||||||
|
if (id === undefined) {
|
||||||
|
throw "id is undefined!";
|
||||||
|
}
|
||||||
|
if (username === undefined) {
|
||||||
|
throw "username is undefined!";
|
||||||
|
}
|
||||||
|
const ws = new WebSocket("ws://192.254.0.2:" + (9000 + id) + "/" + username);
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
if (typeof event.data == "string") {
|
||||||
|
let msg = JSON.parse(event.data);
|
||||||
|
if (event.data[0] == "[") {
|
||||||
|
let usernames = [];
|
||||||
|
for (u of msg) {
|
||||||
|
if (u.length() > 0) {
|
||||||
|
let un = document.createElement("div");
|
||||||
|
un.innerText = u;
|
||||||
|
usernames.push(un);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("usernames").replaceChildren(...usernames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
482
src/apps/chat.rs
482
src/apps/chat.rs
@ -1,30 +1,167 @@
|
|||||||
use core::fmt::Write;
|
use core::str::from_utf8_unchecked;
|
||||||
use dhcparse::dhcpv4::MAX_MESSAGE_SIZE;
|
|
||||||
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
|
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex};
|
||||||
use heapless::{String, Vec};
|
use embassy_time::Timer;
|
||||||
|
use heapless::String;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use percent_encoding::percent_decode_str;
|
use pico_website::{unimplemented, unwrap, unwrap_opt};
|
||||||
use pico_website::unwrap;
|
use serde::Serialize;
|
||||||
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
|
|
||||||
|
|
||||||
use crate::socket::{HttpRequestType, HttpResCode};
|
use crate::{apps::App, socket::ws::WsMsg};
|
||||||
|
|
||||||
use super::App;
|
// Must be <= u8::MAX-1;
|
||||||
|
pub const USERS_LEN: u8 = 4;
|
||||||
|
|
||||||
const MEMORY_SIZE: usize = 16;
|
const MSG_MAX_SIZE: usize = 10;
|
||||||
const USERNAME_MIN_SIZE: usize = 3;
|
#[derive(Debug, Serialize)]
|
||||||
const USERNAME_SIZE: usize = 16;
|
struct Msg<'a> {
|
||||||
const MSG_SIZE: usize = 128;
|
id: usize,
|
||||||
|
author: u8,
|
||||||
|
content: &'a str,
|
||||||
|
}
|
||||||
|
// {"id"=999999,"author"="","content"=""}
|
||||||
|
|
||||||
static MESSAGES: Mutex<ThreadModeRawMutex, Messages> = Mutex::new(Messages::new());
|
const MSGS_SIZE: usize = 30;
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Msgs {
|
||||||
|
/// Memory layout with sizes in bytes : ...|content: len|len: 2|author+1: 1|...
|
||||||
|
/// `author=0` means theres no message, it's just padding and should be skipped.
|
||||||
|
/// No message is splitted
|
||||||
|
inner: [u8; MSGS_SIZE],
|
||||||
|
/// next byte index
|
||||||
|
head: usize,
|
||||||
|
next_msg: usize,
|
||||||
|
}
|
||||||
|
impl Msgs {
|
||||||
|
const fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: [0; _],
|
||||||
|
head: 0,
|
||||||
|
next_msg: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn push(&mut self, author: u8, content: &str) {
|
||||||
|
if self.head + content.len() + 3 >= MSGS_SIZE {
|
||||||
|
self.inner[self.head..].fill(0);
|
||||||
|
self.head = 0
|
||||||
|
}
|
||||||
|
self.inner[self.head..self.head + content.len()].copy_from_slice(content.as_bytes());
|
||||||
|
self.head += content.len();
|
||||||
|
self.inner[self.head..self.head + 2].copy_from_slice(&(content.len() as u16).to_le_bytes());
|
||||||
|
self.inner[self.head + 2] = author + 1;
|
||||||
|
self.head += 3;
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if self.current_id == 0 {
|
||||||
|
self.finished = true;
|
||||||
|
} else {
|
||||||
|
self.current_id -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Msg {
|
||||||
|
id: self.current_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();
|
||||||
|
return Some(i as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub static USERNAMES: Mutex<ThreadModeRawMutex, Usernames> = Mutex::new(Usernames::default());
|
||||||
|
|
||||||
pub struct ChatApp {
|
pub struct ChatApp {
|
||||||
res_buf: String<1100>,
|
id: u8,
|
||||||
|
json_buf: [u8; 48 + USERNAME_MAX_LEN + MSG_MAX_SIZE],
|
||||||
}
|
}
|
||||||
impl ChatApp {
|
impl ChatApp {
|
||||||
pub fn new() -> Self {
|
pub fn new(id: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
res_buf: String::new(),
|
id,
|
||||||
|
json_buf: [0; _],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,245 +169,106 @@ impl App for ChatApp {
|
|||||||
fn socket_name(&self) -> &'static str {
|
fn socket_name(&self) -> &'static str {
|
||||||
"chat"
|
"chat"
|
||||||
}
|
}
|
||||||
async fn handle_request<'a>(
|
fn accept_ws(&self, path: &str) -> bool {
|
||||||
|
path.len() > 1 && path.len() <= 17
|
||||||
|
}
|
||||||
|
async fn handle_ws<'a, const BUF_SIZE: usize, const RES_HEAD_BUF_SIZE: usize>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
path: &str,
|
_path: &str,
|
||||||
req_type: HttpRequestType,
|
mut ws: crate::socket::ws::Ws<'a, BUF_SIZE, RES_HEAD_BUF_SIZE>,
|
||||||
content: &str,
|
) {
|
||||||
) -> (HttpResCode, &'static str, &'a str) {
|
Timer::after_millis(500).await;
|
||||||
match (req_type, path) {
|
let r: Result<(), ()> = try {
|
||||||
(HttpRequestType::Get, "/" | "/index" | "/index.html" | "/chat" | "/chat.html") => {
|
// Send all messages at the beginning
|
||||||
(HttpResCode::Ok, "html", include_str!("./chat.html"))
|
{
|
||||||
|
let msgs = MSGS.lock().await;
|
||||||
|
for m in msgs.iter() {
|
||||||
|
let n = unwrap(serde_json_core::to_slice(&m, &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?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let usernames = USERNAMES.lock().await;
|
||||||
|
let n = unwrap(serde_json_core::to_slice(&(*usernames), &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?;
|
||||||
}
|
}
|
||||||
(_, 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) {
|
loop {
|
||||||
Ok(msg) => msg,
|
Timer::after_secs(1).await;
|
||||||
Err(_) => {
|
{
|
||||||
warn!("Invalid utf8 msg argument");
|
let msgs = MSGS.lock().await;
|
||||||
return (HttpResCode::BadRequest, "", "");
|
info!("{:?}", msgs);
|
||||||
}
|
Timer::after_millis(100).await;
|
||||||
});
|
for m in msgs.iter() {
|
||||||
}
|
info!("{:?}", m);
|
||||||
Some(("poll", "true")) => poll = true,
|
Timer::after_millis(100).await;
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!(
|
while let Some(r) = ws.rcv().await? {
|
||||||
"load:{:?} | username:{:?} | msg:{:?}",
|
info!("{:?}", r);
|
||||||
load, username, msg_content
|
if let WsMsg::Text(r) = r {
|
||||||
);
|
if r.starts_with("send ") {
|
||||||
if path == "/chat/connect" && username.is_some() {
|
if r.len() > 5 + MSG_MAX_SIZE {
|
||||||
self.res_buf.clear();
|
warn!("Message too long! (len={})", r.len() - 5);
|
||||||
unwrap(write!(
|
return;
|
||||||
&mut self.res_buf,
|
}
|
||||||
"<input id=\"username\" style=\"display: none;\" name=\"username\" value=\"{}\">
|
MSGS.lock()
|
||||||
<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
|
.await
|
||||||
}
|
.push(self.id, r.get(5..).unwrap_or_default());
|
||||||
None => {
|
}
|
||||||
if load.is_some() {
|
// if r.starts_with("reqmsg ") {
|
||||||
if (msg_id as isize)
|
// let Ok(msg_id) = r.get(7..).unwrap_or_default().parse::<isize>() else {
|
||||||
== (msgs.next as isize - MEMORY_SIZE as isize - 1)
|
// warn!("Invalid requested message : {}", r);
|
||||||
{
|
// return;
|
||||||
unwrap(write!(
|
// };
|
||||||
&mut self.res_buf,
|
// // 0 is next msg
|
||||||
"><em>Older messages forgotten</em></div>"
|
// let msg_rel_id = if msg_id >= 0 {
|
||||||
))
|
|
||||||
.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 {
|
// if msg_id < 0 {
|
||||||
(HttpResCode::NotFound, "", "")
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if r.is_err() {
|
||||||
|
warn!(
|
||||||
|
"Socket {}: error in ws, terminating connection",
|
||||||
|
self.socket_name()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Message {
|
// #[derive(Deserialize)]
|
||||||
author: String<USERNAME_SIZE>,
|
// enum ClientMsg {
|
||||||
content: String<MSG_SIZE>,
|
// ReqMsg(usize),
|
||||||
}
|
// }
|
||||||
|
|
||||||
struct Messages {
|
pub async fn id_to_static_str(id: u8) -> &'static str {
|
||||||
inner: ConstGenericRingBuffer<Message, MEMORY_SIZE>,
|
match id {
|
||||||
next: u16,
|
0 => "0",
|
||||||
}
|
1 => "1",
|
||||||
impl Messages {
|
2 => "2",
|
||||||
const fn new() -> Self {
|
3 => "3",
|
||||||
Self {
|
4 => "4",
|
||||||
inner: ConstGenericRingBuffer::new(),
|
5 => "5",
|
||||||
next: 0,
|
6 => "6",
|
||||||
}
|
7 => "7",
|
||||||
}
|
8 => "8",
|
||||||
fn get_abs(&self, id: u16) -> Option<&Message> {
|
9 => "9",
|
||||||
if (id as isize) < (self.next as isize - MEMORY_SIZE as isize) {
|
10 => "10",
|
||||||
return None;
|
11 => "11",
|
||||||
}
|
12 => "12",
|
||||||
self.inner.get_signed((id as isize) - (self.next as isize))
|
13 => "13",
|
||||||
}
|
14 => "14",
|
||||||
fn push(&mut self, msg: Message) {
|
15 => "15",
|
||||||
info!("{}: {}", msg.author, msg.content);
|
_ => unimplemented().await,
|
||||||
self.inner.push(msg);
|
|
||||||
self.next += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use heapless::Vec;
|
use heapless::{LinearMap, Vec};
|
||||||
use pico_website::unwrap;
|
use pico_website::unwrap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apps::Content,
|
apps::{self, Content, chat::id_to_static_str},
|
||||||
socket::{HttpRequestType, HttpResCode},
|
socket::{HttpRequestType, HttpResCode},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,52 +39,108 @@ impl App for IndexApp {
|
|||||||
path => {
|
path => {
|
||||||
let (path, args) = path.split_once('?').unwrap_or((path, ""));
|
let (path, args) = path.split_once('?').unwrap_or((path, ""));
|
||||||
let mut team = None;
|
let mut team = None;
|
||||||
|
let mut name = None;
|
||||||
|
let mut id = None;
|
||||||
for arg in args.split('&') {
|
for arg in args.split('&') {
|
||||||
match arg.split_once('=') {
|
match arg.split_once('=') {
|
||||||
Some(("team", "0")) => team = Some("0"),
|
Some(("team", "0")) => team = Some("0"),
|
||||||
Some(("team", "1")) => team = Some("1"),
|
Some(("team", "1")) => team = Some("1"),
|
||||||
|
Some(("name", n)) => {
|
||||||
|
if n.len() >= 1 && n.len() <= 16 {
|
||||||
|
name = Some(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(("id", id)) => {
|
||||||
|
if let Ok(id) = id.parse::<u8>() {
|
||||||
|
if id < apps::chat::USERS_LEN {}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if path == "/ttt" {
|
match path {
|
||||||
let Some(team) = team else {
|
"/ttt" => {
|
||||||
return (HttpResCode::BadRequest, "", None);
|
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)))
|
|
||||||
} else if path == "/ttt.js" {
|
|
||||||
let Some(team) = team else {
|
|
||||||
return (HttpResCode::BadRequest, "", None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut content = Vec::new();
|
|
||||||
let r: Result<(), &str> = try {
|
|
||||||
content.push("const team = ")?;
|
|
||||||
content.push(team)?;
|
|
||||||
content.push(";")?;
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
content.push(include_str!("ttt.js"))?;
|
let html = include_str!("ttt.html");
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
content.push(include_str!("../../static/ttt.min.js"))?;
|
let html = include_str!("../../static/ttt.min.html");
|
||||||
};
|
|
||||||
unwrap(r).await;
|
let mut content = Vec::new();
|
||||||
(HttpResCode::Ok, "javascript", Some(Content(content)))
|
let r: Result<(), &str> = try {
|
||||||
} else {
|
let (html1, html2) = html.split_once("/ttt.js").ok_or("")?;
|
||||||
(HttpResCode::NotFound, "", None)
|
|
||||||
|
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 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)))
|
||||||
|
}
|
||||||
|
_ => (HttpResCode::NotFound, "", None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
src/lib.rs
12
src/lib.rs
@ -14,7 +14,7 @@ pub async fn unwrap<T, E: Debug>(res: Result<T, E>) -> T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn unwrap_opt<T>(opt: Option<T>) -> T {
|
pub async fn unwrap_opt<T>(opt: Option<T>) -> T {
|
||||||
unwrap(opt.ok_or(())).await
|
expect_opt("option unwraped", opt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert(condition: bool) {
|
pub async fn assert(condition: bool) {
|
||||||
@ -24,6 +24,16 @@ pub async fn assert(condition: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// TODO: make this log work
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(info: &PanicInfo) -> ! {
|
fn panic(info: &PanicInfo) -> ! {
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
#![feature(slice_split_once)]
|
#![feature(slice_split_once)]
|
||||||
#![feature(try_blocks)]
|
#![feature(try_blocks)]
|
||||||
#![feature(impl_trait_in_bindings)]
|
#![feature(impl_trait_in_bindings)]
|
||||||
|
#![feature(generic_arg_infer)]
|
||||||
|
#![feature(array_repeat)]
|
||||||
|
|
||||||
#[cfg(feature = "wifi-connect")]
|
#[cfg(feature = "wifi-connect")]
|
||||||
use core::net::Ipv4Addr;
|
use core::net::Ipv4Addr;
|
||||||
@ -178,8 +180,8 @@ async fn main(spawner: Spawner) {
|
|||||||
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
|
unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "chat")]
|
#[cfg(feature = "chat")]
|
||||||
for _ in 0..4 {
|
for i in 0..4 {
|
||||||
unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await;
|
unwrap(spawner.spawn(socket::chat_listen_task(stack, i, 9000 + i as u16))).await;
|
||||||
}
|
}
|
||||||
info!("All apps lauched!");
|
info!("All apps lauched!");
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ pub async fn index_listen_task(stack: embassy_net::Stack<'static>, port: u16) {
|
|||||||
|
|
||||||
#[cfg(feature = "chat")]
|
#[cfg(feature = "chat")]
|
||||||
#[embassy_executor::task(pool_size = 4)]
|
#[embassy_executor::task(pool_size = 4)]
|
||||||
pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, port: u16) {
|
pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, id: u8, port: u16) {
|
||||||
listen_task(stack, apps::chat::ChatApp::new(), port).await
|
listen_task(stack, apps::chat::ChatApp::new(id), port).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps::App, port: u16) {
|
pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps::App, port: u16) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use core::str::from_utf8;
|
use core::str::from_utf8;
|
||||||
|
|
||||||
use embassy_net::tcp::{TcpReader, TcpSocket, TcpWriter};
|
use embassy_net::tcp::{TcpReader, TcpSocket, TcpWriter};
|
||||||
use embassy_time::Instant;
|
use embassy_time::{Instant, Timer};
|
||||||
use embedded_io_async::{ErrorType, ReadReady, Write};
|
use embedded_io_async::{ErrorType, ReadReady, Write};
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use pico_website::{assert, unwrap, unwrap_opt};
|
use pico_website::{assert, expect_opt, unwrap, unwrap_opt};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum WsMsg<'a> {
|
pub enum WsMsg<'a> {
|
||||||
@ -179,7 +179,7 @@ impl<'a, const BUF_SIZE: usize, const HEAD_BUF_SIZE: usize> Ws<'a, BUF_SIZE, HEA
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
*x ^= unwrap_opt(mask_key.get(i & 0xff)).await;
|
*x ^= unwrap_opt(mask_key.get(i % 4)).await;
|
||||||
}
|
}
|
||||||
if n_after_length + 4 + (length as usize) < n {
|
if n_after_length + 4 + (length as usize) < n {
|
||||||
self.rx.msg_in_buf = Some((
|
self.rx.msg_in_buf = Some((
|
||||||
|
Loading…
Reference in New Issue
Block a user