Refactor controller frontend

This commit is contained in:
maartenvn 2022-02-10 18:46:25 +01:00
parent 563d77d5fa
commit 6dded0b1f2
8 changed files with 342 additions and 221 deletions

View file

@ -1,23 +1,33 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>OBUS controller</title> <link rel="stylesheet" href="/static/css/controller.css" />
<link rel="stylesheet" href="/static/controller.css">
</head>
<body> <title>OBUS controller</title>
<p>Game state: <span id="gamestate"></span></p> </head>
<button onclick="startbutton()">START</button> <body>
<button onclick="restartbutton()">RESTART</button> <!-- State -->
<script src="static/controller.js"></script> <div class="state">
<p>Game state: <span id="gamestate"></span></p>
<div class="center"> <button onclick="onStartButtonClick()">START</button>
<canvas id="display" width="1000" height="500">Timer should be here</canvas> <button onclick="onRestartButtonClick()">RESTART</button>
</div> </div>
<script type="text/javascript" src="/static/segment-display.js"></script>
<div id="modules" class="center"></div> <!-- Timer -->
</body> <div class="center">
<canvas id="display" width="1000" height="500">
Timer should be here
</canvas>
</div>
<!-- Modules -->
<div id="modules" class="center"></div>
<!-- Scripts -->
<script src="/static/js/controller.js"></script>
<script type="text/javascript" src="/static/js/segment-display.js"></script>
</body>
</html> </html>

View file

@ -1,108 +0,0 @@
let estimated_timeout_date;
let display;
let gamestate;
let last_strike_amount = 0;
let strike_audio = new Audio('/static/strike.mp3');
strike_audio.preload = 'auto';
let last_puzzle_state = []
function startbutton() {
fetch('/start');
}
function restartbutton() {
fetch('/restart');
}
function updateDisplay() {
if (gamestate != 'GAME' || !estimated_timeout_date) {
return;
}
setTimeleft((estimated_timeout_date - (new Date()))/1000);
}
function updateModuleState(data) {
if (data.gamestate == 'INACTIVE' || data.gamestate == 'INFO') {
document.getElementById("modules").innerHTML = '';
}
else {
let newmodulestate = [];
for (const puzzlestate of data.puzzles) {
let modulediv = document.createElement('div');
modulediv.textContent = puzzlestate.address;
if (puzzlestate.solved === true) {
modulediv.className = "solved";
} else if (puzzlestate.solved === false) {
modulediv.className = "unsolved";
} else if (puzzlestate.solved === null) {
modulediv.className = "needy";
}
newmodulestate.push(modulediv);
}
document.getElementById("modules").replaceChildren(...newmodulestate);
}
}
function setTimeleft(timeleft) {
if (timeleft <= 0) {
display.setValue('00:00.0');
return
}
let integral = Math.floor(timeleft);
let fractional = timeleft - integral;
let minutes = Math.floor(integral / 60);
let seconds = integral % 60;
display.setValue(String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + '.' + String(Math.floor(fractional * 10)));
}
function state_update() {
// TODO automatically timeout this request after the update interval
fetch('/status.json')
.then(
function(response) {
if (response.status !== 200) {
console.log('FAIL: ' + response.status);
return;
}
response.json().then(function(data) {
gamestate = data.gamestate;
document.getElementById('gamestate').innerHTML = gamestate;
if (gamestate != 'GAME') {
last_strike_amount = 0;
}
else if (gamestate == 'GAME') {
// TODO maybe smooth this with the previous value of estimated_timeout_date?
let new_estimate = new Date();
new_estimate.setTime(new_estimate.getTime() + data.timeleft*1000);
estimated_timeout_date = new_estimate;
let total_strikes = data.puzzles.map(x => x.strikes).reduce((a, b) => a + b, 0);
if (last_strike_amount < total_strikes) {
strike_audio.load();
strike_audio.play();
}
last_strike_amount = total_strikes;
}
updateModuleState(data);
});
}
)
}
window.onload = function() {
setInterval(state_update, 500);
display = new SegmentDisplay("display");
display.pattern = "##:##.#";
display.displayAngle = 0;
display.digitHeight = 100;
display.digitWidth = display.digitHeight / 2;
display.digitDistance = display.digitHeight / 10;
display.segmentWidth = display.digitHeight / 10;
display.segmentDistance = display.digitHeight / 100;
display.segmentCount = 7;
display.cornerType = 1;
display.colorOn = "#24dd22";
display.colorOff = "#1b4105";
display.draw();
display.draw();
setInterval(updateDisplay, 100);
};

