From 95c471e5d80a579e887444feb4f881bc0219a37e Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Tue, 25 Jun 2024 14:58:06 +0200 Subject: [PATCH] refactor(client): :recycle: split source files for readability --- client/app.js | 178 +++++++++++++++++++++++++++ client/index.html | 307 +--------------------------------------------- client/style.css | 114 +++++++++++++++++ 3 files changed, 294 insertions(+), 305 deletions(-) create mode 100644 client/app.js create mode 100644 client/style.css diff --git a/client/app.js b/client/app.js new file mode 100644 index 0000000..7d8f595 --- /dev/null +++ b/client/app.js @@ -0,0 +1,178 @@ +// Input handlers +document + .querySelector('#move-input') + .addEventListener('change', (event) => + assignCommands('backward', 'forward', event) + ) +document + .querySelector('#rotate-input') + .addEventListener('change', (event) => + assignCommands('left', 'right', event) + ) + +// Buttons handlers +for (const button of document + .querySelector('#touch-controls') + .querySelectorAll('button')) { + button.addEventListener('click', () => { + const { command, value } = button.dataset + + // Check datas are set for button + if (command === undefined || value === undefined) { + alert( + `Pas de command ou de valeur assigné au bouton "${button.title}"` + ) + throw new Error(`no command or value assigned to ${button.title}`) + } + + // Check command is valid + if ( + !['forward', 'backward', 'left', 'right', 'stop'].includes(command) + ) { + alert( + `La command "${command}" n'est pas valide en tant que ('forward', 'backward', 'left', 'right', 'stop')` + ) + throw new Error( + `specified command "${command}" is not in ['forward', 'backward', 'left', 'right', 'stop']` + ) + } + + // Check value is valid finite number + if (!Number.isFinite(Number(value))) { + alert(`La valeur "${value}" n'est convertible en entier fini`) + throw new Error( + `value "${value}" cannot be casted to finite integer` + ) + } + + sendCommand(command, Number(value)) + }) +} + +// Keyboard handler +document.addEventListener('keydown', ({ code }) => { + if (code === 'ArrowUp') { + return sendCommand('forward', 2) + } + if (code === 'ArrowDown') { + return sendCommand('backward', 2) + } + if (code === 'ArrowLeft') { + return sendCommand('left', 5) + } + if (code === 'ArrowRight') { + return sendCommand('right', 5) + } + if (code === 'Space') { + return sendCommand('stop', 0) + } +}) + +// Settings handler +document + .querySelector('#settings') + .addEventListener('submit', async (event) => { + // Don't interrupt event if not form submitting + if (!(event instanceof SubmitEvent)) return true + if (event.target === null) return true + // Disable form sending + event.preventDefault() + + const form = new FormData(event.target) + const ipAddress = form.get('ip-address') + + if (ipAddress === null) return + + const endpoint = `http://${ipAddress}` + await testEndpoint(endpoint) + setEndpoint(endpoint) + }) + +/** + * A command string. + * @typedef {('forward' | 'backward' | 'left' | 'right' | 'stop')} Command + */ + +/** + * Send command to the robot. + * + * @param {Command} command Command to send. + * @param {number} value Value of the command if needed. + * + * @returns {Promise} + */ +async function sendCommand(command, value) { + const endpoint = getEndpoint() + const url = new URL(`/get?command=${command}&value=${value}`, endpoint) + const response = await fetch(url) + const text = await response.text() + console.log(text) +} + +/** + * Get robot endpoint address. + * + * @returns {string} + */ +function getEndpoint() { + const endpoint = sessionStorage.getItem('robot-endpoint') + if (endpoint === null) { + alert("Aucune adresse IP n'a été renseignée !") + throw new Error('no given ip address') + } + return endpoint +} + +/** + * Set robot endpoint address. + * + * @param {string} ip Robot ip. + * + * @returns void + */ +function setEndpoint(ip) { + sessionStorage.setItem('robot-endpoint', ip) +} + +/** + * Test connection to endpoint. + * + * @param {string} endpoint Endpoint to test for. + * + * @returns {void} + * @throws {Error} Endpoint unreachable. + */ +async function testEndpoint(endpoint) { + try { + const response = await fetch(endpoint) + if (response.ok) return + } catch (cause) { + alert(`Impossible de joindre l'adresse "${endpoint}"`) + throw new Error(`unable to connect to robot at ${endpoint}`, { + cause, + }) + } +} + +/** + * Assign a command to an input. + * Do negative command if value < 0 else do positive command. + * + * @param {Command} negativeCommand Command if value < 0. + * @param {Command} positiveCommand Command if value >= 0. + * @param {event} event Event of the input. + * + * @returns {Promise} + */ +function assignCommands(negativeCommand, positiveCommand, event) { + if (event.target === null) return + + /** @type {HTMLInputElement} */ + const target = event.target + const value = target.valueAsNumber + + if (value < 0) { + return sendCommand(negativeCommand, Math.abs(value)) + } + return sendCommand(positiveCommand, value) +} diff --git a/client/index.html b/client/index.html index d56dcfe..7c13768 100644 --- a/client/index.html +++ b/client/index.html @@ -3,6 +3,8 @@ + + Contrôle du robot @@ -104,309 +106,4 @@ - - diff --git a/client/style.css b/client/style.css new file mode 100644 index 0000000..4e76cd8 --- /dev/null +++ b/client/style.css @@ -0,0 +1,114 @@ +:root { + --translucent-high: rgba(255, 255, 255, 0.8); + --translucent-low: rgba(255, 255, 255, 0.4); + --padding: 0.5rem; + --padding-half: calc(var(--padding) / 2); + --border-size: 0.2rem; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + background-color: rgb(247, 234, 219); + height: 100dvh; + margin: 0; + padding: 0; + + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; +} + +h1, +h2 { + margin: 0; +} + +h1 { + text-align: center; +} + +main { + padding: var(--padding); + height: 100%; + text-wrap: pretty; +} + +.touch-controls { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--padding); + padding: var(--padding); + + button { + height: 5.5rem; + font-size: 3rem; + border-radius: var(--padding); + } +} + +button { + border: solid var(--border-size) var(--translucent-high); + background: var(--translucent-high); + border-radius: var(--padding-half); + transition: all 0.2s ease; + height: 100%; + font-size: 110%; + + &:active { + border-color: var(--translucent-low); + background-color: var(--translucent-low); + } +} + +.input-controls, +.settings { + padding: var(--padding); + display: flex; + gap: var(--padding); + border: solid var(--border-size) var(--translucent-high); + border-radius: var(--padding-half); + align-items: center; + justify-content: space-around; +} + +.inputs { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(700px, 1fr)); + gap: var(--padding); + width: 100%; +} + +.infos { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(800px, 1fr)); + flex-wrap: wrap; + gap: var(--padding); + justify-content: space-between; +} + +label { + display: grid; + gap: var(--padding); +} + +input { + border: solid var(--border-size) var(--translucent-high); + background: var(--translucent-low); + border-radius: var(--padding-half); + transition: all 0.2s ease; + height: 100%; + padding: var(--padding-half); + font-family: 'Courier New', Courier, monospace; + outline: none; + + &:focus { + background-color: var(--translucent-high); + } +}