Prettified Code!

pull/1111/head^2
DJ2LS 2026-06-02 18:49:23 +00:00 committed by GitHub Action
parent 73204849f2
commit 092c4622ec
10 changed files with 166 additions and 131 deletions

View File

@ -7,11 +7,12 @@ in C/C++, and the frontend is build with VueJS, so you will need Python (from
3.10 to 3.13) to work on the backend, and node+npm to work on the frontend.
System dependencies required:
- python3 (with virtual environment support)
- portaudio
- hamlib (optional, there is a vendored version bundled with the freedata
server, or you can build it from source)
- nodejs + npm
- python3 (with virtual environment support)
- portaudio
- hamlib (optional, there is a vendored version bundled with the freedata
server, or you can build it from source)
- nodejs + npm
Example for debian trixie: `sudo apt update && sudo apt install python3
python3-venv nodejs npm portaudio19-dev`
@ -37,6 +38,7 @@ To fetch the `npm` dependencies go inside `./freedata_gui` and run `npm
install`. See `./freedata_gui/README.md` for additional informations.
## Linting and Formatting (python-only)
The python sources must be formatted and checked using `ruff`. There is a CI
workflow that block all PRs that do not pass the linter/formatter checks.
@ -54,5 +56,6 @@ usefull in the CI. In the future it will no longer be necessary
fix automatically, you must fix them manually.
## Testing backend
To run the backend tests run inside the virtual environment `python -m unittest
discover tests`

View File

