Initial push
This commit is contained in:
@@ -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
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+4445
File diff suppressed because it is too large
Load Diff
@@ -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 |
+1496
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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'));
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user