Compare commits

...

5 Commits

Author SHA1 Message Date
Adam Goldsmith e14904e59b Add support for per-station timeoutLength/lowVoltage 2023-07-14 20:30:24 -04:00
Adam Goldsmith 2feabd4d87 Don't send notifications until log replay finished 2023-07-14 19:46:28 -04:00
Adam Goldsmith 6c015c322c Use human readable duration format in `status_config.yaml` 2023-07-14 18:28:28 -04:00
Adam Goldsmith 9426e2ead6 Pass lowVoltage/timeoutLength via props instead of re-importing config 2023-07-14 18:03:56 -04:00
Adam Goldsmith 269cdb65f1 Enable `include-workspace-root` setting for pnpm
This allows the root frontend app to run via `pnpm start` nicely. This
setting was not previously required, I think.
2023-07-14 17:29:59 -04:00
7 changed files with 83 additions and 33 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
include-workspace-root=true

View File

@ -7,6 +7,7 @@
"aprs-parser": "github:ad1217/npm-aprs-parser#no-dynamic-require",
"distinct-colors": "^1.0.4",
"ol": "^6.15.1",
"parse-duration": "^1.1.0",
"vue": "^3.3.4",
"vue3-openlayers": "^0.1.75"
},

View File

@ -14,6 +14,9 @@ dependencies:
ol:
specifier: ^6.15.1
version: 6.15.1
parse-duration:
specifier: ^1.1.0
version: 1.1.0
vue:
specifier: ^3.3.4
version: 3.3.4
@ -720,6 +723,10 @@ packages:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
dev: false
/parse-duration@1.1.0:
resolution: {integrity: sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==}
dev: false
/parse-headers@2.0.5:
resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==}
dev: false

View File

@ -54,5 +54,6 @@ wss.on('connection', (ws) => {
.split('\n')
.filter((line) => line !== '')
.forEach((line) => ws.send(line));
ws.send("FINISHED REPLAY");
}
});

View File