@ -9,7 +9,10 @@ import {
validateCallsignWithoutSSID,
} from "./freedata";
import { processFreedataMessages } from "./messagesHandler";
import { processFreedataDomains, processFreedataBroadcastsPerDomain } from "./broadcastsHandler";
import {
processFreedataDomains,
processFreedataBroadcastsPerDomain,
} from "./broadcastsHandler";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
@ -327,13 +330,12 @@ export async function setStationInfo(callsign, info) {
return await apiPost(`/freedata/station/${callsign}`, info);
}
export async function getFreedataBroadcasts() {
broadcastStore.loading = true;
try {
const res = await apiGet("/freedata/broadcasts");
if (res) {
processFreedataBroadcasts(res)
processFreedataBroadcasts(res);
}
} catch (error) {
console.error("Error fetching broadcasts:", error);
@ -347,7 +349,7 @@ export async function getFreedataBroadcastsPerDomain(domain) {
try {
const res = await apiGet(`/freedata/broadcasts/${domain}/`);
if (res) {
processFreedataBroadcastsPerDomain(res)
processFreedataBroadcastsPerDomain(res);
}
} catch (error) {
console.error("Error fetching broadcasts:", error);
@ -356,13 +358,12 @@ export async function getFreedataBroadcastsPerDomain(domain) {
}
}
export async function getFreedataDomains() {
broadcastStore.loading = true;
try {
const res = await apiGet("/freedata/broadcasts/domains");
if (res) {
processFreedataDomains(res)
processFreedataDomains(res);
}
} catch (error) {
console.error("Error fetching broadcasts:", error);
@ -389,12 +390,8 @@ export async function deleteFreedataBroadcastDomain(id) {
return await apiDelete(`/freedata/broadcasts/${id}`);
}
export async function postFreedataBroadcastADIF(id) {
return await apiPost(`/freedata/broadcasts/${id}/adif`, {
action: "retransmit",
});
}

View File

@ -9,8 +9,8 @@ const MAX_BLOCKS = 10;
export function addDataToAudio(data) {
const int16 = new Int16Array(data);
const copy = new Int16Array(int16); // Kopie für Sicherheit
/*
const copy = new Int16Array(int16); // Kopie für Sicherheit
/*
const stream = audio.rxStream;
if (stream.length >= MAX_BLOCKS) {
@ -19,7 +19,5 @@ export function addDataToAudio(data) {
stream.push(copy);
*/
audio.addBlock(copy);
audio.addBlock(copy);
}

View File

@ -1,19 +1,19 @@
import { setActivePinia } from 'pinia';
import pinia from '../store/index';
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useBroadcastStore } from '../store/broadcastStore.js';
import { useBroadcastStore } from "../store/broadcastStore.js";
import {
deleteFreedataBroadcastDomain,
deleteFreedataBroadcastMessage,
postFreedataBroadcastADIF,
retransmitFreedataBroadcast,
sendFreedataBroadcastMessage
sendFreedataBroadcastMessage,
} from "@/js/api";
const broadcast = useBroadcastStore(pinia);
export async function processFreedataBroadcastsPerDomain(data) {
console.log(data)
console.log(data);
broadcast.setBroadcastsForDomain(data);
}
@ -25,12 +25,10 @@ export async function processFreedataDomains(data) {
// sum of unread_count
broadcast.totalUnreadMessages = Object.values(data).reduce(
(sum, domain) => sum + (domain.unread_count || 0),
0
0,
);
}
export function newBroadcastMessage(params) {
sendFreedataBroadcastMessage(params);
broadcast.triggerScrollToBottom();
@ -42,7 +40,7 @@ export function repeatBroadcastTransmission(id) {
export function deleteBroadcastDomainFromDB(domain) {
deleteFreedataBroadcastDomain(domain);
broadcast.selectedDomain = '';
broadcast.selectedDomain = "";
}
export function sendBroadcastADIFviaUDP(domain) {

View File

@ -5,8 +5,8 @@ import {
getModemState,
getRadioStatus,
getSysInfo,
getFreedataDomains,
getFreedataBroadcastsPerDomain
getFreedataDomains,
getFreedataBroadcastsPerDomain,
} from "./api";
import { processFreedataMessages } from "./messagesHandler";
import { processRadioStatus } from "./radioHandler";
@ -45,7 +45,7 @@ export async function loadAllData() {
getOverallHealth();
getFreedataMessages();
getFreedataDomains();
getFreedataBroadcastsPerDomain(broadcast.selectedDomain)
getFreedataBroadcastsPerDomain(broadcast.selectedDomain);
processFreedataMessages();
processRadioStatus();
}
@ -111,8 +111,8 @@ export function eventDispatcher(data) {
switch (data["message-db"]) {
case "changed":
console.log("fetching new messages...");
getFreedataDomains();
getFreedataBroadcastsPerDomain(broadcast.selectedDomain)
getFreedataDomains();
getFreedataBroadcastsPerDomain(broadcast.selectedDomain);
var messages = getFreedataMessages();
processFreedataMessages(messages);

View File

@ -23,7 +23,7 @@ function connect(endpoint, dispatcher) {
`${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`,
);
if (endpoint.includes("audio")){
if (endpoint.includes("audio")) {
socket.binaryType = "arraybuffer";
}

View File

@ -1,9 +1,16 @@
export function parseMetar(raw) {
const out = {
iata: null,
day: null, hh: null, mm: null,
wind: { dir_deg: null, spd_kt: null, gust_kt: null, var_from: null, var_to: null },
day: null,
hh: null,
mm: null,
wind: {
dir_deg: null,
spd_kt: null,
gust_kt: null,
var_from: null,
var_to: null,
},
visibility: { code: null, meters: null },
temp_c: null,
dew_c: null,
@ -11,66 +18,72 @@ export function parseMetar(raw) {
auto: false,
cor: false,
clouds: [],
raw: raw?.trim() || ''
raw: raw?.trim() || "",
};
if (!raw) return out;
const tokens = raw.replace('=', '').trim().split(/\s+/);
const tokens = raw.replace("=", "").trim().split(/\s+/);
out.auto = tokens.includes('AUTO');
out.cor = tokens.includes('COR');
out.auto = tokens.includes("AUTO");
out.cor = tokens.includes("COR");
for (const t of tokens) {
if (/^[A-Z]{3}$/.test(t)) { out.iata = t; break; }
if (/^[A-Z]{4}$/.test(t)) { out.iata = t; break; } // IACA override
if (/^[A-Z]{3}$/.test(t)) {
out.iata = t;
break;
}
if (/^[A-Z]{4}$/.test(t)) {
out.iata = t;
break;
} // IACA override
}
const tTok = tokens.find(t => /^\d{6}Z$/.test(t));
const tTok = tokens.find((t) => /^\d{6}Z$/.test(t));
if (tTok) {
out.day = tTok.slice(0,2);
out.hh = tTok.slice(2,4);
out.mm = tTok.slice(4,6);
out.day = tTok.slice(0, 2);
out.hh = tTok.slice(2, 4);
out.mm = tTok.slice(4, 6);
}
const wTok = tokens.find(t => /^(?:\d{3}|VRB)\d{2,3}(G\d{2,3})?KT$/.test(t));
const wTok = tokens.find((t) =>
/^(?:\d{3}|VRB)\d{2,3}(G\d{2,3})?KT$/.test(t),
);
if (wTok) {
const m = wTok.match(/^(\d{3}|VRB)(\d{2,3})(?:G(\d{2,3}))?KT$/);
if (m) {
out.wind.dir_deg = m[1] === 'VRB' ? null : parseInt(m[1], 10);
out.wind.spd_kt = parseInt(m[2], 10);
out.wind.dir_deg = m[1] === "VRB" ? null : parseInt(m[1], 10);
out.wind.spd_kt = parseInt(m[2], 10);
out.wind.gust_kt = m[3] ? parseInt(m[3], 10) : null;
}
}
const vTok = tokens.find(t => /^\d{3}V\d{3}$/.test(t));
const vTok = tokens.find((t) => /^\d{3}V\d{3}$/.test(t));
if (vTok) {
const m = vTok.match(/^(\d{3})V(\d{3})$/);
out.wind.var_from = parseInt(m[1], 10);
out.wind.var_to = parseInt(m[2], 10);
out.wind.var_to = parseInt(m[2], 10);
}
if (tokens.includes('CAVOK')) {
out.visibility.code = 'CAVOK';
if (tokens.includes("CAVOK")) {
out.visibility.code = "CAVOK";
out.visibility.meters = 10000;
} else {
const vis = tokens.find(t => /^\d{4}$/.test(t));
const vis = tokens.find((t) => /^\d{4}$/.test(t));
if (vis) {
out.visibility.code = vis;
out.visibility.meters = parseInt(vis, 10);
}
}
const tdTok = tokens.find(t => /^(M?\d{1,2})\/(M?\d{1,2})$/.test(t));
const tdTok = tokens.find((t) => /^(M?\d{1,2})\/(M?\d{1,2})$/.test(t));
if (tdTok) {
const m = tdTok.match(/^(M?\d{1,2})\/(M?\d{1,2})$/);
out.temp_c = _signed(m[1]);
out.dew_c = _signed(m[2]);
out.dew_c = _signed(m[2]);
}
const qTok = tokens.find(t => /^(Q\d{4}|A\d{4})$/.test(t));
const qTok = tokens.find((t) => /^(Q\d{4}|A\d{4})$/.test(t));
if (qTok) {
if (qTok.startsWith('Q')) out.qnh_hpa = parseInt(qTok.slice(1), 10);
if (qTok.startsWith("Q")) out.qnh_hpa = parseInt(qTok.slice(1), 10);
else {
const inHg = parseInt(qTok.slice(1), 10) / 100;
out.qnh_hpa = Math.round(inHg * 33.8639);
@ -83,7 +96,7 @@ export function parseMetar(raw) {
out.clouds.push({
cover: m[1],
base_ft: parseInt(m[2], 10) * 100,
type: m[3] || null
type: m[3] || null,
});
}
}
@ -91,93 +104,117 @@ export function parseMetar(raw) {
return out;
}
function _signed(s) { return s?.startsWith('M') ? -parseInt(s.slice(1),10) : parseInt(s,10); }
function _signed(s) {
return s?.startsWith("M") ? -parseInt(s.slice(1), 10) : parseInt(s, 10);
}
// ---------------- Flight Category ----------------
export function determineFlightCategory(parsed) {
const cavok = parsed.visibility.code === 'CAVOK';
const cavok = parsed.visibility.code === "CAVOK";
let ceiling = null;
for (const c of parsed.clouds || []) {
if (c.cover === 'BKN' || c.cover === 'OVC') {
if (c.cover === "BKN" || c.cover === "OVC") {
if (ceiling === null || c.base_ft < ceiling) ceiling = c.base_ft;
}
}
const v = parsed.visibility.meters ?? 0;
if (cavok) return 'VFR';
if (v >= 5000 && (ceiling === null || ceiling > 3000)) return 'VFR';
if ((v >= 3000 && v < 5000) || (ceiling !== null && ceiling >= 1000 && ceiling <= 3000)) return 'MVFR';
if ((v >= 1500 && v < 3000) || (ceiling !== null && ceiling >= 500 && ceiling < 1000)) return 'IFR';
return 'LIFR';
if (cavok) return "VFR";
if (v >= 5000 && (ceiling === null || ceiling > 3000)) return "VFR";
if (
(v >= 3000 && v < 5000) ||
(ceiling !== null && ceiling >= 1000 && ceiling <= 3000)
)
return "MVFR";
if (
(v >= 1500 && v < 3000) ||
(ceiling !== null && ceiling >= 500 && ceiling < 1000)
)
return "IFR";
return "LIFR";
}
export function categoryClass(cat) {
switch (cat) {
case 'VFR': return 'text-bg-success';
case 'MVFR': return 'text-bg-primary';
case 'IFR': return 'text-bg-danger';
case 'LIFR': return 'text-bg-magenta';
default: return 'text-bg-secondary';
case "VFR":
return "text-bg-success";
case "MVFR":
return "text-bg-primary";
case "IFR":
return "text-bg-danger";
case "LIFR":
return "text-bg-magenta";
default:
return "text-bg-secondary";
}
}
// ---------------- Formatters (labels shown in Vue) ----------------
export function windToString(w) {
if (!w) return '—';
const base = (w.dir_deg == null)
? `VRB/${w.spd_kt ?? 0}kt`
: `${w.dir_deg}°/${w.spd_kt ?? 0}kt`;
const gust = w.gust_kt ? ` G${w.gust_kt}` : '';
const varrng = (w.var_from != null && w.var_to != null) ? ` V${w.var_from}-${w.var_to}` : '';
if (!w) return "—";
const base =
w.dir_deg == null
? `VRB/${w.spd_kt ?? 0}kt`
: `${w.dir_deg}°/${w.spd_kt ?? 0}kt`;
const gust = w.gust_kt ? ` G${w.gust_kt}` : "";
const varrng =
w.var_from != null && w.var_to != null ? ` V${w.var_from}-${w.var_to}` : "";
return base + gust + varrng;
}
export function visibilityToString(v) {
return v?.code || '—';
return v?.code || "—";
}
export function cloudsToString(clouds) {
if (!clouds || clouds.length === 0) return '—';
return clouds.map(c => `${c.cover}${String(Math.round(c.base_ft/100)).padStart(3,'0')}${c.type || ''}`).join(' ');
if (!clouds || clouds.length === 0) return "—";
return clouds
.map(
(c) =>
`${c.cover}${String(Math.round(c.base_ft / 100)).padStart(3, "0")}${c.type || ""}`,
)
.join(" ");
}
export function tempDewToString(t, d) {
const tStr = (typeof t === 'number') ? `${t}` : '—';
const dStr = (typeof d === 'number') ? `${d}` : '—';
const tStr = typeof t === "number" ? `${t}` : "—";
const dStr = typeof d === "number" ? `${d}` : "—";
return `${tStr}/${dStr}°C`;
}
export function qnhToString(q) {
return (typeof q === 'number') ? `${q} hPa` : '—';
return typeof q === "number" ? `${q} hPa` : "—";
}
export function timeToString(day, hh, mm) {
return (day && hh && mm) ? `${day}.${hh}:${mm}Z` : '—';
return day && hh && mm ? `${day}.${hh}:${mm}Z` : "—";
}
// ---------------- SVG Compass + Windbarb (WMO-like) ----------------
export function buildCompassSVG(wind, opts = {}) {
const size = opts.size ?? 160;
const cx = size/2, cy = size/2;
const cx = size / 2,
cy = size / 2;
const dirDeg = wind?.dir_deg;
const spd = wind?.spd_kt ?? 0;
const gust = wind?.gust_kt;
const showDir = (dirDeg == null) ? null : (dirDeg + 180) % 360;
const transform = (showDir == null) ? '' : `transform="rotate(${showDir} ${cx} ${cy})"`;
const showDir = dirDeg == null ? null : (dirDeg + 180) % 360;
const transform =
showDir == null ? "" : `transform="rotate(${showDir} ${cx} ${cy})"`;
const svgGrid = `
<circle cx="${cx}" cy="${cy}" r="${size*0.4375}" fill="none" stroke="#ced4da"/>
<circle cx="${cx}" cy="${cy}" r="${size*0.3125}" fill="none" stroke="#e9ecef"/>
<circle cx="${cx}" cy="${cy}" r="${size*0.1875}" fill="none" stroke="#f1f3f5"/>
<line x1="${size*0.0625}" y1="${cy}" x2="${size*0.9375}" y2="${cy}" stroke="#dee2e6"/>
<line x1="${cx}" y1="${size*0.0625}" x2="${cx}" y2="${size*0.9375}" stroke="#dee2e6"/>
<text x="${cx}" y="${size*0.0375}" text-anchor="middle" font-size="10" fill="#6c757d">N</text>
<text x="${size*0.9625}" y="${cy+4}" text-anchor="end" font-size="10" fill="#6c757d">E</text>
<text x="${cx}" y="${size*0.975}" text-anchor="middle" font-size="10" fill="#6c757d">S</text>
<text x="${size*0.0375}" y="${cy+4}" text-anchor="start" font-size="10" fill="#6c757d">W</text>
<circle cx="${cx}" cy="${cy}" r="${size * 0.4375}" fill="none" stroke="#ced4da"/>
<circle cx="${cx}" cy="${cy}" r="${size * 0.3125}" fill="none" stroke="#e9ecef"/>
<circle cx="${cx}" cy="${cy}" r="${size * 0.1875}" fill="none" stroke="#f1f3f5"/>
<line x1="${size * 0.0625}" y1="${cy}" x2="${size * 0.9375}" y2="${cy}" stroke="#dee2e6"/>
<line x1="${cx}" y1="${size * 0.0625}" x2="${cx}" y2="${size * 0.9375}" stroke="#dee2e6"/>
<text x="${cx}" y="${size * 0.0375}" text-anchor="middle" font-size="10" fill="#6c757d">N</text>
<text x="${size * 0.9625}" y="${cy + 4}" text-anchor="end" font-size="10" fill="#6c757d">E</text>
<text x="${cx}" y="${size * 0.975}" text-anchor="middle" font-size="10" fill="#6c757d">S</text>
<text x="${size * 0.0375}" y="${cy + 4}" text-anchor="start" font-size="10" fill="#6c757d">W</text>
`;
let windGroup = `<circle cx="${cx}" cy="${cy}" r="10" stroke="#0d6efd" stroke-width="3" fill="none"/>`;
@ -186,9 +223,10 @@ export function buildCompassSVG(wind, opts = {}) {
windGroup = `<g ${transform}>${_wmoBarb(cx, cy, spd, size)}</g>`;
}
const gustRing = (gust && gust > spd)
? `<circle cx="${cx}" cy="${cy}" r="${size*0.48}" fill="none" stroke="#0d6efd" stroke-dasharray="4 4" />`
: '';
const gustRing =
gust && gust > spd
? `<circle cx="${cx}" cy="${cy}" r="${size * 0.48}" fill="none" stroke="#0d6efd" stroke-dasharray="4 4" />`
: "";
return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="display:block;max-width:${size}px">
@ -199,32 +237,42 @@ export function buildCompassSVG(wind, opts = {}) {
}
function _wmoBarb(cx, cy, spdKt, size) {
const stroke = '#0d6efd';
const stroke = "#0d6efd";
const lw = 3;
const staffLen = size * 0.45;
const step = Math.max(6, size * 0.05);
const barbLong = Math.max(12, size * 0.10);
const barbHalf = barbLong * 0.6;
const barbLong = Math.max(12, size * 0.1);
const barbHalf = barbLong * 0.6;
let s = Math.round((spdKt || 0) / 5) * 5;
const tri = Math.floor(s / 50); s -= tri * 50;
const ten = Math.floor(s / 10); s -= ten * 10;
const tri = Math.floor(s / 50);
s -= tri * 50;
const ten = Math.floor(s / 10);
s -= ten * 10;
const five = Math.floor(s / 5);
const elems = [];
elems.push(`<line x1="${cx}" y1="${cy}" x2="${cx}" y2="${cy - staffLen}" stroke="${stroke}" stroke-width="${lw}" />`);
elems.push(
`<line x1="${cx}" y1="${cy}" x2="${cx}" y2="${cy - staffLen}" stroke="${stroke}" stroke-width="${lw}" />`,
);
let y = cy - staffLen + step*1.2;
let y = cy - staffLen + step * 1.2;
for (let i = 0; i < tri; i++) {
elems.push(`<polygon points="${cx},${y} ${cx},${y - step} ${cx + barbLong},${y - step/2}" fill="${stroke}" />`);
elems.push(
`<polygon points="${cx},${y} ${cx},${y - step} ${cx + barbLong},${y - step / 2}" fill="${stroke}" />`,
);
y += step;
}
for (let i = 0; i < ten; i++) {
elems.push(`<line x1="${cx}" y1="${y}" x2="${cx + barbLong}" y2="${y - barbLong*0.35}" stroke="${stroke}" stroke-width="${lw}" />`);
elems.push(
`<line x1="${cx}" y1="${y}" x2="${cx + barbLong}" y2="${y - barbLong * 0.35}" stroke="${stroke}" stroke-width="${lw}" />`,
);
y += step;
}
if (five) {
elems.push(`<line x1="${cx}" y1="${y}" x2="${cx + barbHalf}" y2="${y - barbHalf*0.35}" stroke="${stroke}" stroke-width="${lw}" />`);
elems.push(
`<line x1="${cx}" y1="${y}" x2="${cx + barbHalf}" y2="${y - barbHalf * 0.35}" stroke="${stroke}" stroke-width="${lw}" />`,
);
}
return elems.join('\n');
return elems.join("\n");
}

View File

@ -53,11 +53,6 @@ export const useAudioStore = defineStore("audioStore", () => {
}
}
const loadAudioDevices = async () => {
try {
const devices = await getAudioDevices();
@ -83,7 +78,6 @@ export const useAudioStore = defineStore("audioStore", () => {
resetBuffer,
get bufferedBlockCount() {
return readyBlocks;
},
},
};
});

View File

@ -1,9 +1,7 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useBroadcastStore = defineStore("broadcastStore", () => {
// Indicator if we are loading data
var loading = ref(false);
@ -29,21 +27,20 @@ export const useBroadcastStore = defineStore("broadcastStore", () => {
const newPriority = ref("");
// selectedMessage
const selectedMessage = ref()
const selectedMessage = ref();
// unread message counter
var totalUnreadMessages = ref(0);
function triggerScrollToBottom() {
scrollTrigger.value++;
}
function setDomains(data){
function setDomains(data) {
domains.value = data;
}
function setBroadcastsForDomain(data){
function setBroadcastsForDomain(data) {
domainBroadcasts.value = data;
}
@ -61,6 +58,6 @@ export const useBroadcastStore = defineStore("broadcastStore", () => {
newPriority,
newMessageType,
selectedMessage,
totalUnreadMessages
totalUnreadMessages,
};
})
});

View File

@ -96,7 +96,7 @@ const defaultConfig = {
EXP: {
enable_ring_buffer: false,
enable_vhf: false,
enable_groupchat: false
enable_groupchat: false,
},
},
};