diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 94335f10..924f2f9f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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`
diff --git a/freedata_gui/src/js/api.js b/freedata_gui/src/js/api.js
index c5ef458a..8931866d 100644
--- a/freedata_gui/src/js/api.js
+++ b/freedata_gui/src/js/api.js
@@ -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",
});
}
-
-
diff --git a/freedata_gui/src/js/audioStreamHandler.js b/freedata_gui/src/js/audioStreamHandler.js
index dea67dca..99c1841c 100644
--- a/freedata_gui/src/js/audioStreamHandler.js
+++ b/freedata_gui/src/js/audioStreamHandler.js
@@ -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);
}
diff --git a/freedata_gui/src/js/broadcastsHandler.js b/freedata_gui/src/js/broadcastsHandler.js
index f1358441..5a6b068d 100644
--- a/freedata_gui/src/js/broadcastsHandler.js
+++ b/freedata_gui/src/js/broadcastsHandler.js
@@ -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) {
diff --git a/freedata_gui/src/js/eventHandler.js b/freedata_gui/src/js/eventHandler.js
index e28936e1..922bc06a 100644
--- a/freedata_gui/src/js/eventHandler.js
+++ b/freedata_gui/src/js/eventHandler.js
@@ -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);
diff --git a/freedata_gui/src/js/event_sock.js b/freedata_gui/src/js/event_sock.js
index 87d80040..4cfb5954 100644
--- a/freedata_gui/src/js/event_sock.js
+++ b/freedata_gui/src/js/event_sock.js
@@ -23,7 +23,7 @@ function connect(endpoint, dispatcher) {
`${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`,
);
- if (endpoint.includes("audio")){
+ if (endpoint.includes("audio")) {
socket.binaryType = "arraybuffer";
}
diff --git a/freedata_gui/src/js/metar.js b/freedata_gui/src/js/metar.js
index d3ac3c29..83292a6c 100644
--- a/freedata_gui/src/js/metar.js
+++ b/freedata_gui/src/js/metar.js
@@ -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 = `
-
-
-
-
-
- N
- E
- S
- W
+
+
+
+
+
+ N
+ E
+ S
+ W
`;
let windGroup = ``;
@@ -186,9 +223,10 @@ export function buildCompassSVG(wind, opts = {}) {
windGroup = `${_wmoBarb(cx, cy, spd, size)}`;
}
- const gustRing = (gust && gust > spd)
- ? ``
- : '';
+ const gustRing =
+ gust && gust > spd
+ ? ``
+ : "";
return `