request device sync when not connected
This commit is contained in:
parent
852fa32c93
commit
0daba1bd73
4 changed files with 194 additions and 130 deletions
|
|
@ -4,18 +4,26 @@ type ApiEnvelope =
|
||||||
| { type: "hello" }
|
| { type: "hello" }
|
||||||
| { type: "device_event"; deviceId: string; event: unknown };
|
| { type: "device_event"; deviceId: string; event: unknown };
|
||||||
|
|
||||||
type DeviceMessage = {
|
type DeviceMessage =
|
||||||
|
| {
|
||||||
DeviceId: string;
|
DeviceId: string;
|
||||||
} | {
|
}
|
||||||
|
| {
|
||||||
QuizResponse: number;
|
QuizResponse: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type DeviceQuestionData = {
|
type DeviceQuestionData = {
|
||||||
text: string;
|
text: string;
|
||||||
points: number;
|
points: number;
|
||||||
index: number;
|
index: number;
|
||||||
q_type: "Choice" | { Numeric: { min: number; max: number } }
|
q_type: "Choice" | { Numeric: { min: number; max: number } };
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type ProxyOutput =
|
||||||
|
| { ConnectPrompt: string }
|
||||||
|
| { Question: DeviceQuestionData }
|
||||||
|
| "Results"
|
||||||
|
| { Error: string };
|
||||||
|
|
||||||
type QuizQuestion =
|
type QuizQuestion =
|
||||||
| {
|
| {
|
||||||
|
|
@ -69,9 +77,11 @@ function registerSocket(socket: Socket, deviceId: string) {
|
||||||
console.log("Registered", socket.remoteAddress, deviceId);
|
console.log("Registered", socket.remoteAddress, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDeviceQuestionData(
|
function writeProxyOutput(socket: Socket, output: ProxyOutput) {
|
||||||
quizData: QuizState,
|
socket.write(`${JSON.stringify(output)}\n`);
|
||||||
): DeviceQuestionData | null {
|
}
|
||||||
|
|
||||||
|
function toDeviceQuestionData(quizData: QuizState): DeviceQuestionData | null {
|
||||||
if (!quizData.currentQuestion) return null;
|
if (!quizData.currentQuestion) return null;
|
||||||
const question = quizData.currentQuestion;
|
const question = quizData.currentQuestion;
|
||||||
const q_type =
|
const q_type =
|
||||||
|
|
@ -108,6 +118,7 @@ const listener = Bun.listen({
|
||||||
|
|
||||||
if ("DeviceId" in data) {
|
if ("DeviceId" in data) {
|
||||||
registerSocket(socket, data.DeviceId);
|
registerSocket(socket, data.DeviceId);
|
||||||
|
writeProxyOutput(socket, { ConnectPrompt: data.DeviceId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ("QuizResponse" in data) {
|
if ("QuizResponse" in data) {
|
||||||
|
|
@ -146,7 +157,7 @@ apiSocket.onmessage = (e) => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
const event = message.event as PartySocketEvent;
|
const event = message.event as PartySocketEvent;
|
||||||
if (event.type === "error") {
|
if (event.type === "error") {
|
||||||
socket.write(`${JSON.stringify({ Error: event.message })}\n`);
|
writeProxyOutput(socket, { Error: event.message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,25 +165,27 @@ apiSocket.onmessage = (e) => {
|
||||||
const quizData = event.party?.data ?? null;
|
const quizData = event.party?.data ?? null;
|
||||||
if (!quizData) return;
|
if (!quizData) return;
|
||||||
const question = toDeviceQuestionData(quizData);
|
const question = toDeviceQuestionData(quizData);
|
||||||
socket.write(
|
if (question) {
|
||||||
`${JSON.stringify({ Question: question, Status: quizData.status })}\n`,
|
writeProxyOutput(socket, { Question: question });
|
||||||
);
|
} else if (quizData.status === "results") {
|
||||||
|
writeProxyOutput(socket, "Results");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "quiz_state") {
|
if (event.type === "quiz_state") {
|
||||||
const question = toDeviceQuestionData(event.quiz);
|
const question = toDeviceQuestionData(event.quiz);
|
||||||
socket.write(
|
if (question) {
|
||||||
`${JSON.stringify({ Question: question, Status: event.quiz.status })}\n`,
|
writeProxyOutput(socket, { Question: question });
|
||||||
);
|
} else if (event.quiz.status === "results") {
|
||||||
|
writeProxyOutput(socket, "Results");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.write(`${JSON.stringify(message.event)}\n`);
|
writeProxyOutput(socket, { Error: "Unsupported proxy event." });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
apiSocket.onerror = (error) => {
|
apiSocket.onerror = (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const DOT: char = char::from_u32(0b1010_0101).unwrap();
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ViewState {
|
pub enum ViewState {
|
||||||
Loading,
|
Loading,
|
||||||
|
ConnectPrompt,
|
||||||
Question,
|
Question,
|
||||||
Results,
|
Results,
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +45,7 @@ pub struct QuestionDataNet<'a> {
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub enum ProxyOutput<'a> {
|
pub enum ProxyOutput<'a> {
|
||||||
|
ConnectPrompt(&'a str),
|
||||||
Question(QuestionDataNet<'a>),
|
Question(QuestionDataNet<'a>),
|
||||||
Results,
|
Results,
|
||||||
Error(&'a str),
|
Error(&'a str),
|
||||||
|
|
@ -81,6 +83,7 @@ impl WheelData {
|
||||||
|
|
||||||
pub struct DeviceState {
|
pub struct DeviceState {
|
||||||
view: ViewState,
|
view: ViewState,
|
||||||
|
device_id: Option<OwnedStr<64>>,
|
||||||
question: Option<QuestionData>,
|
question: Option<QuestionData>,
|
||||||
wheel: WheelData,
|
wheel: WheelData,
|
||||||
last_index: usize,
|
last_index: usize,
|
||||||
|
|
@ -91,6 +94,7 @@ impl DeviceState {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
view: ViewState::Loading,
|
view: ViewState::Loading,
|
||||||
|
device_id: None,
|
||||||
question: None,
|
question: None,
|
||||||
wheel: WheelData::empty(),
|
wheel: WheelData::empty(),
|
||||||
last_index: 0,
|
last_index: 0,
|
||||||
|
|
@ -119,6 +123,17 @@ impl DeviceState {
|
||||||
|
|
||||||
pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) {
|
pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) {
|
||||||
match data {
|
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) => {
|
ProxyOutput::Question(data) => {
|
||||||
let data: QuestionData = data.into();
|
let data: QuestionData = data.into();
|
||||||
let mut future_wheel = WheelData::empty();
|
let mut future_wheel = WheelData::empty();
|
||||||
|
|
@ -157,6 +172,20 @@ impl DeviceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_lines(&mut self) -> Option<(OwnedStr<16>, OwnedStr<16>)> {
|
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 {
|
if self.view != ViewState::Question {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -233,11 +262,7 @@ pub fn wheel_delta(old_angle: i32, current_angle: i32, inverted: bool) -> i32 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if inverted {
|
if inverted { -diff } else { diff }
|
||||||
-diff
|
|
||||||
} else {
|
|
||||||
diff
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_wheel_delta(
|
pub fn apply_wheel_delta(
|
||||||
|
|
@ -304,7 +329,10 @@ impl<const CAP: usize> ufmt::uWrite for OwnedStrWriter<CAP> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_str<const CAP: usize>(text: &str, width: usize) -> Result<OwnedStr<CAP>, owned_str::Error> {
|
pub fn center_str<const CAP: usize>(
|
||||||
|
text: &str,
|
||||||
|
width: usize,
|
||||||
|
) -> Result<OwnedStr<CAP>, owned_str::Error> {
|
||||||
let mut res = OwnedStr::new();
|
let mut res = OwnedStr::new();
|
||||||
let len = text.len();
|
let len = text.len();
|
||||||
let padding = (width.saturating_sub(len) + 1) / 2;
|
let padding = (width.saturating_sub(len) + 1) / 2;
|
||||||
|
|
@ -317,7 +345,30 @@ pub fn center_str<const CAP: usize>(text: &str, width: usize) -> Result<OwnedStr
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{apply_wheel_delta, wheel_delta};
|
use super::{DeviceState, ViewState, apply_wheel_delta, parse_proxy_output, wheel_delta};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_and_renders_connect_prompt() {
|
||||||
|
let data = parse_proxy_output(r#"{"ConnectPrompt":"esp32-1"}"#).unwrap();
|
||||||
|
let mut state = DeviceState::new();
|
||||||
|
|
||||||
|
state.apply_proxy_output(data);
|
||||||
|
|
||||||
|
assert_eq!(state.view_state(), ViewState::ConnectPrompt);
|
||||||
|
let (line1, line2) = state.render_lines().unwrap();
|
||||||
|
assert_eq!(line1.as_str(), "Connect device");
|
||||||
|
assert_eq!(line2.as_str(), " esp32-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_results_message() {
|
||||||
|
let data = parse_proxy_output(r#""Results""#).unwrap();
|
||||||
|
let mut state = DeviceState::new();
|
||||||
|
|
||||||
|
state.apply_proxy_output(data);
|
||||||
|
|
||||||
|
assert_eq!(state.view_state(), ViewState::Results);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wraps_forward_across_zero() {
|
fn wraps_forward_across_zero() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue