working turns!

This commit is contained in:
Arkitu 2025-04-08 17:39:59 +02:00
parent 8956acfcb4
commit 0c0024d66c
5 changed files with 175 additions and 48 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
/wifi.json

20
Cargo.lock generated
View File

@ -812,6 +812,7 @@ checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [ dependencies = [
"defmt", "defmt",
"hash32", "hash32",
"serde",
"stable_deref_trait", "stable_deref_trait",
] ]
@ -1090,6 +1091,8 @@ dependencies = [
"panic-probe", "panic-probe",
"portable-atomic", "portable-atomic",
"rand_core", "rand_core",
"serde",
"serde-json-core",
"static_cell", "static_cell",
] ]
@ -1295,6 +1298,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1334,6 +1343,17 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-json-core"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b81787e655bd59cecadc91f7b6b8651330b2be6c33246039a65e5cd6f4e0828"
dependencies = [
"heapless",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.219"

View File

@ -3,6 +3,10 @@ name = "pico-website"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
wifi-connect = ["dep:serde-json-core", "dep:serde"] # you need to add a wifi.conf file with your wifi credentials (for example : "Example Wifi name:pa$$w0rd")
default = ["wifi-connect"]
[dependencies] [dependencies]
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [ embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = [
"defmt", "defmt",
@ -25,8 +29,8 @@ embassy-net = { git = "https://github.com/embassy-rs/embassy", features = [
"proto-ipv4", "proto-ipv4",
"tcp", "tcp",
"dhcpv4", "dhcpv4",
"dns", # "dns",
"icmp", # "icmp",
"packet-trace" "packet-trace"
] } ] }
cyw43-pio = {git = "https://github.com/embassy-rs/embassy"} cyw43-pio = {git = "https://github.com/embassy-rs/embassy"}
@ -44,3 +48,5 @@ heapless = "*"
rand_core = "*" rand_core = "*"
log = "*" log = "*"
serde-json-core = {version = "*", optional = true}
serde = {version = "*", optional = true, default-features = false, features = ["derive"]}

View File

@ -4,26 +4,30 @@
#![feature(impl_trait_in_assoc_type)] #![feature(impl_trait_in_assoc_type)]
#![feature(slice_split_once)] #![feature(slice_split_once)]
use core::fmt::{Debug, Write}; use core::fmt::{Debug, Display, Write};
use core::net::Ipv4Addr;
use core::ops::Not;
use core::str::from_utf8; use core::str::from_utf8;
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use cortex_m::interrupt::Mutex; use cortex_m::interrupt::Mutex;
use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi}; use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi};
use embassy_executor::Spawner; use embassy_executor::{Executor, Spawner};
use embassy_net::tcp::TcpSocket; use embassy_net::tcp::TcpSocket;
use embassy_net::{Config, Stack, StackResources}; use embassy_net::{Config, DhcpConfig, StackResources};
use embassy_rp::bind_interrupts; use embassy_rp::bind_interrupts;
use embassy_rp::clocks::RoscRng; use embassy_rp::clocks::RoscRng;
use embassy_rp::gpio::{Level, Output}; use embassy_rp::gpio::{Level, Output};
use embassy_rp::multicore::{Stack, spawn_core1};
use embassy_rp::peripherals::USB; use embassy_rp::peripherals::USB;
use embassy_rp::peripherals::{DMA_CH0, PIO0}; use embassy_rp::peripherals::{DMA_CH0, PIO0};
use embassy_rp::pio::program::ProgramWithDefines;
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 embassy_time::Duration; use embassy_time::Duration;
use embassy_time::Timer; use embassy_time::Timer;
use embedded_io_async::Write as _; use embedded_io_async::Write as _;
use heapless::{String, Vec}; use heapless::{String, Vec};
use log::{debug, info, warn}; use log::{debug, error, info, warn};
use rand_core::RngCore; use rand_core::RngCore;
use static_cell::StaticCell; use static_cell::StaticCell;
use {defmt_rtt as _, panic_probe as _}; use {defmt_rtt as _, panic_probe as _};
@ -50,6 +54,18 @@ async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'sta
runner.run().await runner.run().await
} }
async fn unwrap<T, E: Debug>(res: Result<T, E>) -> T {
match res {
Ok(v) => v,
Err(e) => {
error!("FATAL ERROR : {:?}", e);
loop {
Timer::after_millis(0).await;
}
}
}
}
#[embassy_executor::main] #[embassy_executor::main]
async fn main(spawner: Spawner) { async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default()); let p = embassy_rp::init(Default::default());
@ -76,13 +92,14 @@ async fn main(spawner: Spawner) {
static STATE: StaticCell<cyw43::State> = StaticCell::new(); static STATE: StaticCell<cyw43::State> = StaticCell::new();
let state = STATE.init(cyw43::State::new()); let state = STATE.init(cyw43::State::new());
let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await;
spawner.spawn(cyw43_task(runner)).unwrap(); unwrap(spawner.spawn(cyw43_task(runner))).await;
control.init(clm).await; control.init(clm).await;
control control
.set_power_management(cyw43::PowerManagementMode::PowerSave) .set_power_management(cyw43::PowerManagementMode::PowerSave)
.await; .await;
#[cfg(not(feature = "wifi-connect"))]
// Use a link-local address for communication without DHCP server // Use a link-local address for communication without DHCP server
let config = Config::ipv4_static(embassy_net::StaticConfigV4 { let config = Config::ipv4_static(embassy_net::StaticConfigV4 {
address: embassy_net::Ipv4Cidr::new(embassy_net::Ipv4Address::new(192, 254, 0, 2), 16), address: embassy_net::Ipv4Cidr::new(embassy_net::Ipv4Address::new(192, 254, 0, 2), 16),
@ -90,11 +107,26 @@ async fn main(spawner: Spawner) {
gateway: None, gateway: None,
}); });
#[cfg(feature = "wifi-connect")]
let wifi_conf = unwrap(serde_json_core::from_slice::<WifiConnectConf>(include_bytes!("../wifi.json"))).await.0;
// Use a link-local address for communication without DHCP server
// let config = Config::dhcpv4(embassy_net::DhcpConfig::default());
#[cfg(feature = "wifi-connect")]
let config = match wifi_conf.ip {
Some(ip) => Config::ipv4_static(embassy_net::StaticConfigV4 {
address: embassy_net::Ipv4Cidr::new(ip, 24),
dns_servers: heapless::Vec::new(),
gateway: None,
}),
None => Config::dhcpv4(DhcpConfig::default())
};
// Generate random seed // Generate random seed
let seed = rng.next_u64(); let seed = rng.next_u64();
// Init network stack // Init network stack
static RESOURCES: StaticCell<StackResources<2>> = StaticCell::new(); static RESOURCES: StaticCell<StackResources<4>> = StaticCell::new();
let (stack, runner) = embassy_net::new( let (stack, runner) = embassy_net::new(
net_device, net_device,
config, config,
@ -102,13 +134,45 @@ async fn main(spawner: Spawner) {
seed, seed,
); );
spawner.spawn(net_task(runner)).unwrap(); unwrap(spawner.spawn(net_task(runner))).await;
#[cfg(not(feature = "wifi-connect"))]
//control.start_ap_open("cyw43", 5).await; //control.start_ap_open("cyw43", 5).await;
control.start_ap_wpa2("cyw43", "password", 5).await; control.start_ap_wpa2("cyw43", "password", 5).await;
spawner.spawn(listen_task(stack, Team::Zero, 80)).unwrap(); #[cfg(feature = "wifi-connect")]
spawner.spawn(listen_task(stack, Team::One, 81)).unwrap(); {
loop {
match control
.join(wifi_conf.name, cyw43::JoinOptions::new(wifi_conf.password.as_bytes()))
.await
{
Ok(_) => break,
Err(err) => {
info!("join failed with status={}", err.status);
}
}
}
info!("Network joined!");
info!("waiting for link...");
stack.wait_link_up().await;
// Wait for DHCP, not necessary when using static IP
info!("waiting for DHCP...");
stack.wait_config_up().await;
// while !stack.is_config_up() {
// Timer::after_millis(100).await;
// }
info!("DHCP is now up!");
info!(
"ip : {}",
unwrap(stack.config_v4().ok_or("no dhcp config"))
.await
.address
)
}
unwrap(spawner.spawn(listen_task(stack, Team::Zero, 80))).await;
unwrap(spawner.spawn(listen_task(stack, Team::One, 81))).await;
} }
static TURN: AtomicBool = AtomicBool::new(false); static TURN: AtomicBool = AtomicBool::new(false);
@ -116,10 +180,11 @@ static TURN: AtomicBool = AtomicBool::new(false);
static BOARD: AtomicU32 = AtomicU32::new(0); static BOARD: AtomicU32 = AtomicU32::new(0);
#[embassy_executor::task(pool_size = 2)] #[embassy_executor::task(pool_size = 2)]
async fn listen_task(stack: Stack<'static>, team: Team, port: u16) { async fn listen_task(stack: embassy_net::Stack<'static>, team: Team, port: u16) {
loop { // loop {
info!("team:{:?}", team); // info!("team:{:?}", team);
} // Timer::after_millis(0).await;
// }
let mut rx_buffer = [0; 4096]; let mut rx_buffer = [0; 4096];
let mut tx_buffer = [0; 4096]; let mut tx_buffer = [0; 4096];
let mut buf = [0; 4096]; let mut buf = [0; 4096];
@ -156,13 +221,6 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
} }
}; };
info!(
"Socket {:?}: request :\n{}",
team,
from_utf8(&buf[..n]).unwrap()
);
Timer::after_secs(0).await;
let mut headers: &[u8] = &buf[..n]; let mut headers: &[u8] = &buf[..n];
let mut content: &[u8] = &[]; let mut content: &[u8] = &[];
for i in 0..(n - 1) { for i in 0..(n - 1) {
@ -187,7 +245,7 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
Some(b"GET") => HttpRequestType::Get, Some(b"GET") => HttpRequestType::Get,
Some(b"POST") => HttpRequestType::Post, Some(b"POST") => HttpRequestType::Post,
Some(t) => { Some(t) => {
warn!("Unknown request type : {}", from_utf8(t).unwrap()); warn!("Unknown request type : {}", unwrap(from_utf8(t)).await);
break; break;
} }
None => { None => {
@ -196,7 +254,7 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
} }
}, },
match l1.next() { match l1.next() {
Some(path) => from_utf8(path).unwrap(), Some(path) => unwrap(from_utf8(path)).await,
None => { None => {
warn!("No path"); warn!("No path");
break; break;
@ -206,6 +264,14 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
} }
}; };
info!(
"Socket {:?}: {:?} request for {}",
team,
request_type,
path
);
Timer::after_secs(0).await;
let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path {
"/" => (HttpResCode::Ok, "html", include_bytes!("../web/index.html")), "/" => (HttpResCode::Ok, "html", include_bytes!("../web/index.html")),
"/htmx.min.js" => ( "/htmx.min.js" => (
@ -221,13 +287,16 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
p => 'res: { p => 'res: {
if (p.starts_with("/ttt/cell") && p.len() == 10) || p == "/ttt/board" { if (p.starts_with("/ttt/cell") && p.len() == 10) || p == "/ttt/board" {
let mut board = BOARD.load(Ordering::Acquire); let mut board = BOARD.load(Ordering::Acquire);
let mut turn = TURN.load(Ordering::Acquire);
if p.starts_with("/ttt/cell") { // just return correct board in case of unauthorized move
let clicked_c: Cell = if p.starts_with("/ttt/cell") && team == turn.into() {
match TryInto::<Cell>::try_into(p.chars().nth(9).unwrap()) { let clicked_c: Cell = match TryInto::<Cell>::try_into(
Ok(c) => c, unwrap(p.chars().nth(9).ok_or("no 9th char")).await,
Err(_) => break 'res (HttpResCode::NotFound, "", &[]), ) {
}; Ok(c) => c,
Err(_) => break 'res (HttpResCode::NotFound, "", &[]),
};
if board if board
& ((2_u32.pow(clicked_c as u32)) & ((2_u32.pow(clicked_c as u32))
+ (2_u32.pow(9 + clicked_c as u32))) + (2_u32.pow(9 + clicked_c as u32)))
@ -236,7 +305,9 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
break 'res (HttpResCode::Forbidden, "", &[]); break 'res (HttpResCode::Forbidden, "", &[]);
} }
board = board | 2_u32.pow((team as u32 * 9) + clicked_c as u32); board = board | 2_u32.pow((team as u32 * 9) + clicked_c as u32);
turn = (!team).into();
BOARD.store(board, Ordering::Release); BOARD.store(board, Ordering::Release);
TURN.store(turn, Ordering::Release);
} }
res_buf.clear(); res_buf.clear();
@ -250,24 +321,26 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
}; };
match picked_by { match picked_by {
Some(Team::Zero) => { Some(Team::Zero) => {
res_buf unwrap(res_buf
.extend_from_slice( .extend_from_slice(
b"<div class=\"cell\" style=\"background-color:blue\"></div>", b"<div class=\"cell\" style=\"background-color:blue\"></div>",
) )).await;
.unwrap();
} }
Some(Team::One) => { Some(Team::One) => {
res_buf.extend_from_slice( unwrap(res_buf.extend_from_slice(
b"<div class=\"cell\" style=\"background-color:red\"></div>", b"<div class=\"cell\" style=\"background-color:red\"></div>",
) )).await;
.unwrap();
} }
None => { None => if team == turn.into() {
write!( unwrap(write!(
&mut res_buf, &mut res_buf,
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#grid\" hx-swap=\"innerHTML\"></button>", "<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#grid\" hx-swap=\"innerHTML\"></button>",
c c
).unwrap(); )).await;
} else {
unwrap(res_buf.extend_from_slice(
b"<div class=\"cell\"></div>",
)).await;
} }
}; };
} }
@ -292,13 +365,6 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
break; break;
} }
info!(
"Socket {:?}: response :\n{}",
team,
from_utf8(&res_head_buf).unwrap()
);
Timer::after_secs(0).await;
match socket.write_all(&res_head_buf).await { match socket.write_all(&res_head_buf).await {
Ok(()) => {} Ok(()) => {}
Err(e) => { Err(e) => {
@ -317,10 +383,19 @@ async fn listen_task(stack: Stack<'static>, team: Team, port: u16) {
} }
} }
#[derive(Clone, Copy, Debug)]
enum HttpRequestType { enum HttpRequestType {
Get, Get,
Post, Post,
} }
impl Into<&str> for HttpRequestType {
fn into(self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST"
}
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum HttpResCode { enum HttpResCode {
@ -338,7 +413,7 @@ impl Into<&str> for HttpResCode {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Team { enum Team {
Zero = 0, Zero = 0,
One = 1, One = 1,
@ -348,6 +423,23 @@ impl From<bool> for Team {
if value { Team::One } else { Team::Zero } if value { Team::One } else { Team::Zero }
} }
} }
impl Into<bool> for Team {
fn into(self) -> bool {
match self {
Team::Zero => false,
Team::One => true
}
}
}
impl Not for Team {
type Output = Team;
fn not(self) -> Self::Output {
match self {
Team::Zero => Team::One,
Team::One => Team::Zero
}
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Cell { enum Cell {
@ -395,3 +487,11 @@ impl TryFrom<u8> for Cell {
}) })
} }
} }
#[cfg(feature="wifi-connect")]
#[derive(serde::Deserialize)]
struct WifiConnectConf<'a> {
name: &'a str,
password: &'a str,
ip: Option<Ipv4Addr>
}

View File

@ -23,7 +23,7 @@
id="grid" id="grid"
hx-post="/ttt/board" hx-post="/ttt/board"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-trigger="load" hx-trigger="every 1s"
></div> ></div>
</body> </body>
</html> </html>