From 0daba1bd732da1796be67031b71d0eb1dd251025 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Tue, 12 May 2026 22:47:24 +0200 Subject: [PATCH] request device sync when not connected --- dev-proxy/index.ts | 191 +++++++++++++++++++++------------------- dev-proxy/package.json | 20 ++--- dev-proxy/tsconfig.json | 48 +++++----- device-state/src/lib.rs | 65 ++++++++++++-- 4 files changed, 194 insertions(+), 130 deletions(-) diff --git a/dev-proxy/index.ts b/dev-proxy/index.ts index 956863a..f625f94 100644 --- a/dev-proxy/index.ts +++ b/dev-proxy/index.ts @@ -4,51 +4,59 @@ type ApiEnvelope = | { type: "hello" } | { type: "device_event"; deviceId: string; event: unknown }; -type DeviceMessage = { - DeviceId: string; -} | { - QuizResponse: number; -} +type DeviceMessage = + | { + DeviceId: string; + } + | { + QuizResponse: number; + }; type DeviceQuestionData = { - text: string; - points: number; - index: number; - q_type: "Choice" | { Numeric: { min: number; max: number } } -} + text: string; + points: number; + index: number; + q_type: "Choice" | { Numeric: { min: number; max: number } }; +}; + +type ProxyOutput = + | { ConnectPrompt: string } + | { Question: DeviceQuestionData } + | "Results" + | { Error: string }; type QuizQuestion = - | { - type: "choice"; - text: string; - points: number; - } - | { - type: "numeric"; - text: string; - points: number; - range: { min: number; max: number }; - }; + | { + type: "choice"; + text: string; + points: number; + } + | { + type: "numeric"; + text: string; + points: number; + range: { min: number; max: number }; + }; type QuizState = { - status: "running" | "results"; - questionIndex: number; - currentQuestion: QuizQuestion | null; + status: "running" | "results"; + questionIndex: number; + currentQuestion: QuizQuestion | null; }; type PartyStatusEvent = { - type: "party_status"; - party: { data?: QuizState } | null; + type: "party_status"; + party: { data?: QuizState } | null; }; type QuizStateEvent = { - type: "quiz_state"; - quiz: QuizState; + type: "quiz_state"; + quiz: QuizState; }; type ErrorEvent = { - type: "error"; - message: string; + type: "error"; + message: string; }; type PartySocketEvent = PartyStatusEvent | QuizStateEvent | ErrorEvent; @@ -65,66 +73,69 @@ function registerSocket(socket: Socket, deviceId: string) { const existing = sockets.get(deviceId); if (existing && existing !== socket) existing.end(); sockets.set(deviceId, socket); - socketIds.set(socket, deviceId); - console.log("Registered", socket.remoteAddress, deviceId); + socketIds.set(socket, deviceId); + console.log("Registered", socket.remoteAddress, deviceId); } -function toDeviceQuestionData( - quizData: QuizState, -): DeviceQuestionData | null { - if (!quizData.currentQuestion) return null; - const question = quizData.currentQuestion; - const q_type = - question.type === "choice" - ? "Choice" - : { Numeric: { min: question.range.min, max: question.range.max } }; +function writeProxyOutput(socket: Socket, output: ProxyOutput) { + socket.write(`${JSON.stringify(output)}\n`); +} - return { - text: question.text, - points: question.points, - index: quizData.questionIndex, - q_type, - }; +function toDeviceQuestionData(quizData: QuizState): DeviceQuestionData | null { + if (!quizData.currentQuestion) return null; + const question = quizData.currentQuestion; + const q_type = + question.type === "choice" + ? "Choice" + : { Numeric: { min: question.range.min, max: question.range.max } }; + + return { + text: question.text, + points: question.points, + index: quizData.questionIndex, + q_type, + }; } const listener = Bun.listen({ port: 7070, hostname: "0.0.0.0", - socket: { - open(socket) { - socket.setKeepAlive(true); - console.log("Connection", socket.remoteAddress, socket.remotePort); - }, - data(socket, buf) { - const raw = new TextDecoder().decode(buf).trim(); - let data: DeviceMessage; - try { - data = JSON.parse(raw); - } catch { - return; - } - console.log("Data", socket.remoteAddress, data); - if (!data) return; + socket: { + open(socket) { + socket.setKeepAlive(true); + console.log("Connection", socket.remoteAddress, socket.remotePort); + }, + data(socket, buf) { + const raw = new TextDecoder().decode(buf).trim(); + let data: DeviceMessage; + try { + data = JSON.parse(raw); + } catch { + return; + } + console.log("Data", socket.remoteAddress, data); + if (!data) return; - if ("DeviceId" in data) { - registerSocket(socket, data.DeviceId); - return; - } - if ("QuizResponse" in data) { - const deviceId = socketDeviceId(socket); - if (!deviceId) return; - apiSocket?.send( - JSON.stringify({ - type: "device_message", - deviceId, - payload: { QuizResponse: data.QuizResponse }, - }), - ); - return; - } + if ("DeviceId" in data) { + registerSocket(socket, data.DeviceId); + writeProxyOutput(socket, { ConnectPrompt: data.DeviceId }); + return; + } + if ("QuizResponse" in data) { + const deviceId = socketDeviceId(socket); + if (!deviceId) return; + apiSocket?.send( + JSON.stringify({ + type: "device_message", + deviceId, + payload: { QuizResponse: data.QuizResponse }, + }), + ); + return; + } }, close(socket) { - console.log("Connection", socket.remoteAddress); + console.log("Connection", socket.remoteAddress); const deviceId = socketDeviceId(socket); if (deviceId && sockets.get(deviceId) === socket) { sockets.delete(deviceId); @@ -146,7 +157,7 @@ apiSocket.onmessage = (e) => { if (!socket) return; const event = message.event as PartySocketEvent; if (event.type === "error") { - socket.write(`${JSON.stringify({ Error: event.message })}\n`); + writeProxyOutput(socket, { Error: event.message }); return; } @@ -154,25 +165,27 @@ apiSocket.onmessage = (e) => { const quizData = event.party?.data ?? null; if (!quizData) return; const question = toDeviceQuestionData(quizData); - socket.write( - `${JSON.stringify({ Question: question, Status: quizData.status })}\n`, - ); + if (question) { + writeProxyOutput(socket, { Question: question }); + } else if (quizData.status === "results") { + writeProxyOutput(socket, "Results"); + } return; } if (event.type === "quiz_state") { const question = toDeviceQuestionData(event.quiz); - socket.write( - `${JSON.stringify({ Question: question, Status: event.quiz.status })}\n`, - ); + if (question) { + writeProxyOutput(socket, { Question: question }); + } else if (event.quiz.status === "results") { + writeProxyOutput(socket, "Results"); + } return; } - socket.write(`${JSON.stringify(message.event)}\n`); + writeProxyOutput(socket, { Error: "Unsupported proxy event." }); }; - - apiSocket.onerror = (error) => { console.error(error); }; diff --git a/dev-proxy/package.json b/dev-proxy/package.json index 20fc719..cb16183 100644 --- a/dev-proxy/package.json +++ b/dev-proxy/package.json @@ -1,12 +1,12 @@ { - "name": "dev-proxy", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } + "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 index bfa0fea..146fe4e 100644 --- a/dev-proxy/tsconfig.json +++ b/dev-proxy/tsconfig.json @@ -1,29 +1,29 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "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, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": 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 - } + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } } diff --git a/device-state/src/lib.rs b/device-state/src/lib.rs index e162dde..ca30d0a 100644 --- a/device-state/src/lib.rs +++ b/device-state/src/lib.rs @@ -16,6 +16,7 @@ const DOT: char = char::from_u32(0b1010_0101).unwrap(); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ViewState { Loading, + ConnectPrompt, Question, Results, } @@ -44,6 +45,7 @@ pub struct QuestionDataNet<'a> { #[derive(Deserialize)] pub enum ProxyOutput<'a> { + ConnectPrompt(&'a str), Question(QuestionDataNet<'a>), Results, Error(&'a str), @@ -81,6 +83,7 @@ impl WheelData { pub struct DeviceState { view: ViewState, + device_id: Option>, question: Option, wheel: WheelData, last_index: usize, @@ -91,6 +94,7 @@ impl DeviceState { pub const fn new() -> Self { Self { view: ViewState::Loading, + device_id: None, question: None, wheel: WheelData::empty(), last_index: 0, @@ -119,6 +123,17 @@ impl DeviceState { pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) { match data { + ProxyOutput::ConnectPrompt(device_id) => { + let mut owned_device_id = OwnedStr::new(); + for char in device_id.chars() { + if owned_device_id.try_push(char).is_err() { + break; + } + } + self.device_id = Some(owned_device_id); + self.question = None; + self.view = ViewState::ConnectPrompt; + } ProxyOutput::Question(data) => { let data: QuestionData = data.into(); let mut future_wheel = WheelData::empty(); @@ -157,6 +172,20 @@ impl DeviceState { } pub fn render_lines(&mut self) -> Option<(OwnedStr<16>, OwnedStr<16>)> { + if self.view == ViewState::ConnectPrompt { + let device_id = self.device_id.as_ref()?; + let mut display_id: OwnedStr<16> = OwnedStr::new(); + for char in device_id.as_str().chars() { + if display_id.try_push(char).is_err() { + break; + } + } + return Some(( + OwnedStr::from_str("Connect device").unwrap(), + center_str::<16>(display_id.as_str(), 16).unwrap(), + )); + } + if self.view != ViewState::Question { return None; } @@ -233,11 +262,7 @@ pub fn wheel_delta(old_angle: i32, current_angle: i32, inverted: bool) -> i32 { }; } - if inverted { - -diff - } else { - diff - } + if inverted { -diff } else { diff } } pub fn apply_wheel_delta( @@ -304,7 +329,10 @@ impl ufmt::uWrite for OwnedStrWriter { } } -pub fn center_str(text: &str, width: usize) -> Result, owned_str::Error> { +pub fn center_str( + text: &str, + width: usize, +) -> Result, owned_str::Error> { let mut res = OwnedStr::new(); let len = text.len(); let padding = (width.saturating_sub(len) + 1) / 2; @@ -317,7 +345,30 @@ pub fn center_str(text: &str, width: usize) -> Result