From 332560840a8f2d20eb26760f123aa6873a53d26a Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 08:57:43 +0100 Subject: [PATCH 01/69] Added config file on backend --- radio/app/loggingConfig.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 radio/app/loggingConfig.py diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py new file mode 100644 index 000000000..d481e4b47 --- /dev/null +++ b/radio/app/loggingConfig.py @@ -0,0 +1,9 @@ +import logging + +def setup_logging() -> logging.Logger: + + fgcs_logger = logging.getLogger("fgcs") + + fgcs_logger.addHandler( + logging.handlers + ) \ No newline at end of file From dfbd218cd5a615d45b1649c21385621f66c337e3 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 09:20:12 +0100 Subject: [PATCH 02/69] Changed print to log --- radio/app.py | 10 +++++----- radio/app/utils.py | 7 +++---- .../tests/mission_test_files/upload_mission_helper.py | 3 ++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/radio/app.py b/radio/app.py index 218c895d7..b61749c97 100644 --- a/radio/app.py +++ b/radio/app.py @@ -1,13 +1,13 @@ import os import app.droneStatus as droneStatus -from app import create_app, socketio +from app import create_app, socketio, logger from pathlib import Path from dotenv import load_dotenv app = create_app(debug=True) if __name__ == "__main__": - print("Loading dotenv.") + logger.info("Loading dotenv.") env_path = Path("../gcs/.env") load_dotenv(dotenv_path=env_path) @@ -19,9 +19,9 @@ port = 4237 host = "127.0.0.1" - print("Starting backend.") - print(host) + logger.info(f"Starting backend at {host}:{port}.") socketio.run(app, allow_unsafe_werkzeug=True, host=host, port=port) + if droneStatus.drone: droneStatus.drone.close() - print("Backend closed.") + logger.info("Backend closed.") diff --git a/radio/app/utils.py b/radio/app/utils.py index d336eda34..a799e49d9 100644 --- a/radio/app/utils.py +++ b/radio/app/utils.py @@ -4,6 +4,7 @@ from pymavlink import mavutil from serial.tools import list_ports +from app import logger from app.customTypes import VehicleType from . import socketio @@ -19,10 +20,8 @@ def getComPort() -> str: while True: ports = list(list_ports.comports()) - print("Available COM ports:") - for i in range(len(ports)): - port = ports[i] - print(f"\t[{i}]\t{port.name}: {port.description}") + + logger.info("Available COM PORTS: \n" + "\n".join(f"\t[{i}]\t{port.name}: {port.description}" for i, port in enumerate(ports))) inp_port = input("Enter a port index to connect to: ") if not inp_port.isdigit(): diff --git a/radio/tests/mission_test_files/upload_mission_helper.py b/radio/tests/mission_test_files/upload_mission_helper.py index f81fd4f7f..3dad84d87 100644 --- a/radio/tests/mission_test_files/upload_mission_helper.py +++ b/radio/tests/mission_test_files/upload_mission_helper.py @@ -1,5 +1,6 @@ from pymavlink import mavutil, mavwp +from app import logger def uploadMission(file_name, mission_type, master): with open(file_name, "r") as f: @@ -74,4 +75,4 @@ def uploadMission(file_name, mission_type, master): master.mav.send(loader.wp(msg.seq)) - print(f"Uploaded {mission_type} with {loader.count()} items") + logger.info(f"Uploaded {mission_type} with {loader.count()} items") From 87977487b8ab2c4705b21293da6ca355320a72d5 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 09:20:21 +0100 Subject: [PATCH 03/69] Added logging stream handler and socket handler --- radio/app/loggingConfig.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index d481e4b47..84d7ca3b2 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -1,9 +1,34 @@ +import sys import logging -def setup_logging() -> logging.Logger: +from flask_socketio import SocketIO + + +class SocketIOHandler(logging.Handler): + + def __init__(self, sio: SocketIO) -> None: + super().__init__() + self.socket = sio + + def emit(self, record) -> None: + try: + entry = self.format(record) + self.socket.emit("log", {"level": record.levelname, "msg": entry}) + except: + self.handleError(record) + + +def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: fgcs_logger = logging.getLogger("fgcs") - fgcs_logger.addHandler( - logging.handlers - ) \ No newline at end of file + fgcs_logger.setLevel(logging.DEBUG if debug else logging.INFO) + fgcs_logger.addHandler(SocketIOHandler(conn)) + + # Stream handler on debug mode only + if debug: + fgcs_logger.addHandler(logging.StreamHandler(sys.stderr)) + + flask_logger = logging.getLogger("werkzeug") + flask_logger.setLevel(logging.WARNING) + flask_logger.addHandler(SocketIOHandler(conn)) \ No newline at end of file From 443a5d9c23879198e8c2c5ac3852d740c90beb4b Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 09:20:32 +0100 Subject: [PATCH 04/69] Run log setup on create app --- radio/app/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/radio/app/__init__.py b/radio/app/__init__.py index 102ea4678..03f24168b 100644 --- a/radio/app/__init__.py +++ b/radio/app/__init__.py @@ -3,17 +3,11 @@ from flask import Flask from flask_socketio import SocketIO -logging.basicConfig(level=logging.DEBUG) - -logger = logging.getLogger("fgcs") -logger.setLevel(logging.DEBUG) - -flask_logger = logging.getLogger("werkzeug") -flask_logger.setLevel(logging.INFO) - +from loggingConfig import setup_logging socketio = SocketIO(cors_allowed_origins="*", async_mode="threading") +logger = logging.getLogger("fgcs") def create_app(debug: bool = False) -> Flask: """ @@ -22,6 +16,9 @@ def create_app(debug: bool = False) -> Flask: Args: debug: Boolean value for if the debugging should be True or False """ + + setup_logging(socketio, debug) + from app.endpoints import endpoints app = Flask(__name__) From 5ba4638b333d0e2064d67cf898bbdea2099873af Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:18:24 +0100 Subject: [PATCH 05/69] Added two log handlers --- gcs/src/helpers/logHandlers.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 gcs/src/helpers/logHandlers.js diff --git a/gcs/src/helpers/logHandlers.js b/gcs/src/helpers/logHandlers.js new file mode 100644 index 000000000..cd271d40b --- /dev/null +++ b/gcs/src/helpers/logHandlers.js @@ -0,0 +1,10 @@ +import { ipcRenderer } from "electron"; + + +export function consoleLogHandler(msg) { + console.log(`[${msg.level}] [${msg.timestamp}] ${msg.message}`) +} + +export function electronLogHandler(msg) { + window.ipcRenderer.pushLog(msg); +} \ No newline at end of file From 22d7bb2f1ff1e87392d2c95ca085045d11fa199f Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:18:36 +0100 Subject: [PATCH 06/69] Create log handlers on socket init --- gcs/src/components/mainContent.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx index 47dd2ffb9..e593ea333 100644 --- a/gcs/src/components/mainContent.jsx +++ b/gcs/src/components/mainContent.jsx @@ -29,6 +29,8 @@ import { ErrorBoundary } from "react-error-boundary" import AlertProvider from "./dashboard/alertProvider" import ErrorBoundaryFallback from "./error/errorBoundary" import { initSocket } from "../redux/slices/socketSlice" +import { registerHandler } from "../redux/slices/loggingSlice" +import { consoleLogHandler, fileLogHandler } from "../helpers/logHandlers" export default function AppContent() { // Conditionally render UI so the webcam route is literally just a webcam @@ -38,6 +40,13 @@ export default function AppContent() { const dispatch = useDispatch() useEffect(() => { dispatch(initSocket()) + + // Only add console log handler in dev mode + if (process.env.NODE_ENV === "development") { + dispatch(registerHandler(consoleLogHandler)) + } + + dispatch(registerHandler(fileLogHandler)) }, []) return ( From f2df11ddf4f4f16cb9b8e02a03c48a63785ae0bc Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:18:48 +0100 Subject: [PATCH 07/69] Register logging redux slice --- gcs/src/redux/store.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gcs/src/redux/store.js b/gcs/src/redux/store.js index ef815cbad..877082882 100644 --- a/gcs/src/redux/store.js +++ b/gcs/src/redux/store.js @@ -15,6 +15,7 @@ import droneConnectionSlice, { import missionInfoSlice from "./slices/missionSlice" import statusTextSlice from "./slices/statusTextSlice" import notificationSlice from "./slices/notificationSlice" +import loggingSlice from "./slices/loggingSlice" const rootReducer = combineSlices( logAnalyserSlice, @@ -24,6 +25,7 @@ const rootReducer = combineSlices( missionInfoSlice, statusTextSlice, notificationSlice, + loggingSlice ) // Get the persisted state, we only want to take a couple of things from here. From bfb1a566d9d49b41b828e7640f8300f79938677e Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:19:09 +0100 Subject: [PATCH 08/69] Dispatch log event on reception from backend --- gcs/src/redux/middleware/socketMiddleware.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 20defb958..968794e46 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -47,6 +47,7 @@ import { setExtraData, } from "../slices/droneInfoSlice" import { pushMessage } from "../slices/statusTextSlice.js" +import { emitLog } from "../slices/loggingSlice.js" const SocketEvents = Object.freeze({ // socket.on events @@ -139,6 +140,12 @@ const socketMiddleware = (store) => { console.log(`Disconnected from socket via redux, ${socket.socket.id}`) store.dispatch(socketDisconnected()) }) + + socket.socket.on("log", (msg) => { + // Now we just want to send these to electron + console.log(`Got Log Message from backend: ${msg.level} - ${msg.message} ${msg.timestamp}`) + store.dispatch(emitLog(msg)) + }) /* ==================== From 928c94db0ad9f1c5eec03b6863668fa7a360b8a4 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:19:26 +0100 Subject: [PATCH 09/69] Add log history --- gcs/src/redux/slices/loggingSlice.js | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 gcs/src/redux/slices/loggingSlice.js diff --git a/gcs/src/redux/slices/loggingSlice.js b/gcs/src/redux/slices/loggingSlice.js new file mode 100644 index 000000000..e81906264 --- /dev/null +++ b/gcs/src/redux/slices/loggingSlice.js @@ -0,0 +1,35 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const KEEP_LATEST_N_LOGS = 50; + +const loggingSlice = createSlice({ + name: "logging", + initialState: { + logHistory: [], + handlers: [], + }, + reducers: { + emitLog: (state, action) => { + state.logHistory.push(action.payload); + if (state.logHistory.length > KEEP_LATEST_N_LOGS) state.logHistory.shift(); + + state.handlers.forEach(handler => { + handler(action.payload); + }) + }, + registerHandler: (state, action) => { + if (action.payload in state.handlers) return; + state.handlers.push(action.payload); + } + }, + selectors: { + selectRecentLogs: (state) => state.logHistory, + selectMostRecent: (state, n) => state.logHistory.slice(-n, -1) + }, + +}) + +export const {emitLog, registerHandler} = loggingSlice.actions +export const {selectMostRecent, selectRecentLogs} = loggingSlice.selectors + +export default loggingSlice \ No newline at end of file From 0894783a7550cb49a96e84f54d3fcf63554f60c8 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 11:19:40 +0100 Subject: [PATCH 10/69] Ensure logging is initialised after sockio connection --- radio/app/__init__.py | 8 ++++---- radio/app/loggingConfig.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/radio/app/__init__.py b/radio/app/__init__.py index 03f24168b..aa1dc8b00 100644 --- a/radio/app/__init__.py +++ b/radio/app/__init__.py @@ -3,10 +3,9 @@ from flask import Flask from flask_socketio import SocketIO -from loggingConfig import setup_logging +from app.loggingConfig import setup_logging socketio = SocketIO(cors_allowed_origins="*", async_mode="threading") - logger = logging.getLogger("fgcs") def create_app(debug: bool = False) -> Flask: @@ -17,7 +16,6 @@ def create_app(debug: bool = False) -> Flask: debug: Boolean value for if the debugging should be True or False """ - setup_logging(socketio, debug) from app.endpoints import endpoints @@ -27,6 +25,8 @@ def create_app(debug: bool = False) -> Flask: app.register_blueprint(endpoints) - logger.info("Initialising app") socketio.init_app(app) + setup_logging(socketio, debug) + + logger.info("Initialising app") return app diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index 84d7ca3b2..6bec22821 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -1,4 +1,5 @@ import sys +import time import logging from flask_socketio import SocketIO @@ -11,9 +12,10 @@ def __init__(self, sio: SocketIO) -> None: self.socket = sio def emit(self, record) -> None: + try: entry = self.format(record) - self.socket.emit("log", {"level": record.levelname, "msg": entry}) + self.socket.emit("log", {"level": record.levelname, "message": entry, "timestamp": time.time()}) except: self.handleError(record) From 214e91f7c1fd038effd0c0ce3b2d866aa1a9932d Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:40:35 +0100 Subject: [PATCH 11/69] Add log4js --- gcs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/gcs/package.json b/gcs/package.json index ae71c4c11..f5692eb92 100644 --- a/gcs/package.json +++ b/gcs/package.json @@ -49,6 +49,7 @@ "colormap": "^2.3.2", "eslint-config-prettier": "^9.1.0", "glob": "^10.3.12", + "log4js": "^6.9.1", "mapbox-gl": "^3.0.1", "maplibre-gl": "^3.6.2", "moment": "^2.29.4", From 087db9d6976e73fdbd72e8fa17fbd3d878596c30 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:40:40 +0100 Subject: [PATCH 12/69] Add logging settings --- gcs/data/default_settings.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index 1c89cc4cb..a1467b615 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -33,6 +33,21 @@ "default": false, "type": "boolean", "display": "Sync Dashboard and Missions Map Viewstate" + }, + "logDirectory": { + "default": "", + "type": "directory", + "display": "Log directory" + }, + "onlyKeepLastLog": { + "default": false, + "type": "boolean", + "display": "Only keep the most recent log file" + }, + "combineLogs": { + "default": false, + "type": "boolean", + "display": "Combine frontend and backend log files" } }, "Dashboard": { From 0b5b3be4304f31e66c23eaa271f3c49e17d2bdd4 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:40:54 +0100 Subject: [PATCH 13/69] Extract electron functionality into modules --- gcs/electron/main.ts | 161 ++++--------------------------- gcs/electron/modules/logging.ts | 77 +++++++++++++++ gcs/electron/modules/settings.ts | 72 ++++++++++++++ gcs/electron/modules/webcam.ts | 84 ++++++++++++++++ 4 files changed, 254 insertions(+), 140 deletions(-) create mode 100644 gcs/electron/modules/logging.ts create mode 100644 gcs/electron/modules/settings.ts create mode 100644 gcs/electron/modules/webcam.ts diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index a5004937e..713d0fbc8 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, Event, Menu, MenuItemConstructorOptions, MessageBoxOptions, Rectangle, app, dialog, ipcMain, nativeImage, shell } from 'electron' +import { BrowserWindow, Menu, MenuItemConstructorOptions, MessageBoxOptions, app, dialog, ipcMain, nativeImage, shell } from 'electron' import { ChildProcessWithoutNullStreams, spawn, spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' @@ -6,6 +6,9 @@ import packageInfo from '../package.json' // @ts-expect-error - no types available import openFile, { clearRecentFiles, getRecentFiles } from './fla' +import registerSettingsIPC, { getUserConfiguration } from './modules/settings' +import registerWebcamIPC, { setupWebcamWindow } from './modules/webcam' +import registerLoggingIPC, { setupLog4js } from './modules/logging' // The built directory structure // // ├─┬─┬ dist @@ -44,144 +47,8 @@ function getWindow() { return BrowserWindow.getFocusedWindow() } -// Settings logic - -interface Settings { - version: string, - settings: object -} - -let userSettings: Settings | null = null - -function saveUserConfiguration(settings: Settings){ - userSettings = settings; - fs.writeFileSync(path.join(app.getPath('userData'), 'settings.json'), JSON.stringify(userSettings, null, 2), 'utf-8'); -} - -/** - * Checks the application version within the loaded user settings and updates if it is outdated - * @param configPath The path to the configuration file - * @returns - */ -function checkAppVersion(configPath: string){ - - if (userSettings === null){ - console.warn("Attempting to check app version when user settings have not been loaded"); - return; - } - - if (userSettings.version == app.getVersion()) - return; - - userSettings.version = app.getVersion(); - fs.writeFileSync(configPath, JSON.stringify(userSettings)) -} - -/** - * Called when the application requests user settings - * - * @returns - */ -function getUserConfiguration(){ - - // Return the already loaded user settings if loaded - console.log("Fetching user settings!"); - if (userSettings !== null) return userSettings - - - // Directories - const userDir = app.getPath('userData'); - const config = path.join(userDir, 'settings.json'); - - // Write version and blank settings to user config if doesn't exist - if (!fs.existsSync(config)) { - console.log("Generating user settings") - userSettings = {version: app.getVersion(), settings: {}} - fs.writeFileSync(config, JSON.stringify(userSettings)) - } else{ - console.log("Reading user settings from config file " + config) - userSettings = JSON.parse(fs.readFileSync(config, 'utf-8')) - checkAppVersion(config) - } - return userSettings -} - -ipcMain.handle("getSettings", () => {return getUserConfiguration(); }) -ipcMain.handle("setSettings", (_, settings) => {saveUserConfiguration(settings)}) - -// Webcam popout window - -const MIN_WEBCAM_HEIGHT: number = 100 -const WEBCAM_TITLEBAR_HEIGHT: number = 28 - -// Disable unused vars because they are needed for TS function type -// eslint-disable-next-line no-unused-vars -type ResizeCallback = (event: Event, arg1: Rectangle) => void; - -let currentResizeHandler: ResizeCallback | null = null - -/** - * If id and name are provided, passes the id and name to the webcam popout so that the given - * video stream is rendered. If id or name are not provided, prevents any video streams from - * being rendered on the window so that the webcam is not showing in the background - * @param id The device stream ID - * @param name The name of the device - */ -function loadWebcam(id: string = "", name: string = ""){ - - const params: string = id && name ? "/webcam?deviceId=" + id + "&deviceName=" + name : "/webcam"; - - if (VITE_DEV_SERVER_URL) - webcamPopoutWin?.loadURL(VITE_DEV_SERVER_URL + "#" + params) - else - webcamPopoutWin?.loadFile(path.join(process.env.DIST, 'index.html'), {hash: params}) -} - -function openWebcamPopout(videoStreamId: string, name: string, aspect: number){ - - if (webcamPopoutWin === null) return; - loadWebcam(videoStreamId, name); - - webcamPopoutWin.setTitle(name); - - // Remove previous resize handler - if (currentResizeHandler) - webcamPopoutWin.off("will-resize", currentResizeHandler) - - // Create resize handler to maintain aspect ratio - currentResizeHandler = function(event, newBounds){ - event.preventDefault(); - - const newWidth = newBounds.width; - const newHeight = Math.round((newWidth / aspect) + WEBCAM_TITLEBAR_HEIGHT); - - webcamPopoutWin?.setBounds({ - x: newBounds.x, - y: newBounds.y, - width: newWidth, - height: newHeight - }); - } - - webcamPopoutWin.on('will-resize', currentResizeHandler); - - // Ensure initial size fits the aspect ratio () - webcamPopoutWin.setSize(webcamPopoutWin.getBounds().width, Math.round(webcamPopoutWin.getBounds().width / aspect) + WEBCAM_TITLEBAR_HEIGHT); - - webcamPopoutWin.setMinimumSize(Math.round(aspect * (MIN_WEBCAM_HEIGHT-28)), MIN_WEBCAM_HEIGHT); - webcamPopoutWin.show(); - -} - -function closeWebcamPopout(){ - webcamPopoutWin?.hide() - loadWebcam(); - win?.webContents.send("webcam-closed"); -} - -ipcMain.handle("openWebcamWindow", (_, videoStreamId, name, aspect) => {openWebcamPopout(videoStreamId, name, aspect)}) -ipcMain.handle("closeWebcamWindow", () => closeWebcamPopout()) - +registerLoggingIPC(); +registerSettingsIPC(); ipcMain.handle("isMac", () => { return process.platform == "darwin" }) ipcMain.on('close', () => {closeWithBackend()}) @@ -203,6 +70,15 @@ ipcMain.on("zoom_out", () => { }) ipcMain.on("openFileInExplorer", (_event, filePath) => {shell.showItemInFolder(filePath)}) +ipcMain.handle('selectDirectory', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'], + defaultPath: app.getAppPath() + }); + if (result.canceled) return null; + return result.filePaths[0]; +}); + function createWindow() { win = new BrowserWindow({ icon: path.join(process.env.VITE_PUBLIC, 'app_icon.ico'), @@ -238,7 +114,8 @@ function createWindow() { // We load the webcam route here to prevent having to load the page on popout - loadWebcam(); + registerWebcamIPC(win); + setupWebcamWindow(); // Open links in browser, not within the electron window. // Note, links must have target="_blank" @@ -428,6 +305,10 @@ app.on('activate', () => { }) app.whenReady().then(() => { + + const {combineLogs, onlyKeepLastLog} = getUserConfiguration()?.settings["General"]; + setupLog4js(combineLogs, onlyKeepLastLog); + createLoadingWindow() // Open file and Get Recent Logs ipcMain.handle('fla:open-file', openFile) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts new file mode 100644 index 000000000..e42cc4244 --- /dev/null +++ b/gcs/electron/modules/logging.ts @@ -0,0 +1,77 @@ +/** + * Functions related to both console and file logging + */ +import path from "node:path"; +import { app, ipcMain } from "electron"; + +import * as log4js from "log4js"; + +export let electronLogger: log4js.Logger +export let frontendLogger: log4js.Logger + +const LOG_PATH = path.join(app.getPath("home"), "FGCS", "logs"); + +/** + * Get the path of the frontend log file, based on the user preferences. + * If the user wants both frontend and backend logs in a single file, then the log file + * is written in the FGCS/tmp directory along with the backend log, then combined into a single log file + */ +function getLogPath(name: string, combine: boolean, keepLast: boolean): string { + const logFile = combine || keepLast ? `${name}.log` : `${name}-${new Date().getTime()}.log`; + return path.join(LOG_PATH, logFile); +} + +export function setupLog4js(combineLogs: boolean, onlyKeepLast: boolean){ + + console.log("SETTUP ULOGING") + const logPath = getLogPath("frontend", combineLogs, onlyKeepLast); + + // Change these for the console and file patterns respectively + const CONSOLE_PATTERN = "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %m" + const FILE_PATTERN = "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %c - %m" + + log4js.configure({ + appenders: { + stdout: { + type: 'stdout', + layout: { + type: 'pattern', + pattern: CONSOLE_PATTERN, + } + }, + file: { + type: 'file', + filename: logPath, + layout: { + type: 'pattern', + pattern: FILE_PATTERN + }, + flags: "w" + } + }, + categories: { + electron: { + appenders: ["stdout", "file"], + level: 'info' + }, + frontend: { + appenders: ["file"], + level: 'debug' + }, + default: { + appenders: ["file"], + level: "info" + } + } + }) + + electronLogger = log4js.getLogger("electron"); + frontendLogger = log4js.getLogger("frontend"); + + electronLogger.info("Setup frontend logging"); +} + +export default function registerLoggingIPC(){ + console.log("Recieved log message") + ipcMain.handle("logMessage", (_, {level, message, timestamp}) => {frontendLogger.log(level, message)}) +} \ No newline at end of file diff --git a/gcs/electron/modules/settings.ts b/gcs/electron/modules/settings.ts new file mode 100644 index 000000000..79b3c479e --- /dev/null +++ b/gcs/electron/modules/settings.ts @@ -0,0 +1,72 @@ +import fs from 'node:fs' +import path from 'node:path' +import { app, ipcMain } from 'electron'; +import { exit } from 'node:process'; + +// Settings logic + +interface Settings { + version: string, + settings: object +} + +let userSettings: Settings | null = null + +function saveUserConfiguration(settings: Settings){ + userSettings = settings; + fs.writeFileSync(path.join(app.getPath('userData'), 'settings.json'), JSON.stringify(userSettings, null, 2), 'utf-8'); +} + +/** + * Checks the application version within the loaded user settings and updates if it is outdated + * @param configPath The path to the configuration file + * @returns + */ +function checkAppVersion(configPath: string){ + + if (userSettings === null){ + console.warn("Attempting to check app version when user settings have not been loaded"); + return; + } + + if (userSettings.version == app.getVersion()) + return; + + userSettings.version = app.getVersion(); + fs.writeFileSync(configPath, JSON.stringify(userSettings)) +} + +/** + * Called when the application requests user settings + * + * @returns Settings + */ +export function getUserConfiguration(){ + + // Return the already loaded user settings if loaded + console.log("Fetching user settings!"); + if (userSettings !== null) return userSettings + + + // Directories + const userDir = app.getPath('userData'); + const config = path.join(userDir, 'settings.json'); + + // Write version and blank settings to user config if doesn't exist + if (!fs.existsSync(config)) { + console.log("Generating user settings") + userSettings = {version: app.getVersion(), settings: {}} + fs.writeFileSync(config, JSON.stringify(userSettings)) + } else{ + console.log("Reading user settings from config file " + config) + userSettings = JSON.parse(fs.readFileSync(config, 'utf-8')) + checkAppVersion(config) + } + if (userSettings != null) return userSettings + exit(-1); +} + +export default function registerSettingsIPC(){ + ipcMain.handle("getSettings", () => {return getUserConfiguration(); }) + ipcMain.handle("setSettings", (_, settings) => {saveUserConfiguration(settings)}) +} \ No newline at end of file diff --git a/gcs/electron/modules/webcam.ts b/gcs/electron/modules/webcam.ts new file mode 100644 index 000000000..4bf390c02 --- /dev/null +++ b/gcs/electron/modules/webcam.ts @@ -0,0 +1,84 @@ +import path from "path"; +import { BrowserWindow, Event, ipcMain, Rectangle } from "electron"; + +let webcamPopoutWin: BrowserWindow | null + +const MIN_WEBCAM_HEIGHT: number = 100 +const WEBCAM_TITLEBAR_HEIGHT: number = 28 + +// eslint-disable-next-line no-unused-vars +type ResizeCallback = (event: Event, arg1: Rectangle) => void; + +let currentResizeHandler: ResizeCallback | null = null + +export function setupWebcamWindow(){ + webcamPopoutWin = new BrowserWindow({ + width: 400, + height: 300, + frame: false, + alwaysOnTop: true, + icon: path.join(process.env.VITE_PUBLIC, 'app_icon.ico'), + show: false, + title: "Webcam", + webPreferences: { + nodeIntegration: true, + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true + }, + fullscreen: false, + fullscreenable: false, + }); + webcamPopoutWin.loadURL("http://localhost:5173/#/webcam") +} + +export function openWebcamPopout(videoStreamId: string, name: string, aspect: number){ + + if (webcamPopoutWin === null) return; + + webcamPopoutWin.loadURL("http://localhost:5173/#/webcam?deviceId=" + videoStreamId + "&deviceName=" + name); + webcamPopoutWin.setTitle(name); + + // Remove previous resize handler + if (currentResizeHandler) + webcamPopoutWin.off("will-resize", currentResizeHandler) + + // Create resize handler to maintain aspect ratio + currentResizeHandler = function(event, newBounds){ + event.preventDefault(); + + const newWidth = newBounds.width; + const newHeight = Math.round((newWidth / aspect) + WEBCAM_TITLEBAR_HEIGHT); + + webcamPopoutWin?.setBounds({ + x: newBounds.x, + y: newBounds.y, + width: newWidth, + height: newHeight + }); + } + + webcamPopoutWin.on('will-resize', currentResizeHandler); + + // Ensure initial size fits the aspect ratio () + webcamPopoutWin.setSize(webcamPopoutWin.getBounds().width, Math.round(webcamPopoutWin.getBounds().width / aspect) + WEBCAM_TITLEBAR_HEIGHT); + + webcamPopoutWin.setMinimumSize(Math.round(aspect * (MIN_WEBCAM_HEIGHT-28)), MIN_WEBCAM_HEIGHT); + webcamPopoutWin.show(); + +} + +export function closeWebcamPopout(mainWindow: BrowserWindow | null){ + webcamPopoutWin?.hide() + webcamPopoutWin?.loadURL("http://localhost:5173/#/webcam") + mainWindow?.webContents.send("webcam-closed"); +} + +export function destroyWebcamWindow(){ + webcamPopoutWin?.close() + webcamPopoutWin = null +} + +export default function registerWebcamIPC(mainWindow: BrowserWindow){ + ipcMain.handle("openWebcamWindow", (_, videoStreamId, name, aspect) => {openWebcamPopout(videoStreamId, name, aspect)}) + ipcMain.handle("closeWebcamWindow", () => closeWebcamPopout(mainWindow)) +} \ No newline at end of file From f7dc2bb65a5f63d3b9079c074793047b022ddd38 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:41:04 +0100 Subject: [PATCH 14/69] Add exposed ipcRenderer methods --- gcs/electron/preload.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gcs/electron/preload.js b/gcs/electron/preload.js index e7149140f..702203c7b 100644 --- a/gcs/electron/preload.js +++ b/gcs/electron/preload.js @@ -17,6 +17,8 @@ contextBridge.exposeInMainWorld("ipcRenderer", { closeWebcamWindow: () => ipcRenderer.invoke("closeWebcamWindow"), onCameraWindowClose: (callback) => ipcRenderer.on("webcam-closed", () => callback()), + pushLog: (msg) => ipcRenderer.invoke("logMessage", msg), + selectDirectory: () => ipcRenderer.invoke('selectDirectory'), }) // `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it. From a992f819a5f11ab4540290fd6bc98f7aec35f0aa Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:41:13 +0100 Subject: [PATCH 15/69] Add electron log handler --- gcs/src/components/mainContent.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx index e593ea333..787820443 100644 --- a/gcs/src/components/mainContent.jsx +++ b/gcs/src/components/mainContent.jsx @@ -30,7 +30,7 @@ import AlertProvider from "./dashboard/alertProvider" import ErrorBoundaryFallback from "./error/errorBoundary" import { initSocket } from "../redux/slices/socketSlice" import { registerHandler } from "../redux/slices/loggingSlice" -import { consoleLogHandler, fileLogHandler } from "../helpers/logHandlers" +import { consoleLogHandler, electronLogHandler } from "../helpers/logHandlers" export default function AppContent() { // Conditionally render UI so the webcam route is literally just a webcam @@ -46,7 +46,7 @@ export default function AppContent() { dispatch(registerHandler(consoleLogHandler)) } - dispatch(registerHandler(fileLogHandler)) + dispatch(registerHandler(electronLogHandler)) }, []) return ( From 9b6238137bd15113a2922cd2a9ff5a233e2bed5a Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 22 Aug 2025 12:41:18 +0100 Subject: [PATCH 16/69] Add directory setting type --- gcs/src/components/settingsModal.jsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index c1062eccc..f3aee6a01 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -125,6 +125,20 @@ function ExtendableNumberSetting({ settingName, range }) { ) } +function DirectorySetting({settingName}) { + const { getSetting, setSetting } = useSettings() + + const setDirectory = async () => { + const dirHandle = await window.ipcRenderer.selectDirectory(); + if (dirHandle !== null) setSetting(settingName, dirHandle); + } + + return ( + // +
{getSetting(settingName)}
+ ) +} + function Setting({ settingName, df }) { return (
) : df.type == "option" ? ( + ) : df.type == "directory" ? ( + ) : (
) From bc3ea4bd042c5fe0ceb9a49b7496076a5cc4d236 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 25 Aug 2025 15:31:31 +0100 Subject: [PATCH 37/69] Ignore annoying typescript issue --- gcs/electron/modules/logging.ts | 13 +++++++++---- gcs/electron/modules/settings.ts | 2 +- radio/app/drone.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index 05928723d..e1546db79 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -5,6 +5,8 @@ import path from "node:path"; import { app, ipcMain } from "electron"; import * as log4js from "log4js"; + +// @ts-ignore import * as layouts from "log4js/lib/layouts"; let frontendLogger: log4js.Logger @@ -111,10 +113,13 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l backendLogger = log4js.getLogger("backend"); // Log any logs that came through before logging was initialised - logBuffer.forEach(lb => frontendLogger.log(lb.level, lb.message)) - - frontendLogger.info("Setup frontend logging"); + /* frontendLogger.info() + logBuffer.forEach(lb => frontendLogger.info(lb.message)) */ initialised = true + + logBuffer.forEach(logHelper); + + logInfo("Setup user logging") } // We export these log functions purely for logging within electron @@ -122,7 +127,7 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l // Electron process export function logHelper(log: BufferedLog) { if (initialised) - frontendLogger.log(log.level, log.message) + frontendLogger.log(log.level, {_epoch: Date.now() / 1000}, log.message) else logBuffer.push(log) } diff --git a/gcs/electron/modules/settings.ts b/gcs/electron/modules/settings.ts index 1013f6b74..3fa331b0f 100644 --- a/gcs/electron/modules/settings.ts +++ b/gcs/electron/modules/settings.ts @@ -71,8 +71,8 @@ function checkAppVersion(configPath: string){ export function getUserConfiguration(): Settings{ // Return the already loaded user settings if loaded - logDebug("Fetching user settings") if (userSettings !== null) return userSettings + logDebug("Fetching user settings") // Directories diff --git a/radio/app/drone.py b/radio/app/drone.py index fbc8c8009..2583e7ff0 100644 --- a/radio/app/drone.py +++ b/radio/app/drone.py @@ -111,7 +111,7 @@ def __init__( self.master: mavutil.mavserial = mavutil.mavlink_connection(port, baud=baud) except Exception as e: self.master = None - self.logger.critical(str(e)) + self.logger.error(str(e)) if isinstance(e, SerialException): self.connectionError = "Could not connect to drone, invalid port." elif isinstance(e, ConnectionRefusedError): From a26684fd7b8ca3197bc2d8a8fe743dce710b0bf0 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 25 Aug 2025 15:35:13 +0100 Subject: [PATCH 38/69] Remove caller from log format if logging to separate files --- gcs/electron/modules/logging.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index e1546db79..ed308b906 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -32,6 +32,9 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l const appenders = [isDev && !combineLogFiles ? "multifile" : "file"] const directory = isDev && logToWorkspace ? path.join(app.getAppPath(), "logs") : app.getPath("logs") + // If we are logging to separate files no point including the logger name + const resolvedFormat = logFormat.replace("%c", '').replace(" ", " ") + // Log to console as well if in dev if (isDev) appenders.push("console") @@ -84,7 +87,7 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l extension: ".log", layout: { type: 'epochLayout', - pattern: logFormat + pattern: resolvedFormat }, flags: "w" } From 53eb7f217446039936f0c9851a6509f3fd9b7566 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 25 Aug 2025 17:05:47 +0100 Subject: [PATCH 39/69] Add socket emit hook --- gcs/src/redux/middleware/socketMiddleware.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 90ad8b77d..fb4402cd2 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -145,18 +145,25 @@ const socketMiddleware = (store) => { ======================== */ - // In development mode, hook socket on so that we can debug log every time we recieve a socket message + // In development mode, hook socket on and emit so that we can debug log every time we recieve / send a socket message if (process.env.NODE_ENV === "development") { const originalOn = socket.socket.on.bind(socket.socket); socket.socket.on = (event, callback) => { const wrappedCallback = (...args) => { - logDebug(`Event "${event}" recieved by frontend`, ...args); + logDebug(`Event "${event}" recieved by frontend with values ${args.join(", ")}`); callback(...args); }; return originalOn(event, wrappedCallback); }; + + const originalEmit = socket.socket.emit.bind(socket.socket); + + socket.socket.emit = (event, ...args) => { + logDebug(`Event ${event} emitted by frontend with args (${args.join(", ")})`) + return originalEmit(event, ...args) + } } // debug socket logging From a4878e844528e1e24efd48206cb62ec9a3541a36 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 25 Aug 2025 17:45:58 +0100 Subject: [PATCH 40/69] Stop typescript crying at me --- gcs/electron/modules/logging.ts | 53 ++++++++++++-------- gcs/src/redux/middleware/socketMiddleware.js | 2 +- gcs/src/redux/slices/loggingSlice.js | 2 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index ed308b906..016c4a625 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -16,7 +16,8 @@ let initialised: boolean = false; interface BufferedLog { message: string, - level: log4js.Level + level: log4js.Level, + data: any[] } let logBuffer: BufferedLog[] = [] @@ -27,6 +28,8 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l return; } + logInfo("Setting up logging with args (%s, %s, %s, %s)", logToWorkspace, combineLogFiles, logFormat, loggingLevel) + const isDev = process.env.NODE_ENV === 'development' const appenders = [isDev && !combineLogFiles ? "multifile" : "file"] @@ -95,15 +98,18 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l categories: { electron: { appenders: appenders, - level: 'info' + level: 'info', + enableCallStack: true }, frontend: { appenders: appenders, - level: loggingLevel + level: loggingLevel, + enableCallStack: true }, backend: { appenders: appenders, - level: loggingLevel + level: loggingLevel, + enableCallStack: true }, default: { appenders: ["console"], @@ -130,38 +136,45 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l // Electron process export function logHelper(log: BufferedLog) { if (initialised) - frontendLogger.log(log.level, {_epoch: Date.now() / 1000}, log.message) + (frontendLogger as any)[log.level.levelStr.toLowerCase()]({_epoch: Date.now() / 1000}, log.message, ...log.data) else logBuffer.push(log) } -export function logDebug(message: string) { - logHelper({message: message, level: log4js.levels.DEBUG}) +export function logDebug(message: string, ...args: any[]) { + logHelper({message: message, level: log4js.levels.DEBUG, data: args}) +} + +export function logInfo(message: string, ...args: any[]) { + logHelper({message: message, level: log4js.levels.INFO, data: args}) } -export function logInfo(message: string) { - logHelper({message: message, level: log4js.levels.INFO}) +export function logWarning(message: string, ...args: any[]) { + logHelper({message: message, level: log4js.levels.WARN, data: args}) } -export function logWarning(message: string) { - logHelper({message: message, level: log4js.levels.WARN}) +export function logError(message: string, ...args: any[]) { + logHelper({message: message, level: log4js.levels.ERROR, data: args}) } -export function logError(message: string) { - logHelper({message: message, level: log4js.levels.ERROR}) +export function logFatal(message: string, ...args: any[]) { + logHelper({message: message, level: log4js.levels.FATAL, data: args}) } -export function logFatal(message: string) { - logHelper({message: message, level: log4js.levels.FATAL}) +interface LogPayload { + level: 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical', + message: string, + timestamp: number, + source: string } export default function registerLoggingIPC(){ - ipcMain.handle("logMessage", (_, {level, message, timestamp, source}) => { - // backend logs from python come in with CRITICAL level, log4js calls it FATAL (like everything else) - const resolvedLevel = level === "CRITICAL" ? "FATAL" : level; + ipcMain.handle("logMessage", (_, {level, message, timestamp, source}: LogPayload) => { + // backend logs from python come in with CRITICAL level, log4js calls it FATAL (like every other logger ever) + const resolvedLevel = level === "critical" ? "fatal" : level; source === "backend" - ? backendLogger.log(resolvedLevel, {_epoch: timestamp}, message) - : frontendLogger.log(level, {_epoch: timestamp}, message); + ? backendLogger[resolvedLevel]({_epoch: timestamp}, message) + : frontendLogger[resolvedLevel]({_epoch: timestamp}, message); }) } \ No newline at end of file diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index fb4402cd2..b17afeab1 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -161,7 +161,7 @@ const socketMiddleware = (store) => { const originalEmit = socket.socket.emit.bind(socket.socket); socket.socket.emit = (event, ...args) => { - logDebug(`Event ${event} emitted by frontend with args (${args.join(", ")})`) + logDebug(`Event "${event}" emitted by frontend with args (${args.join(", ")})`) return originalEmit(event, ...args) } } diff --git a/gcs/src/redux/slices/loggingSlice.js b/gcs/src/redux/slices/loggingSlice.js index 0dc61668a..fc2ef4e2a 100644 --- a/gcs/src/redux/slices/loggingSlice.js +++ b/gcs/src/redux/slices/loggingSlice.js @@ -15,7 +15,7 @@ const loggingSlice = createSlice({ if (state.logHistory.length > KEEP_LATEST_N_LOGS) state.logHistory.shift(); state.handlers.forEach(handler => { - handler({...action.payload, timestamp: action.payload.timestamp ?? Date.now() / 1000}); + handler({...action.payload, timestamp: action.payload.timestamp ?? Date.now() / 1000, level: action.payload.level.toLowerCase()}); }) state.debugLogCount += 1 }, From 3851c80af8aafca987d4ea77108b6fd19fa75de2 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 25 Aug 2025 21:47:27 +0100 Subject: [PATCH 41/69] Finished up logging --- gcs/data/default_settings.json | 2 +- gcs/electron/modules/logging.ts | 45 +++++++++++++++++---------------- gcs/src/helpers/logging.js | 12 +++++++-- radio/app/loggingConfig.py | 8 +++++- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index 7b09bdf99..2ff2b41ec 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -72,7 +72,7 @@ "display": "Log Format", "type": "string", "matches": "^(?:[^%]|%(?:d(?:\\{[^}]+\\})?|p|c|m|n|h|z|f|l|o|r|x\\{[^}]+\\}))*$", - "default": "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %c - %m" + "default": "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %f:%l - %m" }, "loggingLevel": { "description": "Current development log level", diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index 016c4a625..4e1292451 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -45,23 +45,13 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l // the log is sent not when it is recieved, we need a custom layout to deal with it) log4js.addLayout('epochLayout', (config: log4js.Config) => { return (loggingEvent) => { - - let ts; - const data = loggingEvent.data[0] - - if (data._epoch != null) { - - const epoch = Number(data._epoch); - const epochMs = epoch * 1e3; - ts = new Date(epochMs).toISOString(); - } else { - ts = loggingEvent.startTime.toISOString(); - } - - const newTokens = {...config.tokens, d: () => ts} - - return layouts.patternLayout(config.pattern, newTokens)({...loggingEvent, data: loggingEvent.data.slice(1)}); + return layouts.patternLayout(config.pattern)({...loggingEvent, + startTime: new Date(data._epoch * 1000), + fileName: data._file, + lineNumber: data._lineNo, + data: loggingEvent.data.slice(1) + }); }; }); @@ -135,8 +125,17 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l // Since there is a chance logging is uninitialised when logging within the // Electron process export function logHelper(log: BufferedLog) { - if (initialised) - (frontendLogger as any)[log.level.levelStr.toLowerCase()]({_epoch: Date.now() / 1000}, log.message, ...log.data) + if (initialised) { + + const err = new Error() + const caller = err.stack?.split('\n').at(3) ?? "unknown" + const file = caller.split("\\").at(-1) ?? ""; + const fileName = file.slice(0, file.indexOf(":")); + + const lineNo = caller.split(":").at(-2); + + (frontendLogger as any)[log.level.levelStr.toLowerCase()]({_epoch: Date.now() / 1000, _file: fileName, _lineNo: lineNo}, log.message, ...log.data) + } else logBuffer.push(log) } @@ -165,16 +164,18 @@ interface LogPayload { level: 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical', message: string, timestamp: number, - source: string + source: string, + file: string, + line: number } export default function registerLoggingIPC(){ - ipcMain.handle("logMessage", (_, {level, message, timestamp, source}: LogPayload) => { + ipcMain.handle("logMessage", (_, {level, message, timestamp, source, file, line}: LogPayload) => { // backend logs from python come in with CRITICAL level, log4js calls it FATAL (like every other logger ever) const resolvedLevel = level === "critical" ? "fatal" : level; source === "backend" - ? backendLogger[resolvedLevel]({_epoch: timestamp}, message) - : frontendLogger[resolvedLevel]({_epoch: timestamp}, message); + ? backendLogger[resolvedLevel]({_epoch: timestamp, _file: file, _lineNo: line}, message) + : frontendLogger[resolvedLevel]({_epoch: timestamp, _file: file, _lineNo: line}, message); }) } \ No newline at end of file diff --git a/gcs/src/helpers/logging.js b/gcs/src/helpers/logging.js index fe195907e..dec2de2c2 100644 --- a/gcs/src/helpers/logging.js +++ b/gcs/src/helpers/logging.js @@ -3,13 +3,21 @@ import { store } from "../redux/store" function logHelper(level, msg){ + const err = new Error(); + const caller = err.stack.split('\n')[3]; + + const file = caller.split("/").at(-1) + const fileName = file.slice(0, file.indexOf(":")) + const lineNo = caller.split(":").at(-2) + store.dispatch(emitLog({ message: msg, level: level, timestamp: new Date() / 1000, - source: "frontend" + source: "frontend", + file: fileName, + line: lineNo })) - } export function logDebug(msg){ diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index 715fac4b9..1e5171e24 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -15,7 +15,13 @@ def emit(self, record) -> None: try: entry = self.format(record) - self.socket.emit("log", {"level": record.levelname, "message": entry, "timestamp": time.time()}) + self.socket.emit("log", { + "level": record.levelname, + "message": entry, + "timestamp": time.time(), + "file": record.filename, + "line": record.lineno + }) except: self.handleError(record) From 67246991339ade44d1cf18ca80fc221d7f6d61ef Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 10:12:57 +0100 Subject: [PATCH 42/69] Add a bit of documentation --- gcs/electron/modules/logging.ts | 58 +++++++++++++++++++------------- gcs/electron/modules/settings.ts | 31 +++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index 4e1292451..ee3a6dfcd 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -1,5 +1,5 @@ /** - * Functions related to both console and file logging + * Functions related to application-wide logging */ import path from "node:path"; import { app, ipcMain } from "electron"; @@ -21,6 +21,15 @@ interface BufferedLog { } let logBuffer: BufferedLog[] = [] +/** + * Setup logging for the application. Logs from the frontend are dispatched to this logger + * through ipcRenderer.pushLog. Logs from backend are dispatched to logger through socket, then ipcRenderer.pushLog + * + * @param logToWorkspace Developer setting forcing logs to be stored in gcs/logs + * @param combineLogFiles Developer setting that combines the log files into one "fgcs.log" file + * @param logFormat The format for the logger (see log4js patterns) + * @param loggingLevel The level of the frontend and backend loggers + */ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, logFormat: string, loggingLevel: string){ if (initialised) { @@ -28,21 +37,19 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l return; } - logInfo("Setting up logging with args (%s, %s, %s, %s)", logToWorkspace, combineLogFiles, logFormat, loggingLevel) - - const isDev = process.env.NODE_ENV === 'development' - - const appenders = [isDev && !combineLogFiles ? "multifile" : "file"] - const directory = isDev && logToWorkspace ? path.join(app.getAppPath(), "logs") : app.getPath("logs") + logDebug("Setting up logging with args (%s, %s, %s, %s)", logToWorkspace, combineLogFiles, logFormat, loggingLevel) + + const appenders = [combineLogFiles ? "multifile" : "file"] + const directory = logToWorkspace ? path.join(app.getAppPath(), "logs") : app.getPath("logs") // If we are logging to separate files no point including the logger name const resolvedFormat = logFormat.replace("%c", '').replace(" ", " ") // Log to console as well if in dev - if (isDev) appenders.push("console") + if (process.env.NODE_ENV === 'development') appenders.push("console") - // Since logs coming from the backend supply their own epoch timestamp (so that we can log based on when - // the log is sent not when it is recieved, we need a custom layout to deal with it) + // Since logs coming from the backend supply their own epoch timestamp, file and line no (so that we can log based on when + // the log is sent not when it is recieved), we need a custom layout to deal with it log4js.addLayout('epochLayout', (config: log4js.Config) => { return (loggingEvent) => { const data = loggingEvent.data[0] @@ -108,25 +115,30 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l } }) - frontendLogger = log4js.getLogger("frontend"); - backendLogger = log4js.getLogger("backend"); + frontendLogger = log4js.getLogger("frontend") + backendLogger = log4js.getLogger("backend") - // Log any logs that came through before logging was initialised - /* frontendLogger.info() - logBuffer.forEach(lb => frontendLogger.info(lb.message)) */ + // Dispatch log message that arrived before setup initialised = true - - logBuffer.forEach(logHelper); + logBuffer.forEach(logHelper) + logBuffer = [] logInfo("Setup user logging") } -// We export these log functions purely for logging within electron -// Since there is a chance logging is uninitialised when logging within the -// Electron process -export function logHelper(log: BufferedLog) { +/** + * Log the given buffered log + * + * If logging is initialised, sends the log straight to the frontend log4js logger. + * If logging is not initialised yet, appends to a buffer which gets logged once + * log4js is initialised + * + * @param log The log data + */ +function logHelper(log: BufferedLog) { if (initialised) { + // Inspect stack to find file const err = new Error() const caller = err.stack?.split('\n').at(3) ?? "unknown" const file = caller.split("\\").at(-1) ?? ""; @@ -173,9 +185,9 @@ export default function registerLoggingIPC(){ ipcMain.handle("logMessage", (_, {level, message, timestamp, source, file, line}: LogPayload) => { // backend logs from python come in with CRITICAL level, log4js calls it FATAL (like every other logger ever) - const resolvedLevel = level === "critical" ? "fatal" : level; + const resolvedLevel = level === "critical" ? "fatal" : level source === "backend" ? backendLogger[resolvedLevel]({_epoch: timestamp, _file: file, _lineNo: line}, message) - : frontendLogger[resolvedLevel]({_epoch: timestamp, _file: file, _lineNo: line}, message); + : frontendLogger[resolvedLevel]({_epoch: timestamp, _file: file, _lineNo: line}, message) }) } \ No newline at end of file diff --git a/gcs/electron/modules/settings.ts b/gcs/electron/modules/settings.ts index 3fa331b0f..87a4d0228 100644 --- a/gcs/electron/modules/settings.ts +++ b/gcs/electron/modules/settings.ts @@ -1,11 +1,11 @@ -// This file contains logs of disgusting typescript but it DOES +// This file contains logs of disgusting hacky typescript but it DOES // make it so that accessing a setting which does not exist in default_settings.json -// prevents compilation so that's good +// causes a build error so we should never crash from accessing non-existant settings import fs from 'node:fs' import path from 'node:path' -import { app, ipcMain } from 'electron'; import { exit } from 'node:process'; +import { app, ipcMain } from 'electron'; import Data from "../../data/default_settings.json" import { logDebug, logFatal, logInfo, logWarning } from './logging'; @@ -23,8 +23,8 @@ interface DefaultSetting> { [k: string]: any } - - +// Hack to create a type which MAY contain keys from the default_settings.json, and each of those keys +// MAY contain any number of the settings listed in that section of default_settings.json type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; }; @@ -35,7 +35,6 @@ interface Settings { settings: PartialSettings } - let userSettings: Settings | null = null const defaultSettings: SettingsShape = Data @@ -74,7 +73,6 @@ export function getUserConfiguration(): Settings{ if (userSettings !== null) return userSettings logDebug("Fetching user settings") - // Directories const userDir = app.getPath('userData'); const config = path.join(userDir, 'settings.json'); @@ -95,8 +93,13 @@ export function getUserConfiguration(): Settings{ exit(-1); } +function getDefault>(group: G, setting: S): SettingValue { + const df = defaultSettings[group][setting] as DefaultSetting + return df.default +} + /** - * Get the given user setting + * Get the given user setting (or the default value from default_settings.json) * * A method exists for this in the frontend (see the settings provider) * but this is useful to have if you need to access a setting in electron @@ -107,15 +110,15 @@ export function getUserConfiguration(): Settings{ */ export function getSetting>(group: G, setting: S): SettingValue { - const userConfig = getUserConfiguration().settings[group] as Partial | undefined; + // Development settings only take effect in development! + if (process.env.NODE_ENV == "production" && group == "Development") return getDefault(group, setting) + + //Get user setting or return default if it doesn't exist + const userConfig = getUserConfiguration().settings[group] as Partial; const userSetting = userConfig?.[setting]; if (userSetting) return userSetting as SettingValue - - // Fallback to the defaultSettings - - const defaultSetting = defaultSettings[group][setting] as DefaultSetting - return defaultSetting.default + return getDefault(group, setting) } export default function registerSettingsIPC(){ From 289d8f30c3907d736f3899fd158471a6de759ec5 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 10:43:48 +0100 Subject: [PATCH 43/69] Update default log format --- gcs/data/default_settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index 2ff2b41ec..0fb777e06 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -71,8 +71,7 @@ "description": "Timestamp format for log messages", "display": "Log Format", "type": "string", - "matches": "^(?:[^%]|%(?:d(?:\\{[^}]+\\})?|p|c|m|n|h|z|f|l|o|r|x\\{[^}]+\\}))*$", - "default": "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %f:%l - %m" + "default": "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %c - %m" }, "loggingLevel": { "description": "Current development log level", From 3be5a9c6a995f00c177f2e4ae8c2abe15e8f03e4 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 10:54:34 +0100 Subject: [PATCH 44/69] Removed unused electron logger --- gcs/electron/modules/logging.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index ee3a6dfcd..de3eb8d01 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -93,11 +93,6 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l } }, categories: { - electron: { - appenders: appenders, - level: 'info', - enableCallStack: true - }, frontend: { appenders: appenders, level: loggingLevel, From 5e3013ca4f60aa1cee0ea99a643f9b7c042b418d Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:34:31 +0100 Subject: [PATCH 45/69] Moved log dispatch to middleware because reducers are supposed to be pure functions :( --- gcs/src/redux/middleware/loggingMiddleware.js | 19 +++++++++++++++++++ gcs/src/redux/middleware/socketMiddleware.js | 2 +- gcs/src/redux/slices/loggingSlice.js | 4 ---- gcs/src/redux/store.js | 3 ++- radio/app/loggingConfig.py | 5 ++--- 5 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 gcs/src/redux/middleware/loggingMiddleware.js diff --git a/gcs/src/redux/middleware/loggingMiddleware.js b/gcs/src/redux/middleware/loggingMiddleware.js new file mode 100644 index 000000000..cc5e5f193 --- /dev/null +++ b/gcs/src/redux/middleware/loggingMiddleware.js @@ -0,0 +1,19 @@ + + +const loggingMiddleware = store => next => action => { + + console.log(action.type) + const result = next(action); + const state = store.getState(); + + if (action.type == 'logging/emitLog') { + state.logging.handlers.forEach(handler => { + handler({...action.payload, timestamp: action.payload.timestamp ?? Date.now() / 1000, level: action.payload.level.toLowerCase()}); + }) + } + + return result; +} + + +export default loggingMiddleware \ No newline at end of file diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index b17afeab1..aa832713e 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -541,7 +541,7 @@ const socketMiddleware = (store) => { } } - next(action) + return next(action) } } diff --git a/gcs/src/redux/slices/loggingSlice.js b/gcs/src/redux/slices/loggingSlice.js index fc2ef4e2a..900984b6c 100644 --- a/gcs/src/redux/slices/loggingSlice.js +++ b/gcs/src/redux/slices/loggingSlice.js @@ -13,10 +13,6 @@ const loggingSlice = createSlice({ emitLog: (state, action) => { state.logHistory.push(action.payload); if (state.logHistory.length > KEEP_LATEST_N_LOGS) state.logHistory.shift(); - - state.handlers.forEach(handler => { - handler({...action.payload, timestamp: action.payload.timestamp ?? Date.now() / 1000, level: action.payload.level.toLowerCase()}); - }) state.debugLogCount += 1 }, registerHandler: (state, action) => { diff --git a/gcs/src/redux/store.js b/gcs/src/redux/store.js index b6c5a8cdb..9b3498382 100644 --- a/gcs/src/redux/store.js +++ b/gcs/src/redux/store.js @@ -16,6 +16,7 @@ import missionInfoSlice from "./slices/missionSlice" import statusTextSlice from "./slices/statusTextSlice" import notificationSlice from "./slices/notificationSlice" import loggingSlice from "./slices/loggingSlice" +import loggingMiddleware from "./middleware/loggingMiddleware" const rootReducer = combineSlices( logAnalyserSlice, @@ -39,7 +40,7 @@ export const store = configureStore({ return getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, - }).concat([socketMiddleware]) + }).concat([socketMiddleware, loggingMiddleware]) }, }) diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index 1e5171e24..8e098eed2 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -12,7 +12,7 @@ def __init__(self, sio: SocketIO) -> None: self.socket = sio def emit(self, record) -> None: - + try: entry = self.format(record) self.socket.emit("log", { @@ -33,8 +33,7 @@ def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: fgcs_logger.setLevel(logging.DEBUG if debug else logging.INFO) fgcs_logger.addHandler(SocketIOHandler(conn)) - # Stream handler on debug mode only - flask_logger = logging.getLogger("werkzeug") + flask_logger.setLevel(logging.WARNING) flask_logger.addHandler(SocketIOHandler(conn)) \ No newline at end of file From cbe4108916b94c96cfb39f17403756b0173017a9 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:46:10 +0100 Subject: [PATCH 46/69] Fixed combining files being the wrong way round --- gcs/electron/modules/logging.ts | 2 +- gcs/src/redux/middleware/socketMiddleware.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gcs/electron/modules/logging.ts b/gcs/electron/modules/logging.ts index de3eb8d01..918129336 100644 --- a/gcs/electron/modules/logging.ts +++ b/gcs/electron/modules/logging.ts @@ -39,7 +39,7 @@ export function setupLog4js(logToWorkspace: boolean, combineLogFiles: boolean, l logDebug("Setting up logging with args (%s, %s, %s, %s)", logToWorkspace, combineLogFiles, logFormat, loggingLevel) - const appenders = [combineLogFiles ? "multifile" : "file"] + const appenders = [combineLogFiles ? "file" : "multifile"] const directory = logToWorkspace ? path.join(app.getAppPath(), "logs") : app.getPath("logs") // If we are logging to separate files no point including the logger name diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index aa832713e..9c6bc2c8d 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -147,21 +147,21 @@ const socketMiddleware = (store) => { // In development mode, hook socket on and emit so that we can debug log every time we recieve / send a socket message if (process.env.NODE_ENV === "development") { - const originalOn = socket.socket.on.bind(socket.socket); + const originalOn = socket.socket.on.bind(socket.socket) socket.socket.on = (event, callback) => { const wrappedCallback = (...args) => { - logDebug(`Event "${event}" recieved by frontend with values ${args.join(", ")}`); + if (event != "log") logDebug(`Event "${event}" recieved by frontend with values (${args.map(a => typeof(a) == "object" ? JSON.stringify(a) : a).join(", ")})`); callback(...args); }; return originalOn(event, wrappedCallback); }; - const originalEmit = socket.socket.emit.bind(socket.socket); + const originalEmit = socket.socket.emit.bind(socket.socket) socket.socket.emit = (event, ...args) => { - logDebug(`Event "${event}" emitted by frontend with args (${args.join(", ")})`) + logDebug(`Event "${event}" emitted by frontend with args (${args.map(a => typeof(a) == "object" ? JSON.stringify(a) : a).join(", ")})`) return originalEmit(event, ...args) } } From 4e9f8668f9ca9ed032a31c518f3966b030e5a19d Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:48:50 +0100 Subject: [PATCH 47/69] Bad descriptor gone --- gcs/data/default_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index 0fb777e06..d10754b92 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -68,7 +68,7 @@ "default": false }, "loggingFormat": { - "description": "Timestamp format for log messages", + "description": "Format for log messages. See log4js patterns for more info", "display": "Log Format", "type": "string", "default": "[%d{dd/MM/yyyy hh:mm:ss:SSS}] [%p] %c - %m" From 27fed4bf54c810ee343fdd056ab12336e4d1915d Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:54:16 +0100 Subject: [PATCH 48/69] Explicitly import process to please eslint --- gcs/src/components/mainContent.jsx | 2 ++ gcs/src/components/settingsModal.jsx | 2 ++ gcs/src/redux/middleware/socketMiddleware.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx index 55b25159c..a951ecd45 100644 --- a/gcs/src/components/mainContent.jsx +++ b/gcs/src/components/mainContent.jsx @@ -13,6 +13,8 @@ import { Commands } from "./spotlight/commandHandler" import SingleRunWrapper from "./SingleRunWrapper" import { SettingsProvider } from "../helpers/settingsProvider" +import process from "process"; + // Routes import FLA from "../fla" import Graphs from "../graphs" diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index f16ab411f..373900765 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -13,6 +13,8 @@ import { IconTrash } from "@tabler/icons-react" import { memo, useEffect, useState } from "react" import DefaultSettings from "../../data/default_settings.json" +import process from "process"; + const isValidNumber = (num, range) => { return ( num && diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 9c6bc2c8d..cc3556881 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -59,6 +59,8 @@ import { pushMessage } from "../slices/statusTextSlice.js" import { emitLog } from "../slices/loggingSlice.js" import { logDebug, logError, logInfo } from "../../helpers/logging.js" +import process from "process"; + const SocketEvents = Object.freeze({ // socket.on events Connect: "connect", From 7b8d0a386d958033851b78fa5ef83bdc71a81444 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:55:49 +0100 Subject: [PATCH 49/69] Fix ruff issues --- radio/app/loggingConfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index 8e098eed2..d96687e72 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -1,5 +1,5 @@ -import sys import time +import socket import logging from flask_socketio import SocketIO @@ -22,7 +22,7 @@ def emit(self, record) -> None: "file": record.filename, "line": record.lineno }) - except: + except socket.error: self.handleError(record) From fdaf361712cc70f25ddbb8fd2c919eb93a8f4eb0 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 21:58:10 +0100 Subject: [PATCH 50/69] More ruff fixes + ran linter --- radio/app.py | 2 +- radio/app/controllers/armController.py | 9 +++--- .../app/controllers/flightModesController.py | 13 +++++--- radio/app/controllers/frameController.py | 1 + radio/app/controllers/gripperController.py | 5 +++- radio/app/controllers/motorTestController.py | 26 +++++++--------- radio/app/drone.py | 1 - radio/app/loggingConfig.py | 30 +++++++++---------- radio/app/utils.py | 9 ++++-- .../upload_mission_helper.py | 1 + 10 files changed, 52 insertions(+), 45 deletions(-) diff --git a/radio/app.py b/radio/app.py index b61749c97..a17bc00ce 100644 --- a/radio/app.py +++ b/radio/app.py @@ -21,7 +21,7 @@ logger.info(f"Starting backend at {host}:{port}.") socketio.run(app, allow_unsafe_werkzeug=True, host=host, port=port) - + if droneStatus.drone: droneStatus.drone.close() logger.info("Backend closed.") diff --git a/radio/app/controllers/armController.py b/radio/app/controllers/armController.py index 6889c2c71..1a6cfe2e2 100644 --- a/radio/app/controllers/armController.py +++ b/radio/app/controllers/armController.py @@ -13,9 +13,8 @@ logger = logging.getLogger("fgcs") + class ArmController: - - def __init__(self, drone: Drone) -> None: """ The Arm controller controls all arm/disarming operations. @@ -66,7 +65,7 @@ def arm(self, force: bool = False) -> Response: if self.drone.droneErrorCb: self.drone.droneErrorCb(str(e)) return {"success": False, "message": "Could not arm, serial exception"} - + logger.warning("Arm failed: unknown error") return {"success": False, "message": "Could not arm, command not accepted"} @@ -108,10 +107,10 @@ def disarm(self, force: bool = False) -> Response: except Exception as e: self.drone.is_listening = True logger.error(e, exc_info=True) - + if self.drone.droneErrorCb: self.drone.droneErrorCb(str(e)) - + return {"success": False, "message": "Could not disarm, serial exception"} logger.warning("Disarm failed: unknown error") diff --git a/radio/app/controllers/flightModesController.py b/radio/app/controllers/flightModesController.py index 26826dcef..4e3a3a3ed 100644 --- a/radio/app/controllers/flightModesController.py +++ b/radio/app/controllers/flightModesController.py @@ -24,6 +24,7 @@ logger = logging.getLogger("fgcs") + class FlightModesController: def __init__(self, drone: Drone) -> None: """ @@ -102,7 +103,9 @@ def setFlightMode(self, mode_number: int, flight_mode: int) -> Response: if self.drone.aircraft_type == 1: if (flight_mode < 0) or (flight_mode > 24): - logger.error(f"Invalid plane flight mode number, must be between 0 and 24 inclusive, got {flight_mode}") + logger.error( + f"Invalid plane flight mode number, must be between 0 and 24 inclusive, got {flight_mode}" + ) return { "success": False, "message": f"Invalid plane flight mode, must be between 0 and 24 inclusive, got {flight_mode}", @@ -110,7 +113,9 @@ def setFlightMode(self, mode_number: int, flight_mode: int) -> Response: mode_name = mavutil.mavlink.enums["PLANE_MODE"][flight_mode].name else: if (flight_mode < 0) or (flight_mode > 27): - logger.error(f"Invalid copter flight mode number, must be between 0 and 27 inclusive, got {flight_mode}") + logger.error( + f"Invalid copter flight mode number, must be between 0 and 27 inclusive, got {flight_mode}" + ) return { "success": False, "message": f"Invalid copter flight mode, must be between 0 and 27 inclusive, got {flight_mode}", @@ -143,9 +148,9 @@ def setCurrentFlightMode(self, flightMode: int) -> Response: """ self.drone.is_listening = False time.sleep(0.3) - + logger.info(f"Setting current flight mode to {flightMode}") - + self.drone.sendCommand( message=mavutil.mavlink.MAV_CMD_DO_SET_MODE, param1=1, diff --git a/radio/app/controllers/frameController.py b/radio/app/controllers/frameController.py index 1f1db1718..c68ab3005 100644 --- a/radio/app/controllers/frameController.py +++ b/radio/app/controllers/frameController.py @@ -9,6 +9,7 @@ logger = logging.getLogger("fgcs") + class FrameController: def __init__(self, drone: Drone) -> None: """The frame class controls all frame class and type related actions diff --git a/radio/app/controllers/gripperController.py b/radio/app/controllers/gripperController.py index 93cac71ff..735b8a96b 100644 --- a/radio/app/controllers/gripperController.py +++ b/radio/app/controllers/gripperController.py @@ -14,6 +14,7 @@ logger = logging.getLogger("fgcs") + class GripperController: def __init__(self, drone: Drone) -> None: """ @@ -130,7 +131,9 @@ def setGripper(self, action: str) -> Response: "message": f"Setting gripper to {action}", } else: - logger.error(f"Could not set gripper state to {action}, command not accepted") + logger.error( + f"Could not set gripper state to {action}, command not accepted" + ) return { "success": False, "message": "Setting gripper failed", diff --git a/radio/app/controllers/motorTestController.py b/radio/app/controllers/motorTestController.py index 0a724fa05..7abdd6a85 100644 --- a/radio/app/controllers/motorTestController.py +++ b/radio/app/controllers/motorTestController.py @@ -19,6 +19,7 @@ logger = logging.getLogger("fgcs") + class MotorTestController: def __init__(self, drone: Drone) -> None: """ @@ -44,16 +45,12 @@ def checkMotorTestValues( # self.drone.logger.info(f"Testing drone values: {data}") throttle = data.get("throttle", -1) if throttle is None or (not (0 <= throttle <= 100)): - logger.error( - f"Invalid value for motor test throttle, got {throttle}" - ) + logger.error(f"Invalid value for motor test throttle, got {throttle}") return 0, 0, "Invalid value for throttle" duration = data.get("duration", -1) if duration is None or duration < 0: - logger.error( - f"Invalid value for motor test duration, got {duration}" - ) + logger.error(f"Invalid value for motor test duration, got {duration}") return 0, 0, "Invalid value for duration" return throttle, duration, None @@ -77,9 +74,7 @@ def testOneMotor(self, data: MotorTestAllValues) -> Response: motor_instance = data.get("motorInstance", None) if motor_instance is None or motor_instance < 1: - logger.error( - f"Invalid value for motor instance, got {motor_instance}" - ) + logger.error(f"Invalid value for motor instance, got {motor_instance}") return {"success": False, "message": "Invalid value for motorInstance"} self.drone.sendCommand( @@ -140,9 +135,7 @@ def testMotorSequence(self, data: MotorTestThrottleDurationAndNumber) -> Respons num_motors = data.get("number_of_motors", None) if num_motors is None or num_motors < 1: - logger.error( - f"Invalid value for number of motors, got {num_motors}" - ) + logger.error(f"Invalid value for number of motors, got {num_motors}") return {"success": False, "message": "Invalid value for number_of_motors"} self.drone.sendCommand( @@ -161,7 +154,10 @@ def testMotorSequence(self, data: MotorTestThrottleDurationAndNumber) -> Respons self.drone.is_listening = True if commandAccepted(response, mavutil.mavlink.MAV_CMD_DO_MOTOR_TEST): - logger.info("Motor sequence test started for motors" + "->".join(range(num_motors))) + logger.info( + "Motor sequence test started for motors" + + "->".join(range(num_motors)) + ) return {"success": True, "message": "Motor sequence test started"} else: logger.error("Motor sequence test not started: command not accepted") @@ -197,9 +193,7 @@ def testAllMotors(self, data: MotorTestThrottleDurationAndNumber) -> Response: # Validate number of motors num_motors = data.get("number_of_motors", None) if num_motors is None or num_motors < 1: - logger.error( - f"Invalid value for number of motors, got {num_motors}" - ) + logger.error(f"Invalid value for number of motors, got {num_motors}") return {"success": False, "message": "Invalid value for number_of_motors"} # Send all commands diff --git a/radio/app/drone.py b/radio/app/drone.py index 2583e7ff0..69f7d6a04 100644 --- a/radio/app/drone.py +++ b/radio/app/drone.py @@ -1,7 +1,6 @@ import copy import os import time -import traceback from logging import Logger, getLogger from pathlib import Path from queue import Queue diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index d96687e72..3debe5d28 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -6,34 +6,34 @@ class SocketIOHandler(logging.Handler): - def __init__(self, sio: SocketIO) -> None: super().__init__() self.socket = sio - - def emit(self, record) -> None: + def emit(self, record) -> None: try: entry = self.format(record) - self.socket.emit("log", { - "level": record.levelname, - "message": entry, - "timestamp": time.time(), - "file": record.filename, - "line": record.lineno - }) + self.socket.emit( + "log", + { + "level": record.levelname, + "message": entry, + "timestamp": time.time(), + "file": record.filename, + "line": record.lineno, + }, + ) except socket.error: self.handleError(record) def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: - fgcs_logger = logging.getLogger("fgcs") - + fgcs_logger.setLevel(logging.DEBUG if debug else logging.INFO) fgcs_logger.addHandler(SocketIOHandler(conn)) - + flask_logger = logging.getLogger("werkzeug") - + flask_logger.setLevel(logging.WARNING) - flask_logger.addHandler(SocketIOHandler(conn)) \ No newline at end of file + flask_logger.addHandler(SocketIOHandler(conn)) diff --git a/radio/app/utils.py b/radio/app/utils.py index a799e49d9..a958cad0d 100644 --- a/radio/app/utils.py +++ b/radio/app/utils.py @@ -20,8 +20,13 @@ def getComPort() -> str: while True: ports = list(list_ports.comports()) - - logger.info("Available COM PORTS: \n" + "\n".join(f"\t[{i}]\t{port.name}: {port.description}" for i, port in enumerate(ports))) + logger.info( + "Available COM PORTS: \n" + + "\n".join( + f"\t[{i}]\t{port.name}: {port.description}" + for i, port in enumerate(ports) + ) + ) inp_port = input("Enter a port index to connect to: ") if not inp_port.isdigit(): diff --git a/radio/tests/mission_test_files/upload_mission_helper.py b/radio/tests/mission_test_files/upload_mission_helper.py index 3dad84d87..8c72c9fa7 100644 --- a/radio/tests/mission_test_files/upload_mission_helper.py +++ b/radio/tests/mission_test_files/upload_mission_helper.py @@ -2,6 +2,7 @@ from app import logger + def uploadMission(file_name, mission_type, master): with open(file_name, "r") as f: lines = f.readlines() From 1f5256283d421e79384f9bfcb8b0dc262741a1db Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:30:05 +0100 Subject: [PATCH 51/69] Fixed bad tests and ruff issues --- radio/app/controllers/motorTestController.py | 4 ++-- radio/app/loggingConfig.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/radio/app/controllers/motorTestController.py b/radio/app/controllers/motorTestController.py index 7abdd6a85..578ebeea3 100644 --- a/radio/app/controllers/motorTestController.py +++ b/radio/app/controllers/motorTestController.py @@ -155,8 +155,8 @@ def testMotorSequence(self, data: MotorTestThrottleDurationAndNumber) -> Respons if commandAccepted(response, mavutil.mavlink.MAV_CMD_DO_MOTOR_TEST): logger.info( - "Motor sequence test started for motors" - + "->".join(range(num_motors)) + "Motor sequence test started for motors " + + "->".join(str(i) for i in range(num_motors)) ) return {"success": True, "message": "Motor sequence test started"} else: diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index 3debe5d28..dbed8583f 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -1,3 +1,4 @@ +import os import time import socket import logging @@ -31,9 +32,14 @@ def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: fgcs_logger = logging.getLogger("fgcs") fgcs_logger.setLevel(logging.DEBUG if debug else logging.INFO) - fgcs_logger.addHandler(SocketIOHandler(conn)) flask_logger = logging.getLogger("werkzeug") flask_logger.setLevel(logging.WARNING) - flask_logger.addHandler(SocketIOHandler(conn)) + + # Our test suite is stupid and all of them just check the last recieved message instead of filtering + # for the message they were expecting so we can't do socket logging in test environment + + if os.environ.get("PYTEST_VERSION") is None: + fgcs_logger.addHandler(SocketIOHandler(conn)) + flask_logger.addHandler(SocketIOHandler(conn)) From 75f51c80b3105df35a0ef044d7271be4142cd345 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:38:19 +0100 Subject: [PATCH 52/69] Fix spelling mistake thanks copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- gcs/src/redux/middleware/socketMiddleware.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index cc3556881..be1ddeaf4 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -153,7 +153,7 @@ const socketMiddleware = (store) => { socket.socket.on = (event, callback) => { const wrappedCallback = (...args) => { - if (event != "log") logDebug(`Event "${event}" recieved by frontend with values (${args.map(a => typeof(a) == "object" ? JSON.stringify(a) : a).join(", ")})`); + if (event != "log") logDebug(`Event "${event}" received by frontend with values (${args.map(a => typeof(a) == "object" ? JSON.stringify(a) : a).join(", ")})`); callback(...args); }; From 9b0fe6de6fd6b9dcf039bfa6904c42cdf11bdffa Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:38:40 +0100 Subject: [PATCH 53/69] Stupid suggestion but I love copilot so sure Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- radio/app/controllers/motorTestController.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radio/app/controllers/motorTestController.py b/radio/app/controllers/motorTestController.py index 578ebeea3..344eb4a73 100644 --- a/radio/app/controllers/motorTestController.py +++ b/radio/app/controllers/motorTestController.py @@ -241,8 +241,8 @@ def testAllMotors(self, data: MotorTestThrottleDurationAndNumber) -> Response: "message": f"All motor test successfully started {successful_responses} / {num_motors} motors", } else: # pragma: no cover - # We should never reach this (since we should only ever have successful_responses <= num_motors) but theoretically could - # If there was a bug in pymavlink + # Defensive programming: This branch should not normally be reached (since successful_responses <= num_motors), + # but is included to handle unexpected cases, such as bugs in pymavlink or unforeseen input. logger.warning( f"All motor test potentially started, but received {successful_responses} responses with {num_motors} motors" ) From bf1002e4e6eb870357d57f49b0289a455b1681ca Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:40:33 +0100 Subject: [PATCH 54/69] I can't soekk received Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- radio/app/loggingConfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index dbed8583f..c1e3b3bff 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -37,7 +37,7 @@ def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: flask_logger.setLevel(logging.WARNING) - # Our test suite is stupid and all of them just check the last recieved message instead of filtering + # Our test suite is stupid and all of them just check the last received message instead of filtering # for the message they were expecting so we can't do socket logging in test environment if os.environ.get("PYTEST_VERSION") is None: From 3a978cb35b48e732ae494453e565749366bbed52 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:40:48 +0100 Subject: [PATCH 55/69] Update gcs/src/components/settingsModal.jsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- gcs/src/components/settingsModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index 373900765..39c2b042e 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -138,7 +138,7 @@ function DirectorySetting({settingName}) { return ( // -
{getSetting(settingName)}
+
{getSetting(settingName)}
) } From 07c4c2402c96419a4803b915cbd5ea77556763cc Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:43:36 +0100 Subject: [PATCH 56/69] Fix mypy issue and fix copilot slop suggestions --- gcs/src/redux/middleware/loggingMiddleware.js | 1 - gcs/src/redux/middleware/socketMiddleware.js | 2 +- radio/app/loggingConfig.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gcs/src/redux/middleware/loggingMiddleware.js b/gcs/src/redux/middleware/loggingMiddleware.js index cc5e5f193..d165622e7 100644 --- a/gcs/src/redux/middleware/loggingMiddleware.js +++ b/gcs/src/redux/middleware/loggingMiddleware.js @@ -2,7 +2,6 @@ const loggingMiddleware = store => next => action => { - console.log(action.type) const result = next(action); const state = store.getState(); diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index cc3556881..f09b85b2e 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -163,7 +163,7 @@ const socketMiddleware = (store) => { const originalEmit = socket.socket.emit.bind(socket.socket) socket.socket.emit = (event, ...args) => { - logDebug(`Event "${event}" emitted by frontend with args (${args.map(a => typeof(a) == "object" ? JSON.stringify(a) : a).join(", ")})`) + logDebug(`Event "${event}" emitted by frontend with args (${args.join(", ")})`) return originalEmit(event, ...args) } } diff --git a/radio/app/loggingConfig.py b/radio/app/loggingConfig.py index dbed8583f..7edcce062 100644 --- a/radio/app/loggingConfig.py +++ b/radio/app/loggingConfig.py @@ -28,7 +28,7 @@ def emit(self, record) -> None: self.handleError(record) -def setup_logging(conn: SocketIO, debug: bool = False) -> logging.Logger: +def setup_logging(conn: SocketIO, debug: bool = False) -> None: fgcs_logger = logging.getLogger("fgcs") fgcs_logger.setLevel(logging.DEBUG if debug else logging.INFO) From b093977abc7c349b7eb7759b4b08e70eb6ed1063 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 22:57:26 +0100 Subject: [PATCH 57/69] Fix missing prettier files for some reason --- gcs/.prettierignore | 2 +- gcs/.prettierrc | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 gcs/.prettierrc diff --git a/gcs/.prettierignore b/gcs/.prettierignore index dc0172620..9db4c1f0e 100644 --- a/gcs/.prettierignore +++ b/gcs/.prettierignore @@ -3,4 +3,4 @@ build coverage dist dist-electron -data +data \ No newline at end of file diff --git a/gcs/.prettierrc b/gcs/.prettierrc new file mode 100644 index 000000000..6e0789a98 --- /dev/null +++ b/gcs/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "all" +} \ No newline at end of file From 80594f845a58a3a5395e754aa2810907251354ef Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 26 Aug 2025 23:00:44 +0100 Subject: [PATCH 58/69] Holy yarn format --- gcs/src/components/config/motorTest.jsx | 3 +- .../components/dashboard/webcam/webcam.jsx | 5 +- gcs/src/components/mainContent.jsx | 8 +- gcs/src/components/navbar.jsx | 1 - gcs/src/components/settingsModal.jsx | 87 ++++++++++++------- .../components/spotlight/commandHandler.js | 6 +- gcs/src/helpers/logHandlers.js | 7 +- gcs/src/helpers/logging.js | 65 +++++++------- gcs/src/redux/middleware/loggingMiddleware.js | 28 +++--- gcs/src/redux/middleware/socketMiddleware.js | 25 +++--- gcs/src/redux/slices/loggingSlice.js | 49 +++++------ gcs/src/redux/store.js | 2 +- gcs/src/webcam.jsx | 12 +-- 13 files changed, 160 insertions(+), 138 deletions(-) diff --git a/gcs/src/components/config/motorTest.jsx b/gcs/src/components/config/motorTest.jsx index 66aa897dc..26604a448 100644 --- a/gcs/src/components/config/motorTest.jsx +++ b/gcs/src/components/config/motorTest.jsx @@ -172,7 +172,8 @@ export default function MotorTestPanel() { return (

{" "} - Motor number: {mappedMotorNumber}, {frameTypeDirection[idx]}{" "} + Motor number: {mappedMotorNumber},{" "} + {frameTypeDirection[idx]}{" "}

) })} diff --git a/gcs/src/components/dashboard/webcam/webcam.jsx b/gcs/src/components/dashboard/webcam/webcam.jsx index 005a05d0a..04b8377a0 100644 --- a/gcs/src/components/dashboard/webcam/webcam.jsx +++ b/gcs/src/components/dashboard/webcam/webcam.jsx @@ -5,8 +5,7 @@ import { IconX } from "@tabler/icons-react" import { useRef } from "react" export default function CameraWindow() { - - const searchParams = new URLSearchParams(window.location.search); + const searchParams = new URLSearchParams(window.location.search) const videoRef = useRef(null) const deviceId = searchParams.get("deviceId", null) @@ -42,4 +41,4 @@ export default function CameraWindow() { )} ) -} \ No newline at end of file +} diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx index a951ecd45..6b66fda09 100644 --- a/gcs/src/components/mainContent.jsx +++ b/gcs/src/components/mainContent.jsx @@ -13,7 +13,7 @@ import { Commands } from "./spotlight/commandHandler" import SingleRunWrapper from "./SingleRunWrapper" import { SettingsProvider } from "../helpers/settingsProvider" -import process from "process"; +import process from "process" // Routes import FLA from "../fla" @@ -34,18 +34,16 @@ import { registerHandler } from "../redux/slices/loggingSlice" import { consoleLogHandler, electronLogHandler } from "../helpers/logHandlers" export default function AppContent() { - // Setup sockets for redux const dispatch = useDispatch() useEffect(() => { - // Only add console log handler in dev mode if (process.env.NODE_ENV === "development") { dispatch(registerHandler(consoleLogHandler)) } - + dispatch(registerHandler(electronLogHandler)) - + dispatch(initSocket()) }, []) diff --git a/gcs/src/components/navbar.jsx b/gcs/src/components/navbar.jsx index 7b42a355e..d65fd174e 100644 --- a/gcs/src/components/navbar.jsx +++ b/gcs/src/components/navbar.jsx @@ -144,7 +144,6 @@ export default function Navbar() { AddCommand("disconnect_from_drone", disconnect) }, []) - const linkClassName = "text-md px-2 rounded-sm outline-none focus:text-falconred-400 hover:text-falconred-400 transition-colors delay-50" diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index 39c2b042e..5b6c7b2de 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -13,7 +13,7 @@ import { IconTrash } from "@tabler/icons-react" import { memo, useEffect, useState } from "react" import DefaultSettings from "../../data/default_settings.json" -import process from "process"; +import process from "process" const isValidNumber = (num, range) => { return ( @@ -128,17 +128,25 @@ function ExtendableNumberSetting({ settingName, range }) { ) } -function DirectorySetting({settingName}) { +function DirectorySetting({ settingName }) { const { getSetting, setSetting } = useSettings() const setDirectory = async () => { - const dirHandle = await window.ipcRenderer.selectDirectory(); - if (dirHandle !== null) setSetting(settingName, dirHandle); + const dirHandle = await window.ipcRenderer.selectDirectory() + if (dirHandle !== null) setSetting(settingName, dirHandle) } return ( // -
{getSetting(settingName)}
+
+ {getSetting(settingName)} + +
) } @@ -163,9 +171,13 @@ function Setting({ settingName, df }) { ) : df.type == "option" ? ( ) : df.type == "directory" ? ( - + ) : ( -