diff options
Diffstat (limited to 'hsm-web/Client/src')
| -rw-r--r-- | hsm-web/Client/src/App.vue | 77 | ||||
| -rw-r--r-- | hsm-web/Client/src/CamStream.vue | 112 | ||||
| -rw-r--r-- | hsm-web/Client/src/INA226.vue | 53 | ||||
| -rw-r--r-- | hsm-web/Client/src/MotorCtl.vue | 162 | ||||
| -rw-r--r-- | hsm-web/Client/src/config.js | 3 | ||||
| -rw-r--r-- | hsm-web/Client/src/main.js | 4 |
6 files changed, 411 insertions, 0 deletions
diff --git a/hsm-web/Client/src/App.vue b/hsm-web/Client/src/App.vue new file mode 100644 index 0000000..4645034 --- /dev/null +++ b/hsm-web/Client/src/App.vue @@ -0,0 +1,77 @@ +<template> + <h1>HsMouse <span id='logo'>ᘛ⁐̤ᕐᐷ</span></h1> + <INA226 /> + <CamStream /> + <MotorCtl /> +</template> + +<script> +import INA226 from './INA226.vue' +import CamStream from './CamStream.vue' +import MotorCtl from './MotorCtl.vue' + +export default { + components: { + INA226, + CamStream, + MotorCtl + } +} +</script> + +<style> +html { + background-color: #002b36; + color: #586e75; + font-family: sans-serif; +} +h1 { + font-size: 24px; + font-weight: 600; +} +h2 { + font-size: 22px; + font-weight: 500; +} +table { + border-collapse: collapse; + border-spacing: 0; + height: 100%; + width: 100%; +} +td { + margin: 0; + padding: 0; +} +button { + background-color: #073642; + border: 1.5px solid #2aa198; + color: #2aa198; + display: block; + min-height: 30px; + height: 100%; + width: 100%; + padding: 0; +} +button:active { + opacity: 0.5; +} +button:disabled { + opacity: 0.5; +} +input { + background-color: transparent; + color: #002b36; + font-family: monospace; + font-size: 16px; + font-weight: bold; + border: none; + margin: 0 4px; + padding: 4px; + resize: none; + width: calc(100% - 16px); +} +#logo { + font-family: monospace; +} +</style> diff --git a/hsm-web/Client/src/CamStream.vue b/hsm-web/Client/src/CamStream.vue new file mode 100644 index 0000000..60cd44d --- /dev/null +++ b/hsm-web/Client/src/CamStream.vue @@ -0,0 +1,112 @@ +<template> + <h2>Camera Stream</h2> + <div id='playerCont'> + <video id='player' muted></video> + </div> + <button @click='toggleStream()' :disabled='disabled'> + {{ command }} + </button> +</template> + +<script> +import axios from 'axios' +import config from './config' +import GstWebRTCAPI from '@tomoxv/gstwebrtc-api/src/gstwebrtc-api.js' + +export default { + data() { + return { + api: null, + listener: null, + player: null, + session: null, + + command: 'Loading', + disabled: true, + streaming: false + } + }, + mounted() { + this.player = document.getElementById('player') + this.monitor() + this.bindStream() + }, + methods: { + // Continuously checks if WebRTC stream is running on server + async monitor() { + const res = await axios.get(config.api + '/isStreaming') + + switch (res.status) { + case 200: + this.command = res.data ? 'Stop' : 'Play' + this.disabled = false + this.streaming = res.data + break + default: + this.command = 'Error' + this.disabled = true + this.streaming = false + } + + setTimeout(this.monitor, 1000) + }, + + // Toggles WebRTC stream + async toggleStream() { + const ep = this.streaming ? '/stopStream' : '/startStream' + const res = await axios.get(config.api + ep) + + if (res.status != 200) { + console.error(res) + } + }, + + // Binds WebRTC stream to video element + bindStream() { + this.api = new GstWebRTCAPI({ + meta: { name: 'WebClient-' + Date.now() }, + signalingServerUrl: 'ws://' + window.location.hostname + ':8443' + }) + + this.listener = { + producerAdded: (producer) => { + console.log("Producer added", producer) + + this.session = this.api.createConsumerSession(producer.id) + this.session.addEventListener('streamsChanged', () => { + if (this.session.streams.length > 0) { + this.player.srcObject = this.session.streams[0] + this.player.play() + } + }) + + this.session.connect() + }, + + producerRemoved: (producer) => { + console.log("Producer removed", producer) + + this.player.pause() + this.player.srcObject = null + this.session = null + } + } + + this.api.registerProducersListener(this.listener) + } + } +} +</script> + +<style> +#playerCont { + background-color: #073642; +} +#player { + aspect-ratio: 32 / 27; + display: block; + margin: auto; + width: 480px; + max-width: 100%; +} +</style> diff --git a/hsm-web/Client/src/INA226.vue b/hsm-web/Client/src/INA226.vue new file mode 100644 index 0000000..dec961b --- /dev/null +++ b/hsm-web/Client/src/INA226.vue @@ -0,0 +1,53 @@ +<template> + <h2>Battery Status</h2> + <table id='tina226'> + <tbody> + <tr> + <td>{{ fmt(reading.voltage, 'V') }}</td> + <td>{{ fmt(reading.current, 'A') }}</td> + <td>{{ fmt(reading.power, 'W') }}</td> + </tr> + </tbody> + </table> +</template> + +<script> +import axios from 'axios' +import config from './config' + +export default { + data() { + return { + reading: { + voltage: 0, + current: 0, + power: 0 + } + } + }, + mounted() { + this.getReading() + }, + methods: { + async getReading() { + const res = await axios.get(config.api + '/ina226') + this.reading = res.data + + setTimeout(this.getReading, 1000) + }, + fmt(val, sfx) { + return val.toFixed(2) + sfx + } + } +} +</script> + +<style> +#tina226 td { + border: 1.5px solid #2aa198; + color: #2aa198; + font-family: monospace; + font-size: 14px; + padding: 5px; +} +</style> diff --git a/hsm-web/Client/src/MotorCtl.vue b/hsm-web/Client/src/MotorCtl.vue new file mode 100644 index 0000000..d7d026e --- /dev/null +++ b/hsm-web/Client/src/MotorCtl.vue @@ -0,0 +1,162 @@ +<template> + <h2>Motor Control</h2> + <table id='tmain'> + <tbody> + <tr> + <td> + <button :class='bdirClass("ccw")' @click='nextDir = "ccw"' :disabled='armed'> + {{ dirIcons.ccw }} + </button> + </td> + <td> + <table id='tmot'> + <tbody> + <tr v-for='ds in [["nw", "n", "ne"], ["w", "", "e"], ["sw", "s", "se"]]' :key='ds.id'> + <td v-for='d in ds' :key='d.id'> + <button v-if='d' :class='bdirClass(d)' @click='nextDir = d' :disabled='armed'> + {{ dirIcons[d] }} + </button> + </td> + </tr> + </tbody> + </table> + </td> + <td> + <button :class='bdirClass("cw")' @click='nextDir = "cw"' :disabled='armed'> + {{ dirIcons.cw }} + </button> + </td> + <td> + <table id='tspeed'> + <tbody> + <tr v-for='s in ["top", "fast", "slow", "slow2", "slow4"]' :key='s.id'> + <td> + <button :class='bspeedClass(s)' @click='nextSpeed = s' :disabled='armed'> + {{ s }} + </button> + </td> + </tr> + </tbody> + </table> + </td> + <td> + <table id='ttime'> + <tbody> + <tr v-for='t in ["4s", "2s", "1s", "0.5s", "0.25s"]' :key='t.id'> + <td> + <button :class='btimeClass(t)' @click='nextTime = t' :disabled='armed'> + {{ t }} + </button> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <table id='tcmd'> + <tbody> + <tr> + <td><input id='ihist' :value='renderCommand()' disabled /></td> + </tr> + </tbody> + </table> + <table id='tarm'> + <tbody> + <tr> + <td><button id='arm' @click='armed = !armed' :disabled='running'>{{ armed ? 'disarm' : 'arm' }}</button></td> + <td><button id='dispatch' @click='dispatch()' :disabled='!armed || running'>dispatch</button></td> + </tr> + </tbody> + </table> +</template> + +<script> +import axios from 'axios' +import config from './config' + +export default { + data() { + return { + dirIcons: { + n: '↑', + ne: '↗', + e: '→', + se: '↘', + s: '↓', + sw: '↙', + w: '←', + nw: '↖', + ccw: '↺', + cw: '↻' + }, + nextDir: 'n', + nextSpeed: 'slow', + nextTime: '1s', + armed: false, + running: false + } + }, + methods: { + bdirClass(d) { + return 'bmot' + (d == this.nextDir ? ' bhigh' : '') + }, + bspeedClass(d) { + return d == this.nextSpeed ? 'bhigh' : '' + }, + btimeClass(t) { + return t == this.nextTime ? 'bhigh' : '' + }, + renderCommand() { + const cmd = ['ccw', 'cw'].includes(this.nextDir) ? 'Tilt' : 'Move' + const dir = this.nextDir.toUpperCase() + const speed = this.nextSpeed.charAt(0).toUpperCase() + this.nextSpeed.slice(1) + const time = this.nextTime.slice(0, -1) + + return `${cmd} ${dir} ${speed} ${time}` + }, + async dispatch() { + this.running = true + + const res = await axios.get(config.api + '/command?cmd=' + this.renderCommand()) + + if (res.status == 200) { + this.armed = false + this.running = false + } + } + } +} +</script> + +<style> +#tmot td { + height: 33%; + width: 33%; +} +#tmain td, #tcmd td { + background-color: #2aa198; +} +.bmot { + font-size: 16px; + font-weight: bold; +} +.bhigh { + background-color: #b58900; + color: #073642; +} +#tarm td:nth-child(1) { + width: 30%; +} +#arm { + background-color: #dc322f; +} +#dispatch { + background-color: #859900; +} +#arm, #dispatch { + color: #073642; + font-weight: bold; +} +</style> diff --git a/hsm-web/Client/src/config.js b/hsm-web/Client/src/config.js new file mode 100644 index 0000000..b2217b7 --- /dev/null +++ b/hsm-web/Client/src/config.js @@ -0,0 +1,3 @@ +module.exports = { + api: `http://${window.location.hostname}:3000` +} diff --git a/hsm-web/Client/src/main.js b/hsm-web/Client/src/main.js new file mode 100644 index 0000000..004c252 --- /dev/null +++ b/hsm-web/Client/src/main.js @@ -0,0 +1,4 @@ +import App from './App.vue' +import { createApp } from 'vue' + +createApp(App).mount('#app') |
