diff --git a/api/src/routes/device-socket.ts b/api/src/routes/device-socket.ts new file mode 100644 index 0000000..bd4e765 --- /dev/null +++ b/api/src/routes/device-socket.ts @@ -0,0 +1,44 @@ +import Elysia from "elysia" +import { broadcastQuizState, partyTopic, socketPartyId, userTopic } from "./party-socket" +import { getMemberRecord, getPartyStatus } from "../party-data" +import { db } from "../db" + +export const partySocketApp = new Elysia() + .group("/dev-socket", (app) => + app + .get("/test", () => ({ ok: 1 })) + .ws("/ws", { + async open(ws) { + let id = "zzxWcTUntIWTHkX8atEOv7Neiu7XEz9t" + ws.subscribe(userTopic(id)) + const membership = await getMemberRecord(db, id); + if (!membership) { + ws.send( + JSON.stringify({ + type: "snapshot", + party: null, + members: [], + }), + ); + return; + } + + socketPartyId.set(ws, membership.partyId); + ws.subscribe(partyTopic(membership.partyId)); + + const snapshot = await getPartyStatus(membership.partyId); + if (snapshot) { + ws.send( + JSON.stringify({ + type: "snapshot", + party: snapshot.party, + members: snapshot.members, + }), + ); + + await broadcastQuizState(ws, membership.partyId); + } + } + + }) + ) diff --git a/dev-proxy/.gitignore b/dev-proxy/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/dev-proxy/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/dev-proxy/README.md b/dev-proxy/README.md new file mode 100644 index 0000000..be0b2ae --- /dev/null +++ b/dev-proxy/README.md @@ -0,0 +1,15 @@ +# dev-proxy + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/dev-proxy/bun.lock b/dev-proxy/bun.lock new file mode 100644 index 0000000..f0f0b89 --- /dev/null +++ b/dev-proxy/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "dev-proxy", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + } +} diff --git a/dev-proxy/index.ts b/dev-proxy/index.ts new file mode 100644 index 0000000..5442734 --- /dev/null +++ b/dev-proxy/index.ts @@ -0,0 +1,38 @@ +import type { + PartySocketEvent, + PartyState, +} from "../api/src/party-types"; +let ws: WebSocket | null = null; + +Bun.listen({ + hostname: "0.0.0.0", + port: 7070, + socket: { + data(socket, data) { + // in1={} in2={} in3={} in4={} angle={} + console.log("recv", data) + }, // message received from client + open(socket) { + console.log("Connected"); + ws = new WebSocket("ws://localhost:3000/api/dev-socket/ws"); + + ws.onmessage = e => { + const data = JSON.parse(e.data) as PartySocketEvent; + switch (data.type) { + case "party_status": + const { party: { data: { currentQuestion } } } = data; + console.log(currentQuestion) + break; + } + } + }, // socket opened + close(socket, error) { + ws?.close(); + ws = null; + }, // socket closed + drain(socket) {}, // socket ready for more data + error(socket, error) {}, // error handler + }, +}); + +console.log("Started on :7070") diff --git a/dev-proxy/package.json b/dev-proxy/package.json new file mode 100644 index 0000000..20fc719 --- /dev/null +++ b/dev-proxy/package.json @@ -0,0 +1,12 @@ +{ + "name": "dev-proxy", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/dev-proxy/tsconfig.json b/dev-proxy/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/dev-proxy/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/pico/src/main.rs b/pico/src/main.rs index db142ae..a8f0d0e 100644 --- a/pico/src/main.rs +++ b/pico/src/main.rs @@ -3,6 +3,8 @@ use arrayvec::ArrayVec; use core::str::FromStr; +use embassy_net::tcp::ConnectError; +use embedded_io::ReadReady; use ag_lcd::{Blink, Cursor, Display, LcdDisplay}; use as5600::As5600; @@ -22,7 +24,7 @@ use embassy_rp::{bind_interrupts, dma}; use embassy_rp::{peripherals::USB, usb}; use embassy_time::{Delay, Duration, Timer}; use static_cell::StaticCell; -use ufmt::uwrite; +use ufmt::{uWrite, uwrite}; use {defmt_rtt as _, panic_probe as _}; bind_interrupts!(struct Irqs { @@ -49,7 +51,7 @@ async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'sta runner.run().await } -const WIFI_NETWORK: &str = "flamme"; +const WIFI_NETWORK: &str = "aura"; const WIFI_PASSWORD: &str = "12345678"; #[embassy_executor::main] @@ -105,6 +107,8 @@ async fn main(spawner: Spawner) { .with_display(Display::On) .with_blink(Blink::On) .with_cursor(Cursor::On) + .with_lines(ag_lcd::Lines::TwoLines) + // .with_autoscroll(ag_lcd::AutoScroll::On) .build(); lcd.set_cursor(Cursor::Off); lcd.set_blink(Blink::Off); @@ -122,6 +126,8 @@ async fn main(spawner: Spawner) { rng.next_u64(), ); spawner.spawn(unwrap!(net_task(runner))); + uwrite!(lcd, "con."); + while let Err(err) = control .join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes())) .await @@ -139,11 +145,13 @@ async fn main(spawner: Spawner) { stack.wait_link_up().await; lcd.clear(); + // lcd.home(); uwrite!(lcd, "dhcp."); stack.wait_config_up().await; let cfg = wait_for_config(stack).await; let local_addr = cfg.address.address(); - uwrite!(lcd, "IP address: {:?}", local_addr.octets()); + info!("IP address: {:?}", local_addr); + // uwrite!(lcd, "IP address: {:?}", local_addr.octets()); let i2c = I2c::new_async(p.I2C0, p.PIN_5, p.PIN_4, Irqs, Config::default()); let mut as5600 = As5600::new(i2c); @@ -162,9 +170,23 @@ async fn main(spawner: Spawner) { led.set_low(); info!("Connecting..."); - let host_addr = embassy_net::Ipv4Address::from_str("84.238.32.253").unwrap(); + uwrite!(lcd, "con2."); + let host_addr = embassy_net::Ipv4Address::from_str("192.168.12.1").unwrap(); if let Err(e) = socket.connect((host_addr, 7070)).await { + lcd.clear(); + uwrite!(lcd, "conerr"); + Timer::after(Duration::from_micros(100)).await; + lcd.set_position(0, 1); + let emsg = match e { + ConnectError::ConnectionReset => "rst", + ConnectError::InvalidState => "inv", + ConnectError::TimedOut => "tout", + ConnectError::NoRoute => "nroute", + }; + uwrite!(lcd, "{}", emsg); warn!("connect error: {:?}", e); + } else { + uwrite!(lcd, "conok"); } info!("Connected to {:?}", socket.remote_endpoint()); @@ -178,7 +200,7 @@ async fn main(spawner: Spawner) { let angle = as5600.angle().unwrap_or(0); { use embedded_io::Write; - let _ = core::write!( + let _ = core::writeln!( &mut buffer[..], "in1={} in2={} in3={} in4={} angle={}", in1, @@ -192,6 +214,22 @@ async fn main(spawner: Spawner) { use embedded_io_async::Write; let _ = socket.write_all(&*buffer).await; } + if socket.read_ready().unwrap_or(false) { + let mut rx_buffer = [0; 4096]; + let n = socket.read(&mut rx_buffer).await.unwrap_or(0); + if n > 0 { + lcd.clear(); + lcd.home(); + let s = core::str::from_utf8(&rx_buffer[..n]).unwrap_or(""); + let npos = s.find('\n').unwrap_or(n); + let display_text = &s[..npos]; + lcd.write_str(display_text).ok(); + + Timer::after(Duration::from_micros(100)).await; + lcd.set_position(0, 1); + lcd.write_str(&s[npos..]); + } + } Timer::after(delay).await; } }