Initial push

This commit is contained in:
Ano-sys
2026-02-22 23:30:15 +01:00
commit 7a2d858dfb
58 changed files with 9896 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+29
View File
@@ -0,0 +1,29 @@
# cotmw
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connection on the Move - Webinterface</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "cotmw",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "npx -y vite build && npx -y vite preview --port 3000 --host 0.0.0.0"
},
"dependencies": {
"axios": "^1.9.0",
"bootstrap": "^5.3.5",
"cors": "^2.8.5",
"express": "^5.1.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"concurrently": "^9.1.2",
"nodemon": "^3.1.10",
"vite": "^6.3.4",
"vite-plugin-vue-devtools": "^7.7.2"
},
"description": "This template should help get you started developing with Vue 3 in Vite.",
"main": "vite.config.js",
"keywords": [],
"author": "",
"license": "ISC"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@serialport/parser-readline": "^13.0.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"serialport": "^13.0.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}
@@ -0,0 +1,42 @@
// server/routes/serialUtils.js
const router = require('express').Router();
const { listSerialPorts, connectSerial, getLatestPacket } = require('../serial');
router.get('/serialports', async (req, res) => {
try {
const ports = await listSerialPorts();
res.json(ports);
} catch (err) {
console.error('Error listing ports:', err);
res.status(500).json({ error: 'Could not list serial ports' });
}
});
router.get('/latest', (req, res) => {
const payload = getLatestPacket();
if (!payload) {
return res.status(204).end(); // No Content
}
res.json(payload);
});
router.post('/connect', (req, res) => {
const { path } = req.body;
if (!path) {
return res.status(400).json({ error: 'Path is required' });
}
try {
connectSerial(path);
res.json({
ok: true,
connectedTo: path,
baudRate: 115200
});
} catch (err) {
console.error('Failed to connect:', err);
res.status(500).json({ error: 'Could not open serial port' });
}
});
module.exports = router;
+39
View File
@@ -0,0 +1,39 @@
// server/serial.js
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
let portInstance = null;
let parser = null;
let latestPayload = null;
let latestImage = null;
function connectSerial(path) {
if (portInstance?.isOpen) portInstance.close();
portInstance = new SerialPort({ path: path, baudRate: 115200 });
parser = portInstance.pipe(new ReadlineParser({ delimiter: '\n' }));
parser.on('data', line => {
if(line.includes("image")){
if(latestImage != null && !latestImage.endsWith("}"))
latestImage += line.trim().replace("\n", "");
else
latestImage = line.trim().replace("\n", "");
console.log(latestImage)
}
else if (line.includes("text")){
console.log(latestPayload)
latestPayload = line.trim();
}
});
portInstance.on('error', console.error);
}
function listSerialPorts() {
return SerialPort.list();
}
function getLatestPacket() {
return [latestPayload, latestImage];
}
module.exports = { connectSerial, listSerialPorts, getLatestPacket };
+11
View File
@@ -0,0 +1,11 @@
const express = require('express');
const cors = require('cors');
const path = require('path');
const serialRouter = require('./routes/serialUtils');
const app = express();
app.use(cors());
app.use(express.json());
app.use('/api/serial-utils', serialRouter);
app.listen(6666, () => console.log('running on port 6666'));
+127
View File
@@ -0,0 +1,127 @@
<script setup>
import 'bootstrap/js/dist/modal.js'
import HUD from './components/HUD.vue'
import SerialConnector from "@/components/SerialConnector.vue";
import { ref, computed } from 'vue';
import Camera from "@/components/Camera.vue";
import Location from "@/components/Location.vue";
import Plot from './components/Plot.vue';
import External from './components/External.vue';
const serialPacket = ref(null);
let ip = ref("***.***.***.***");
let base_station = ref("not connected");
let rssi = ref("not available");
let other_network_ssid = ref("not available")
let other_network_rssi = ref("not available")
let pollingCount = ref(0);
let imageData = ref("");
const rowProps = computed(() =>
[{
key: "IP",
value: ip,
importance: "info"
},{
key: "Station",
value: base_station,
importance: "important"
}, {
key: "RSSI",
value: rssi,
importance: "important"
}, {
key: "Other Network",
value: other_network_ssid,
importance: "info2"
}, {
key: "Other RSSI",
value: other_network_rssi,
importance: "info2"
}, {
key: "Polling Counter",
value: pollingCount,
importance: "info2"
},
]
);
function parseTextPacket(packet){
ip.value = packet["IP"];
base_station.value = packet["Base Station"];
rssi.value = packet["RSSI"];
other_network_ssid.value = packet["Other Network"];
other_network_rssi.value = packet["Other RSSI"];
}
function parseImagePacket(packet){
// console.log(packet["data"]);
let val = packet["data"];
if(!packet["data"].includes("data:image/jpeg;base64,"))
val = "data:image/jpeg;base64," + val;
if(!val.endsWith("\n")) val += "\n";
console.log(val);
imageData.value = val;
}
function onSerialPacket(payload){
if(payload[0] && payload[0]["type"] === "text"){
parseTextPacket(payload[0]);
}
if(payload[1] && payload[1]["type"] === "image"){
parseImagePacket(payload[1]);
}
pollingCount.value++;
// console.log(payload);
}
</script>
<template>
<header>
<!--
<img alt="Raspberry Pi Minimalistic Icon" class="logo" src="./assets/icon_pi.png" width="128" height="128"/>
-->
<meta charset="utf-8">
</header>
<main>
<div>
<HUD :rowProps="rowProps" @CameraButtonClicked="enableCamera" @LocationButtonClicked="enableLocation"></HUD>
<SerialConnector @data="onSerialPacket"></SerialConnector>
<Camera :data="imageData"></Camera>
<Location :baseStation="base_station" :otherNetwork="other_network_ssid" ></Location>
<Plot :rssi="rssi" :otherRssi="other_network_rssi" />
<External :rssi="rssi" :other_rssi="other_network_rssi" :image="imageData"/>
</div>
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+80
View File
@@ -0,0 +1,80 @@
.hud {
position: fixed;
top: 20px;
left: 20px;
width: 240px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid #444;
border-radius: 8px;
padding: 10px;
font-family: monospace;
color: #ddd;
user-select: none;
z-index: 9999;
}
.hud-header {
display: flex;
justify-content: space-between;
font-size: 13px;
font-family: monospace;
color: #eeeeee;
}
.hud-body {
font-family: monospace;
}
.hud-row {
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 1.2;
margin-bottom: 4px;
font-family: monospace;
user-select: none;
}
.hud-inner-btn{
width: 100px;
border: 1px solid #444;
border-radius: 8px;
font-family: monospace;
}
.hud-toggle-btn {
background: transparent;
border: none;
color: #ddd;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.spacer{
height: 1px;
background: transparent;
margin-top: 8px;
margin-bottom: 8px;
}
.label {
color: #d6d6d6;
}
.important {
color: rgb(255, 52, 52);
}
.info {
color: #48f;
}
.success {
color: #4f4;
}
.info2 {
color: #8cf;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+35
View File
@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
@@ -0,0 +1,82 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps(['data']);
const cameraBox = ref(null);
let isDragging = false;
let offset = { x: 0, y: 0 };
const startDrag = (event) => {
isDragging = true;
const box = cameraBox.value;
const rect = box.getBoundingClientRect();
offset.x = event.clientX - rect.left;
offset.y = event.clientY - rect.top;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
};
const onDrag = (event) => {
if (!isDragging) return;
const x = event.clientX - offset.x;
const y = event.clientY - offset.y;
const box = cameraBox.value;
box.style.left = `${x}px`;
box.style.top = `${y}px`;
};
const stopDrag = () => {
isDragging = false;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
};
onMounted(() => {
const box = cameraBox.value;
box.style.left = 'calc(100vw - 30vw)';
box.style.top = 'calc(100vh - 30vw)';
});
</script>
<template>
<div ref="cameraBox" class="camera-box">
<div class="camera-header" @mousedown="startDrag">Camera Feed</div>
<img class="camera-image" :src="props.data" alt="Camera" />
</div>
</template>
<style scoped>
.camera-box {
position: fixed;
width: 25vw;
height: 25vw;
background-color: #000;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
display: flex;
flex-direction: column;
cursor: move;
}
.camera-header {
background-color: #222;
color: #fff;
padding: 6px 12px;
font-size: 14px;
font-weight: bold;
border-bottom: 1px solid #444;
cursor: grab;
user-select: none;
}
.camera-image {
flex-grow: 1;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
@@ -0,0 +1,149 @@
<script setup>
import { ref, watch } from 'vue'
// Props empfangen
const props = defineProps({
rssi: String,
other_rssi: String,
image: String
})
const status = ref('Nothing send yet...')
const passwort = ref('')
const ipAdresse = ref('138.68.114.176')
//const ipAdresse = ref('localhost')
const port = ref('3000')
const isAuthenticated = ref(false)
const senden = async (payload) => {
const url = `http://${ipAdresse.value}:${port.value}/api/send`
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': passwort.value
},
body: JSON.stringify(payload)
})
if (!res.ok) throw new Error('Error wile sending')
status.value = 'Sending!'
} catch (err) {
console.error(err)
status.value = 'Error'
}
}
// sende nur wenn sich was verändert
watch(
() => [props.rssi, props.other_rssi, props.image],
([rssi, other_rssi, newImage]) => {
if (!isAuthenticated.value) return
if (!rssi && !other_rssi && !newImage) return
senden({
rssi: rssi,
other_rssi: other_rssi,
image: newImage
})
},
{ immediate: false }
)
const aktivieren = () => {
if (!passwort.value.trim() || !ipAdresse.value.trim() || !port.value.trim()) {
status.value = 'Please fill in all fields'
} else {
isAuthenticated.value = true
status.value = 'Activated'
}
}
</script>
<template>
<div class="data-box">
<p class="title">External Data Transmission</p>
<div v-if="!isAuthenticated">
<input
v-model="ipAdresse"
type="text"
placeholder="IP Address"
class="input"
/>
<input
v-model="port"
type="text"
placeholder="Port"
class="input"
/>
<input
v-model="passwort"
type="password"
placeholder="API Key"
class="input"
/>
<button @click="aktivieren" class="button">Activate</button>
</div>
<p class="status">{{ status }}</p>
</div>
</template>
<style scoped>
.data-box {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
width: 18vw;
background: rgba(50, 50, 50, 0.85);
z-index: 10;
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.title {
font-size: 1rem;
color: white;
text-align: center;
margin-bottom: 0.5rem;
}
.status {
color: #fff;
margin-top: 0.5rem;
font-size: 0.9rem;
text-align: center;
}
.input {
padding: 0.3rem;
font-size: 0.8rem;
border-radius: 4px;
border: none;
margin-bottom: 0.4rem;
text-align: center;
}
.button {
font-size: 0.8rem;
padding: 0.3rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #2563eb;
}
</style>
+112
View File
@@ -0,0 +1,112 @@
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
import HUDRow from "@/components/HUDRow.vue";
const collapsed = ref(false);
const hud = ref(null);
const pos = reactive({ x: 20, y: 20 });
const dragging = ref(false);
const dragOffset = { x: 0, y: 0 };
const logging = false;
function onMouseDown(e){
log("Dragging started!");
dragging.value = true;
dragOffset.x = e.clientX - pos.x;
dragOffset.y = e.clientY - pos.y;
e.preventDefault();
}
function onMouseMove(e){
if(!dragging.value) return;
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
const hudRect = hud.value.getBoundingClientRect();
const hudW = hudRect.width;
const hudH = hudRect.height;
const docW = document.documentElement.scrollWidth;
const docH = document.documentElement.scrollHeight;
const minX = 20;
const minY = 20;
const maxX = docW - hudW - 20;
const maxY = docH - hudH - 20;
pos.x = Math.min(Math.max(x, minX), maxX);
pos.y = Math.min(Math.max(y, minY), maxY);
log(`Location ${pos.x}, ${pos.y}`);
}
function onMouseUp(){
log("Dragging done!");
dragging.value = false;
}
onMounted(() => {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
})
function toggleHUD() {
collapsed.value = !collapsed.value;
log("HUD toggled!");
}
function log(message){
if(logging)
console.log(message);
}
// HUD TEMPLATE
let HUD_name = "Auto HUD"
const props = defineProps(["rowProps"]);
const rowProps = props.rowProps;
</script>
<template>
<div class="container">
<div ref="hud" class="hud" id="Control" :style="{ top: pos.y + 'px', left: pos.x + 'px', position: 'absolute', cursor: dragging ? 'grabbing' : 'grab' }" @mousedown="onMouseDown">
<div class="hud-header" @click="toggleHUD">
<span>{{ HUD_name }}</span>
<button class="hud-toggle-btn">{{ collapsed ? '▼' : '▲' }}</button>
</div>
<transition name="expand">
<div v-show="!collapsed" class="hud-body">
<div class="spacer"></div>
<HUDRow v-for="rowProp in rowProps" :rowProps="rowProp"></HUDRow>
<div class="spacer"></div>
</div>
</transition>
</div>
</div>
</template>
<style scoped>
.expand-enter-from{
opacity: 0;
}
.expand-enter-to{
opacity: 1;
}
.expand-enter-active{
transition:
transform 0.2s ease,
opacity 0.2s ease;
}
</style>
@@ -0,0 +1,16 @@
<script setup>
const props = defineProps(['rowProps'])
</script>
<template>
<div class="hud-row">
<div class="label">{{ props.rowProps.key }}:</div>
<div :class="props.rowProps.importance">{{ props.rowProps.value }}</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,129 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import IconAntenna from './icons/IconAntenna.vue'
import IconVan from './icons/IconVan.vue'
import IconCar from './icons/IconCar.vue'
// Props: Die aktuellen Netzwerknamen (kommen aus App.vue)
const props = defineProps({
baseStation: String,
otherNetwork: String
})
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const pointA = ref({ x: 0, y: 0, tx: 0, ty: 0 })
const pointB = ref({ x: 0, y: 0, tx: 0, ty: 0 })
const pointC = ref({ x: 0, y: 0, tx: 0, ty: 0 })
const handover = ref(false)
const previousBaseStation = ref(null)
onMounted(() => {
function updateSize() {
width.value = window.innerWidth
height.value = window.innerHeight
// Mittelpunkt des Screens
const centerX = width.value / 2
const centerY = height.value / 2
const radius = 150 // Abstand der Punkte zum Zentrum
// Drei gleichmäßig im Kreis verteilte Punkte (Dreieck)
pointA.value = { x: centerX, y: centerY - radius } // oben
pointB.value = { x: centerX - radius * Math.sin(Math.PI / 3), y: centerY + radius / 2 } // unten links
pointC.value = { x: centerX + radius * Math.sin(Math.PI / 3), y: centerY + radius / 2 } // unten rechts
}
updateSize()
window.addEventListener('resize', updateSize)
})
// wenn sich die Base Station ändert => handover
watch(() => props.baseStation, (newVal) => {
if (previousBaseStation.value && newVal !== previousBaseStation.value) {
handover.value = true
} else {
handover.value = false
}
previousBaseStation.value = newVal
})
</script>
<template>
<div class="checkmate-pattern">
<svg class="overlay" :width="width" :height="height">
<line :x1="pointA.x" :y1="pointA.y" :x2="pointB.x" :y2="pointB.y" stroke="red" stroke-width="3" class="flow-line"/>
<line v-if="handover" :x1="pointB.x" :y1="pointB.y" :x2="pointC.x" :y2="pointC.y" stroke="red" stroke-width="3" class="flow-line"/>
<line v-if="!handover" :x1="pointA.x" :y1="pointA.y" :x2="pointC.x" :y2="pointC.y" stroke="black" stroke-width="3" class="flow-line"/>
<circle :cx="pointA.x" :cy="pointA.y" r="25" fill="black" />
<foreignObject
:x="pointA.x - 15"
:y="pointA.y - 15"
width="30"
height="30"
>
<IconAntenna color="white" style="width: 30px; height: 30px;" />
</foreignObject>
<circle :cx="pointB.x" :cy="pointB.y" r="25" fill="red" />
<foreignObject
:x="pointB.x - 15"
:y="pointB.y - 15"
width="30"
height="30"
>
<IconVan color="white" style="width: 30px; height: 30px;" />
</foreignObject>
<circle :cx="pointC.x" :cy="pointC.y" r="25" fill="blue" />
<foreignObject
:x="pointC.x - 15"
:y="pointC.y - 15"
width="30"
height="30"
>
<IconCar color="white" style="width: 30px; height: 30px;" />
</foreignObject>
</svg>
</div>
</template>
<style scoped>
@keyframes flowing {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -20;
}
}
.flow-line {
stroke-dasharray: 10;
stroke-dashoffset: 0;
animation: flowing 1s linear infinite;
}
.checkmate-pattern {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
background-color: #fff;
background-image:
repeating-linear-gradient(to bottom, #ccc 0px, #ccc 1px, transparent 1px, transparent 20px),
repeating-linear-gradient(to right, #ccc 0px, #ccc 1px, transparent 1px, transparent 20px);
overflow: hidden;
}
.overlay {
position: absolute;
top: 0;
left: 0;
}
</style>
+203
View File
@@ -0,0 +1,203 @@
<template>
<div
class="canvas-container"
:style="{ top: `${position.y}px`, left: `${position.x}px` }"
@mousedown="startDrag"
>
<h2 class="chart-title">RSSI</h2>
<canvas ref="canvas" :width="width" :height="height"></canvas>
</div>
</template>
<script>
export default {
name: 'RSS Plot',
props: {
rssi: {
type: [String, Number],
default: "not available"
},
otherRssi: {
type: [String, Number],
default: "not available"
}
},
data() {
return {
width: 600,
height: 120,
maxPoints: 50,
currentNetworkData: [],
otherNetworkData: [],
ctx: null,
interval: null,
padding: {
top: 10,
right: 10,
bottom: 20,
left: 35
},
isDragging: false,
dragOffset: { x: 0, y: 0 },
position: { x: 50, y: window.innerHeight - 150 }
};
},
mounted() {
const canvas = this.$refs.canvas;
this.ctx = canvas.getContext('2d');
this.interval = setInterval(() => {
this.addRealData();
this.draw();
}, 1000);
window.addEventListener('mousemove', this.onDrag);
window.addEventListener('mouseup', this.stopDrag);
},
beforeUnmount() {
clearInterval(this.interval);
window.removeEventListener('mousemove', this.onDrag);
window.removeEventListener('mouseup', this.stopDrag);
},
methods: {
startDrag(e) {
if (e.target.classList.contains('chart-title')) {
this.isDragging = true;
this.dragOffset = {
x: e.clientX - this.position.x,
y: e.clientY - this.position.y
};
}
},
onDrag(e) {
if (this.isDragging) {
this.position.x = e.clientX - this.dragOffset.x;
this.position.y = e.clientY - this.dragOffset.y;
}
},
stopDrag() {
this.isDragging = false;
},
addRealData() {
if (this.currentNetworkData.length >= this.maxPoints) this.currentNetworkData.shift();
if (this.otherNetworkData.length >= this.maxPoints) this.otherNetworkData.shift();
let currentRssi = parseFloat(this.rssi);
if (isNaN(currentRssi) || this.rssi === "not available") currentRssi = -100;
this.currentNetworkData.push(currentRssi);
let otherRssi = parseFloat(this.otherRssi);
if (isNaN(otherRssi) || this.otherRssi === "not available") otherRssi = -100;
this.otherNetworkData.push(otherRssi);
},
draw() {
const { ctx, width, height, maxPoints, padding } = this;
ctx.clearRect(0, 0, width, height);
const drawArea = {
x: padding.left,
y: padding.top,
w: width - padding.left - padding.right,
h: height - padding.top - padding.bottom
};
const drawYAxis = () => {
ctx.beginPath();
ctx.moveTo(drawArea.x, drawArea.y);
ctx.lineTo(drawArea.x, drawArea.y + drawArea.h);
ctx.stroke();
};
const drawXAxis = () => {
ctx.beginPath();
ctx.moveTo(drawArea.x, drawArea.y + drawArea.h);
ctx.lineTo(drawArea.x + drawArea.w, drawArea.y + drawArea.h);
ctx.stroke();
};
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
drawYAxis();
drawXAxis();
ctx.fillStyle = 'white';
ctx.font = '10px sans-serif';
ctx.textAlign = 'right';
const maxY = -30;
const minY = -90;
const steps = 6;
for (let i = 0; i <= steps; i++) {
const value = maxY - ((maxY - minY) / steps) * i;
const y = drawArea.y + ((i / steps) * drawArea.h);
ctx.fillText(value.toFixed(0), drawArea.x - 5, y + 3);
ctx.beginPath();
ctx.moveTo(drawArea.x, y);
ctx.lineTo(drawArea.x + 4, y);
ctx.stroke();
}
const drawLine = (data, color, label) => {
if (data.length === 0) return;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
data.forEach((yVal, i) => {
const x = drawArea.x + (drawArea.w / maxPoints) * i;
const normalized = Math.max(0, Math.min(1, (yVal - minY) / (maxY - minY)));
const y = drawArea.y + (1 - normalized) * drawArea.h;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
if (data.length > 0) {
ctx.fillStyle = color;
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
const legendY = drawArea.y + 15 + (label === 'Current' ? 0 : 15);
ctx.fillText(`${label}: ${data[data.length - 1].toFixed(0)} dBm`, drawArea.x + drawArea.w - 120, legendY);
}
};
drawLine(this.currentNetworkData, '#4CAF50', 'Current');
drawLine(this.otherNetworkData, '#f44336', 'Other');
}
}
};
</script>
<style scoped>
.canvas-container {
position: absolute;
width: 50vw;
height: 15vh;
background: rgba(50, 50, 50, 0.8);
z-index: 10;
border-radius: 8px;
padding: 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.2);
cursor: move;
}
.chart-title {
margin: 0;
font-size: 1rem;
color: white;
text-align: center;
margin-bottom: 0.25rem;
cursor: grab;
}
canvas {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,241 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import {
getSerialPorts,
connectSerial,
getLatestPacket
} from '@/services/serialService';
const emit = defineEmits(['data']);
let enableSerialConnectorMenu = ref(false);
const ports = ref([]);
const selectedPort = ref('');
const connecting = ref(false);
const connected = ref(false);
const error = ref('');
const content = ref('');
const connectedPort = ref('');
let intervalId;
// Keyboard event handler for menu toggle
async function handleKeyDown(event) {
// Log SHA256 of the event.key
const encoder = new TextEncoder();
const data = encoder.encode(event.key);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
console.log(`SHA256 of key "${event.key}": ${hashHex}`);
const SECRET_ACTIVATION_KEY_HASH_HIDDEN = "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d";
// Check for Ctrl+ (Windows/Linux) or Cmd+ (Mac)
if ((event.ctrlKey || event.metaKey) && hashHex === SECRET_ACTIVATION_KEY_HASH_HIDDEN) {
event.preventDefault(); // Prevent default browser behavior
switchBtnState();
console.log("Menu toggled");
}
else {
console.log("Key not recognized -- guess again ;)");
}
}
// load ports
onMounted(async () => {
try {
const res = await getSerialPorts();
ports.value = res.data;
} catch {
error.value = 'Could not load ports!';
}
// Add keyboard event listener
document.addEventListener('keydown', handleKeyDown);
});
onBeforeUnmount(() => {
clearInterval(intervalId);
// Remove keyboard event listener
document.removeEventListener('keydown', handleKeyDown);
});
async function onConnect() {
connecting.value = true;
error.value = '';
try {
await connectSerial(selectedPort.value);
connected.value = true;
connectedPort.value = selectedPort.value;
intervalId = setInterval(pollLatest, 200);
} catch {
error.value = 'Connection failed!';
} finally {
connecting.value = false;
}
}
async function pollLatest() {
try {
const res = await getLatestPacket();
if (res.status === 200 && res.data) {
// const line = typeof res.data === 'object'
// ? JSON.stringify(res.data)
// : res.data;
content.value = res.data[0] + '\n' + res.data[1] + '\n';
emit('data', [JSON.parse(res.data[0]), JSON.parse(res.data[1])]);
}
} catch (e) {
console.error(e);
}
}
function switchBtnState(){
enableSerialConnectorMenu.value = !enableSerialConnectorMenu.value;
}
</script>
<template>
<div class="serial-connector">
<transition name="slide">
<nav class="navbar-side-panel" v-if="enableSerialConnectorMenu">
<div class="container-fluid">
<label for="port">Choose Serial Port:</label>
<select id="port" v-model="selectedPort">
<option disabled value=""> select device </option>
<option v-for="port in ports" :key="port.path" :value="port.path">
{{ port.path }}
<small v-if="port.manufacturer">({{ port.manufacturer }})</small>
</option>
</select>
<button
@click="onConnect"
:disabled="!selectedPort || connecting || (connected && selectedPort === connectedPort)"
>
{{ connecting ? 'Connecting...' : 'Connect (Baud: 115200)' }}
</button>
<p v-if="connected">
Connected to <strong>{{ selectedPort }}</strong> @ 115200 bps
</p>
<p v-if="error" class="error">{{ error }}</p>
<h4>Incoming Data:</h4>
<pre class="log">{{ content }}</pre>
</div>
</nav>
</transition>
</div>
</template>
<style scoped>
.serial-connector {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.navbar-side-panel {
position: fixed;
top: 20px;
right: 20px;
width: 250px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid #444;
border-radius: 8px;
padding: 10px;
font-family: monospace;
color: #ddd;
user-select: none;
z-index: 9999;
overflow: hidden;
box-shadow: -4px 0 8px rgba(0,0,0,0.2);
}
.navbar-side-panel .container-fluid {
margin: 5px;
background: transparent !important;
padding: 0;
}
.keyboard-hint {
text-align: center;
margin-bottom: 10px;
padding: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.keyboard-hint small {
color: #aaa;
font-size: 11px;
}
.navbar-side-panel label {
display: block;
font-size: 14px;
color: #d6d6d6;
margin-bottom: 4px;
}
.navbar-side-panel select, .navbar-side-panel button {
font-family: monospace;
border: 1px solid #444;
border-radius: 8px;
background: transparent;
color: #ddd;
padding: 4px 8px;
margin-bottom: 8px;
cursor: pointer;
width: 95%;
}
.navbar-side-panel button:disabled {
opacity: 0.5;
cursor: default;
}
.navbar-side-panel p {
margin: 4px 0;
font-size: 14px;
width: 95%;
}
.navbar-side-panel .error {
color: rgb(255, 52, 52);
}
.navbar-side-panel p strong {
color: #4f4;
}
.navbar-side-panel h4 {
font-size: 14px;
color: #d6d6d6;
margin: 8px 0 4px;
}
.navbar-side-panel .log {
background: #111;
padding: 8px;
height: 120px;
width: 95%;
overflow-y: auto;
white-space: pre-wrap;
font-size: 12px;
color: #ccc;
border: 1px solid #444;
border-radius: 4px;
}
.slide-enter-from, .slide-leave-to {
transform: translateX(100%);
}
.slide-enter-to, .slide-leave-from {
transform: translateX(0);
}
.slide-enter-active, .slide-leave-active {
transition: transform 0.3s ease-out;
}
</style>
@@ -0,0 +1,61 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
:fill="color"
height="800px"
width="800px"
version="1.1"
id="Layer_1"
viewBox="0 0 508.075 508.075"
xml:space="preserve"
>
<g>
<g>
<path d="M446.05,4.225c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c71.8,72.5,71.8,190.4,0,262.8c-5.5,5.5-5.4,14.4,0.1,20 c7.6,7.6,17.2,2.7,20-0.1C528.75,223.325,528.75,87.625,446.05,4.225z"/>
</g>
</g>
<g>
<g>
<path d="M392.95,43.725c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c50.2,50.7,50.2,133.2,0,183.9 c-5.5,5.5-5.4,14.4,0.1,20c8.2,8.2,17.2,2.7,20-0.1C454.05,205.625,454.05,105.325,392.95,43.725z"/>
</g>
</g>
<g>
<g>
<path d="M339.95,83.225c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c28.6,28.8,28.6,75.9,0,104.8c-5.5,5.5-5.4,14.4,0.1,20 c6.4,6.4,17.2,2.7,20-0.1C379.45,187.925,379.45,123.025,339.95,83.225z"/>
</g>
</g>
<g>
<g>
<path d="M82.05,24.125c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-82.6,83.4-82.6,219.1,0,302.6c2.8,2.8,12.4,7.6,20,0.1 c5.5-5.5,5.6-14.4,0.1-20C10.25,214.425,10.25,96.525,82.05,24.125z"/>
</g>
</g>
<g>
<g>
<path d="M135.15,63.525c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-61.1,61.6-61.1,162,0,223.6c2.8,2.9,11.8,8.4,20,0.2 c5.5-5.5,5.6-14.4,0.1-20C84.95,196.725,84.95,114.225,135.15,63.525z"/>
</g>
</g>
<g>
<g>
<path d="M188.35,103.025c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-39.5,39.9-39.5,104.8,0,144.7c2.8,2.8,13.5,6.5,20,0.1 c5.5-5.5,5.6-14.4,0.1-20C159.65,179.025,159.65,131.925,188.35,103.025z"/>
</g>
</g>
<g>
<g>
<path d="M375.35,479.825h-29.5l-74.4-281.3c16.9-6.9,28.8-23.6,28.8-43.1c0-25.6-20.7-46.5-46.2-46.5s-46.2,20.9-46.2,46.5 c0,19.4,11.9,36.1,28.8,43.1l-74.4,281.3h-29.5c-7.8,0-14.1,6.3-14.1,14.1c0,7.8,6.3,14.1,14.1,14.1h242.7 c7.8,0,14.1-6.3,14.1-14.1C389.55,486.125,383.15,479.825,375.35,479.825z M236.05,155.525c0-10.1,8.1-18.3,18-18.3 c9.9,0,18,8.2,18,18.3c0,10.1-8.1,18.3-18,18.3C244.15,173.825,236.05,165.625,236.05,155.525z M191.35,479.825l62.7-236.8 l62.7,236.8H191.35z"/>
</g>
</g>
</svg>
</template>
<script>
export default {
name: 'IconAntenna',
props: {
color: {
type: String,
default: '#000000'
}
}
}
</script>
@@ -0,0 +1,76 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" :fill="color" height="800px" width="800px" version="1.1" id="Layer_1" viewBox="0 0 511.999 511.999" xml:space="preserve">
<g>
<g>
<path d="M358.57,0H153.43c-27.195,0-49.319,22.124-49.319,49.319v413.36c0,27.195,22.124,49.319,49.319,49.319H358.57 c27.195,0,49.319-22.124,49.319-49.319V49.319C407.89,22.124,385.764,0,358.57,0z M124.508,49.319 c0-15.948,12.974-28.921,28.921-28.921h205.141c15.948,0,28.921,12.974,28.921,28.921v355.589H124.508V49.319z M387.492,462.681 L387.492,462.681c-0.001,15.947-12.975,28.921-28.922,28.921H153.43c-15.948,0-28.921-12.974-28.921-28.921v-37.374h262.983 V462.681z"/>
</g>
</g>
<g>
<g>
<path d="M270.789,34.677h-29.578c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h29.578 c5.633,0,10.199-4.566,10.199-10.199C280.988,39.243,276.422,34.677,270.789,34.677z"/>
</g>
</g>
<g>
<g>
<path d="M242.204,371.251h-91.255c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h91.255 c5.633,0,10.199-4.566,10.199-10.199C252.403,375.817,247.837,371.251,242.204,371.251z"/>
</g>
</g>
<g>
<g>
<path d="M282.518,371.251h-5.346c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h5.346 c5.633,0,10.199-4.566,10.199-10.199C292.717,375.817,288.151,371.251,282.518,371.251z"/>
</g>
</g>
<g>
<g>
<path d="M256,430.406c-14.903,0-27.028,12.125-27.028,27.028s12.125,27.028,27.028,27.028s27.028-12.125,27.028-27.028 S270.903,430.406,256,430.406z M256,464.064c-3.655,0-6.629-2.974-6.629-6.63s2.974-6.629,6.629-6.629s6.63,2.974,6.63,6.629 S259.655,464.064,256,464.064z"/>
</g>
</g>
<g>
<g>
<path d="M442.611,198.005c-3.983-3.983-10.441-3.983-14.425,0c-3.983,3.983-3.983,10.441,0,14.425 c23.462,23.462,23.462,61.639,0,85.1c-3.983,3.983-3.983,10.441,0,14.425c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987C474.026,280.539,474.026,229.421,442.611,198.005z"/>
</g>
</g>
<g>
<g>
<path d="M470.016,170.6c-3.983-3.983-10.441-3.983-14.425,0c-3.983,3.983-3.983,10.441,0,14.425 c38.574,38.573,38.574,101.337,0,139.912c-3.983,3.983-3.983,10.441,0,14.425c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987c22.538-22.539,34.952-52.506,34.952-84.38S492.555,193.139,470.016,170.6z"/>
</g>
</g>
<g>
<g>
<path d="M83.812,212.43c3.984-3.984,3.984-10.442,0.001-14.425s-10.441-3.983-14.425,0c-31.416,31.416-31.416,82.533,0,113.95 c1.992,1.992,4.602,2.987,7.212,2.987s5.221-0.995,7.212-2.987c3.983-3.983,3.983-10.441,0-14.425 C60.35,274.068,60.35,235.891,83.812,212.43z"/>
</g>
</g>
<g>
<g>
<path d="M56.408,185.025c3.983-3.983,3.983-10.441,0-14.425c-3.983-3.983-10.441-3.983-14.425,0 c-22.538,22.539-34.951,52.505-34.951,84.38s12.412,61.841,34.952,84.38c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987c3.983-3.983,3.983-10.441,0-14.425C17.833,286.362,17.833,223.598,56.408,185.025z"/>
</g>
</g>
<g>
<g>
<path d="M351.794,144.829H160.206c-5.633,0-10.199,4.566-10.199,10.199v111.276c0,5.633,4.566,10.199,10.199,10.199h150.951 l33.095,36.341c1.971,2.165,4.726,3.332,7.543,3.332c1.235,0,2.482-0.224,3.68-0.688c3.929-1.521,6.517-5.3,6.517-9.512V155.028 C361.993,149.395,357.427,144.829,351.794,144.829z M315.663,256.104H170.405v-90.877h171.19v114.402l-18.39-20.193 C321.272,257.314,318.534,256.104,315.663,256.104z"/>
</g>
</g>
<g>
<g>
<path d="M310.566,183.586H201.434c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h109.131 c5.633,0,10.199-4.566,10.199-10.199C320.765,188.152,316.199,183.586,310.566,183.586z"/>
</g>
</g>
<g>
<g>
<path d="M310.566,217.243H201.434c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h109.131 c5.633,0,10.199-4.566,10.199-10.199C320.765,221.809,316.199,217.243,310.566,217.243z"/>
</g>
</g>
</svg>
</template>
<script>
export default {
name: 'IconCar',
props: {
color: {
type: String,
default: '#000000'
}
}
}
</script>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
@@ -0,0 +1,33 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" :fill="color" width="800px" height="800px" viewBox="0 0 64 64" id="Layer_1_1_" version="1.1" xml:space="preserve">
<g>
<path d="M52,53c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S53.654,53,52,53z M52,57c-0.552,0-1-0.448-1-1s0.448-1,1-1 s1,0.448,1,1S52.552,57,52,57z"/>
<path d="M14,53c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S15.654,53,14,53z M14,57c-0.552,0-1-0.448-1-1s0.448-1,1-1 s1,0.448,1,1S14.552,57,14,57z"/>
<path d="M61,51V30c0-2.757-2.243-5-5-5H42.899c-0.465-2.279-2.484-4-4.899-4v-1.586l1.226-1.226C40.494,19.354,42.124,20,43.857,20 c1.832,0,3.554-0.713,4.85-2.009l0.707-0.707l-4.142-4.142l2.443-2.443C48.106,10.886,48.538,11,49,11c1.654,0,3-1.346,3-3 s-1.346-3-3-3s-3,1.346-3,3c0,0.462,0.114,0.894,0.301,1.285l-2.443,2.443l-4.142-4.142l-0.707,0.707 C37.713,9.588,37,11.31,37,13.142c0,1.248,0.338,2.44,0.958,3.486L36,18.586V21c-2.414,0-4.434,1.721-4.899,4H19.195 c-1.718,0-3.295,0.866-4.218,2.315l-5.631,8.848L6.419,37.14C4.374,37.821,3,39.728,3,41.883V51c-1.103,0-2,0.897-2,2v4 c0,1.103,0.897,2,2,2h4.685c1.126,2.361,3.53,4,6.315,4s5.188-1.639,6.315-4h25.37c1.126,2.361,3.53,4,6.315,4s5.188-1.639,6.315-4 H61c1.103,0,2-0.897,2-2v-4C63,51.897,62.103,51,61,51z M49,7c0.552,0,1,0.449,1,1s-0.448,1-1,1s-1-0.449-1-1S48.448,7,49,7z M39.788,10.486l6.726,6.726C45.73,17.725,44.815,18,43.857,18c-1.298,0-2.518-0.505-3.435-1.423C39.505,15.66,39,14.439,39,13.142 C39,12.185,39.275,11.269,39.788,10.486z M36,23h2c1.302,0,2.402,0.839,2.816,2h-7.631C33.598,23.839,34.698,23,36,23z M5.923,39.724C6.59,40.288,7,41.107,7,42c0,1.302-0.839,2.402-2,2.816v-2.933C5,41.047,5.348,40.277,5.923,39.724z M5,46.899 C7.279,46.434,9,44.414,9,42c0-1.2-0.435-2.331-1.184-3.219l2.837-0.945l6.012-9.447c0.554-0.87,1.5-1.39,2.53-1.39H56 c1.654,0,3,1.346,3,3v21h-2V31c0-1.103-0.897-2-2-2H35v22h-2V29H19.555c-0.694,0-1.329,0.352-1.696,0.94L13,37.713V49.08 c-1.502,0.216-2.852,0.906-3.889,1.92H5V46.899z M47.111,51H37V31h18v18.685C54.089,49.251,53.074,49,52,49 C50.098,49,48.374,49.765,47.111,51z M18.889,51c-1.037-1.014-2.387-1.704-3.889-1.92V39h5h2h3v1c0,1.654,1.346,3,3,3h3v8H18.889z M27,39h4v2h-3c-0.552,0-1-0.448-1-1V39z M31,37h-9v-1c0-1.654-1.346-3-3-3h-0.695l1.25-2H31V37z M17.055,35H19 c0.552,0,1,0.448,1,1v1h-4.195L17.055,35z M3,57v-4h4.685C7.251,53.911,7,54.926,7,56c0,0.34,0.033,0.672,0.08,1H3z M14,61 c-2.757,0-5-2.243-5-5s2.243-5,5-5s5,2.243,5,5S16.757,61,14,61z M20.92,57c0.047-0.328,0.08-0.66,0.08-1 c0-1.074-0.251-2.089-0.685-3h25.37C45.251,53.911,45,54.926,45,56c0,0.34,0.033,0.672,0.08,1H20.92z M52,61c-2.757,0-5-2.243-5-5 s2.243-5,5-5s5,2.243,5,5S54.757,61,52,61z M61,57h-2.08c0.047-0.328,0.08-0.66,0.08-1c0-1.074-0.251-2.089-0.685-3H61V57z"/>
<path d="M49,3c1.335,0,2.591,0.52,3.535,1.464S54,6.665,54,8s-0.521,2.591-1.465,3.536l1.414,1.414C55.271,11.627,56,9.87,56,8 s-0.729-3.627-2.051-4.95S50.87,1,49,1s-3.627,0.728-4.949,2.05l1.414,1.414C46.409,3.52,47.665,3,49,3z"/>
<path d="M46,33c-3.859,0-7,3.141-7,7s3.141,7,7,7s7-3.141,7-7S49.859,33,46,33z M46,45c-2.757,0-5-2.243-5-5s2.243-5,5-5 s5,2.243,5,5S48.757,45,46,45z"/>
<rect height="2" width="2" x="27" y="45"/>
</g>
</svg>
</template>
<script>
export default {
name: 'IconVan',
props: {
color: {
type: String,
default: '#000000'
}
}
}
</script>
+8
View File
@@ -0,0 +1,8 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import './assets/hud.css'
createApp(App).mount('#app')
+7
View File
@@ -0,0 +1,7 @@
// src/services/api.js
import axios from 'axios'
export default axios.create({
baseURL: '/api',
headers: { 'Content-Type': 'application/json' }
})
@@ -0,0 +1,13 @@
import api from './api';
export function getSerialPorts() {
return api.get('/serial-utils/serialports');
}
export function connectSerial(path) {
return api.post('/serial-utils/connect', { path });
}
export function getLatestPacket(){
return api.get('/serial-utils/latest');
}
+26
View File
@@ -0,0 +1,26 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:6666', // express server for serial
changeOrigin: true
}
}
}
})