@ -1,5 +1,5 @@
<template>
<tr :class="{ timedOut, lowVoltage, neverHeard: !stationStatus.lastHeard }">
<tr :class="{ timedOut, isLowVoltage, neverHeard: !stationStatus.lastHeard }">
<td :title="callsign">{{ tacticalAndOrCall }}</td>
<template v-if="stationStatus.lastHeard">
<td>{{ formatTime(stationStatus.lastHeard) }}</td>
@ -24,17 +24,22 @@
<script setup>
import { ref, computed, watch } from 'vue';
import config from './status_config.yaml';
import parseDuration from 'parse-duration';
const props = defineProps({
callsign: String,
tactical: String,
timeoutLength: String,
lowVoltage: Number,
finishedReplay: Boolean,
messages: Array,
now: Date,
});
function notify(title, body) {
return new Notification(title, { body: body, requireInteraction: true });
if (props.finishedReplay) {
return new Notification(title, { body: body, requireInteraction: true });
}
}
function formatTime(time, isDuration = false) {
@ -44,6 +49,10 @@ function formatTime(time, isDuration = false) {
);
}
const timeoutLengthMs = computed(() => {
return parseDuration(props.timeoutLength);
});
function prettyDuration(duration) {
let date = new Date(duration);
return [
@ -70,15 +79,16 @@ const timedOut = computed(() => {
return false;
} else {
return (
props.now.getTime() - stationStatus.value.lastHeard > config.timeoutLength
props.now.getTime() - stationStatus.value.lastHeard >
timeoutLengthMs.value
);
}
});
const lowVoltage = computed(() => {
const isLowVoltage = computed(() => {
return (
stationStatus.value.lastVoltage &&
stationStatus.value.lastVoltage < config.lowVoltage
stationStatus.value.lastVoltage < props.lowVoltage
);
});
@ -114,28 +124,25 @@ const stationStatus = computed(() => {
return status;
});
watch(
() => props.lowVoltage,
(newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall}'s battery has dropepd below ${config.lowVoltage}V`,
`Voltage: ${stationStatus.value.lastVoltage}`
);
}
watch(isLowVoltage, (newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall}'s battery has dropepd below ${props.lowVoltage}V`,
`Voltage: ${stationStatus.value.lastVoltage}`
);
}
);
});
watch(timedOut, (newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall.value} has not been heard for over ${prettyDuration(
config.timeoutLength
timeoutLengthMs.value
)}!`,
`Last Heard: ${formatTime(
stationStatus.value.lastHeard
)} (${prettyDuration(
props.now.value - stationStatus.value.lastHeard
props.now.getTime() - stationStatus.value.lastHeard
)} ago!)`
);
}
@ -147,7 +154,7 @@ tr.timedOut {
background-color: red;
}
tr.lowVoltage {
tr.isLowVoltage {
background-color: yellow;
}

View File

@ -21,10 +21,11 @@
</tr>
<StationRow
v-for="(tactical, callsign) in trackedStations"
v-for="(stationProps, callsign) in trackedStations"
:key="callsign"
:callsign="callsign"
:tactical="tactical"
v-bind="{ ...config.default, ...stationProps }"
:finishedReplay="finishedReplay"
:messages="messagesFromStation[callsign] || []"
:now="now"
>
@ -42,12 +43,29 @@ import config from './status_config.yaml';
const parser = new APRSParser();
let aprsStream = null;
const finishedReplay = ref(false);
const messages = ref([]);
const messagesFromStation = ref({});
const now = ref(new Date());
const trackedStations = ref(config.trackedStations);
const trackedStations = ref(normalizeConfigStations());
const canNotify = ref(Notification.permission === 'granted');
function normalizeConfigStations() {
return [...Object.entries(config.trackedStations)]
.map(([callsign, tacticalOrProps]) => {
if (typeof tacticalOrProps === 'string') {
return [callsign, { tactical: tacticalOrProps }];
} else {
return [callsign, tacticalOrProps];
}
})
.reduce((acc, [callsign, props]) => {
console.log(callsign, props);
acc[callsign] = props;
return acc;
}, {});
}
onMounted(() => {
// Connect to websocket aprs stream
connectToStream();
@ -66,7 +84,9 @@ function connectToStream() {
}, 5000);
};
aprsStream.onmessage = (event) => {
if (event.data !== '') {
if (event.data === 'FINISHED REPLAY') {
finishedReplay.value = true;
} else if (event.data !== '') {
handleMessage(JSON.parse(event.data));
}
};
@ -96,7 +116,7 @@ function handleMessage(packet) {
message.data.text.split(';').map((tac_assoc) => {
let [call, tac] = tac_assoc.split('=', 2);
if (tac) {
trackedStations.value[call] = tac;
trackedStations.value[call].tactical = tac;
} else {
delete trackedStations.value[call];
}

View File

@ -1,18 +1,31 @@
timeoutLength: 600000 # 10 * 60 * 1000 = 10 minutes
lowVoltage: 11.9
TACTICAL_whitelist:
- KC1GDW-7
- W1FN
default:
timeoutLength: 10 minutes
lowVoltage: 11.9
trackedStations:
# Digis/iGates
W1FN-1: Moose Mt
W1FN-5: Hanover
W1FN-9: Bath
N1GMC-1: Dame Hill Rd, Orford
N1GMC-2: Sunday Mt, Orford
KB1FDA-1: Corinth
W1FN-1:
tactical: Moose Mt
timeoutLength: 25 minutes
W1FN-5:
tactical: Hanover
timeoutLength: 25 minutes
W1FN-9:
tactical: Bath
timeoutLength: 25 minutes
N1GMC-1:
tactical: Dame Hill Rd, Orford
timeoutLength: 25 minutes
N1GMC-2:
tactical: Sunday Mt, Orford
timeoutLength: 25 minutes
KB1FDA-1:
tactical: Corinth
timeoutLength: 25 minutes
# Vehicles
K1EHZ-10: Recovery