View file

@ -1,118 +1,140 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>CAN debugger</title> <title>CAN debugger</title>
<style> <style>
body { body {
font-family: sans-serif; font-family: sans-serif;
} }
button { button {
margin-bottom: 8px; margin-bottom: 8px;
} }
.parsed { .parsed {
background: lightgreen; background: lightgreen;
} }
td.raw { td.raw {
font-family: monospace, monospace; font-family: monospace, monospace;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
border-bottom: 1px solid black; border-bottom: 1px solid black;
} }
th, td { th,
border-left: 1px solid black; td {
border-right: 1px solid black; border-left: 1px solid black;
padding: 5px; border-right: 1px solid black;
} padding: 5px;
}
th { th {
background-color: #ff7f00;; background-color: #ff7f00;
border-bottom: 2px solid black; border-bottom: 2px solid black;
border-top: 2px solid black; border-top: 2px solid black;
height: 20px; height: 20px;
text-align: left; text-align: left;
} }
@keyframes fadein { @keyframes fadein {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
} }
to {
opacity: 1;
}
}
.fade { .fade {
animation: fadein 0.5s; animation: fadein 0.5s;
} }
table.hide_raw .raw { table.hide_raw .raw {
display:none; display: none;
} }
table.hide_consecutive_states .staterow + .staterow { table.hide_consecutive_states .staterow + .staterow {
display: none; display: none;
} }
tr:hover { tr:hover {
background-color: #aaa; background-color: #aaa;
} }
td.error, td.error > div { td.error,
background-color: rgb(255, 71, 71); td.error > div {
} background-color: rgb(255, 71, 71);
}
td.controller > div { td.controller > div {
background-color: lightseagreen; background-color: lightseagreen;
} }
td.puzzle > div { td.puzzle > div {
background-color: gold; background-color: gold;
} }
td.needy > div { td.needy > div {
background-color: rgb(128, 10, 128); background-color: rgb(128, 10, 128);
} }
.time, .raw_id, .sender_id { .time,
text-align: right; .raw_id,
} .sender_id {
text-align: right;
}
.colorblock { .colorblock {
height: 20px; height: 20px;
width: 20px; width: 20px;
margin: 3.4px; margin: 3.4px;
margin-right: 8.4px; margin-right: 8.4px;
display: inline-block; display: inline-block;
background-color:lime; background-color: lime;
vertical-align: middle; vertical-align: middle;
} }
</style> </style>
</head> </head>
<body> <body>
<button onclick="toggle_logging()" id="toggle_button">Start</button> <button onclick="toggle_logging()" id="toggle_button">Start</button>
<button onclick="clear_log()" id="clear_button">Clear</button> <button onclick="clear_log()" id="clear_button">Clear</button>
<input type="checkbox" id="show_raw" name="show_raw" checked autocomplete="off" onchange="updateShowRaw()"> <input
<label for="show_raw">Show raw address and payload</label> type="checkbox"
id="show_raw"
name="show_raw"
checked
autocomplete="off"
onchange="updateShowRaw()"
/>
<label for="show_raw">Show raw address and payload</label>
<input type="checkbox" id="show_consecutive_states" name="show_consecutive_states" checked autocomplete="off" onchange="updateShowStates()"> <input
<label for="show_consecutive_states">Show consecutive state updates</label> type="checkbox"
id="show_consecutive_states"
name="show_consecutive_states"
checked
autocomplete="off"
onchange="updateShowStates()"
/>
<label for="show_consecutive_states">Show consecutive state updates</label>
<table id="message_table"> <table id="message_table">
<tr id="table_header"> <tr id="table_header">
<th>Human-readable type</th> <th>Human-readable type</th>
<th>Sender ID</th> <th>Sender ID</th>
<th>Parsed payload</th> <th>Parsed payload</th>
<th>Time</th> <th>Time</th>
<th class="raw">Raw Message</th> <th class="raw">Raw Message</th>
<th class="raw">Raw ID</th> <th class="raw">Raw ID</th>
</tr> </tr>
</table> </table>
<script src="static/debugscript.js"></script> <script src="/static/js/debugscript.js"></script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,197 @@
/**
* Soundeffects
*/
const sounds = {
strike: new Audio("/static/sounds/strike.mp3"),
};
/**
* State
*/
const state = {
// Segment display used for telling the remaining time.
display: null,
// Current game state.
gamestate: null,
// Estimated timeout date when the timer should be stopped.
estimatedTimeout: new Date(),
// Amount of strikes (wrong answers) the player has made.
strikes: 0,
};
/**
* Play a given audio file
* @param {Audio} sound Audio object containing a sound.
*/
function playSound(sound) {
sound.load();
sound.play();
}
/**
* Update the modules.
* @param {Array} List of puzzles
*/
function updateModules(puzzles) {
// Delete the modules when the game is inactive or in info mode.
if (state.gamestate == "INACTIVE" || state.gamestate == "INFO") {
document.getElementById("modules").innerHTML = "";
return;
}
const modulesElement = document.getElementById("modules");
// Update the modules.
const modules = [];
for (const puzzle of puzzles) {
const moduleElement = document.createElement("div");
moduleElement.textContent = puzzle.address;
// State: Solved
if (puzzle.solved === true) {
moduleElement.className = "solved";
}
// State: Unsolved
if (puzzle.solved === false) {
moduleElement.className = "unsolved";
}
// State: Needy
if (puzzle.solved === null) {
moduleElement.className = "needy";
}
modules.push(moduleElement);
}
modulesElement.replaceChildren(...modules);
}
/**
* Initialize the game state.
*/
function initializeGameState() {
updateGameState();
setInterval(updateGameState, 500);
}
/**
* Update the game state.
*/
function updateGameState() {
fetch("/status.json")
.then((res) => res.json())
.then((data) => {
// Update the game state
state.gamestate = data.gamestate;
document.getElementById("gamestate").innerHTML = state.gamestate;
// Reset the strike amount if the game is not running anymore.
if (state.gamestate != "GAME") {
state.strikeAmount = 0;
}
if (state.gamestate === "GAME") {
// Update the estimated timeout date
state.estimatedTimeout = new Date(
new Date().getTime() + data.timeleft * 1000
);
// Calculate the ew total amount of strikes for all puzzles.
const newStrikes = data.puzzles
.map((p) => p.strikes)
.reduce((a, b) => a + b, 0);
// Play a "buzzer" sound when a strike is made.
if (state.strikes < newStrikes) {
playSound(sounds.strike);
}
// Update the total amount of strikes
state.strikes = newStrikes;
}
// Update the modules
updateModules(data.puzzles);
});
}
/**
* When the start button is clicked.
*/
function onStartButtonClick() {
fetch("/start");
}
/**
* When the restart button is clicked.
*/
function onRestartButtonClick() {
fetch("/restart");
}
/**
* Initialize the segment display.
*/
function initializeSegmentDisplay() {
display = new SegmentDisplay("display");
display.pattern = "##:##.#";
display.displayAngle = 0;
display.digitHeight = 100;
display.digitWidth = display.digitHeight / 2;
display.digitDistance = display.digitHeight / 10;
display.segmentWidth = display.digitHeight / 10;
display.segmentDistance = display.digitHeight / 100;
display.segmentCount = 7;
display.cornerType = 1;
display.colorOn = "#24dd22";
display.colorOff = "#1b4105";
display.draw();
// Update the segment
setInterval(updateSegmentDisplay, 100);
}
/**
* Update the segment display with the latest game data.
*/
function updateSegmentDisplay() {
// Do not update the timer when the game is not running.
if (state.gamestate != "GAME" || !state.estimatedTimeout) {
return;
}
const timeLeft = (state.estimatedTimeout - new Date()) / 1000;
// Set the display to 0 when there is no time left.
if (timeLeft <= 0) {
display.setValue("00:00.0");
return;
}
const integral = Math.floor(timeLeft);
const fractional = timeLeft - integral;
const minutes = Math.floor(integral / 60);
const seconds = integral % 60;
const minutesStr = String(minutes).padStart(2, "0");
const secondsStr = String(seconds).padStart(2, "0");
const fractionalStr = String(Math.floor(fractional * 10)).padStart(1, "0");
display.setValue(`${minutesStr}:${secondsStr}.${fractionalStr}`);
}
/**
* When the window is loaded.
*/
window.addEventListener("load", () => {
// Initiliaz the game state.
initializeGameState();
// Initialize the segment display.
initializeSegmentDisplay();
});