aboutsummaryrefslogtreecommitdiff
path: root/hsm-web/Client/src
diff options
context:
space:
mode:
Diffstat (limited to 'hsm-web/Client/src')
-rw-r--r--hsm-web/Client/src/App.vue77
-rw-r--r--hsm-web/Client/src/CamStream.vue112
-rw-r--r--hsm-web/Client/src/INA226.vue53
-rw-r--r--hsm-web/Client/src/MotorCtl.vue162
-rw-r--r--hsm-web/Client/src/config.js3
-rw-r--r--hsm-web/Client/src/main.js4
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')