From e9c49a3a8ecbab884fa17d98cc72a2b405a8fe2d Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 12:09:13 +1300 Subject: [PATCH 1/7] Add beginnings of sound playback. Add fake port to test sounds. --- src/renderer/src/model/App.tsx | 50 +++++++++++++ .../model/AppDataManager/AppDataManager.ts | 67 +++++++++++------ .../AppDataManager/DataClasses/AppData.ts | 2 +- .../DataClasses/SettingsData.ts | 2 + .../DataClasses/SoundsSettingsData.ts | 7 ++ .../model/FakePorts/FakePortsController.tsx | 33 +++++++++ src/renderer/src/model/Settings/Settings.tsx | 5 ++ .../Settings/SoundsSettings/SoundsSettings.ts | 36 +++++++++ src/renderer/src/model/Util/SoundPlayer.ts | 74 +++++++++++++++++++ .../src/view/Settings/SettingsView.tsx | 21 ++++++ .../SoundsSettings/SoundsSettingsView.tsx | 56 ++++++++++++++ 11 files changed, 329 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/model/AppDataManager/DataClasses/SoundsSettingsData.ts create mode 100644 src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts create mode 100644 src/renderer/src/model/Util/SoundPlayer.ts create mode 100644 src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx diff --git a/src/renderer/src/model/App.tsx b/src/renderer/src/model/App.tsx index 4737b57f..1be29d63 100644 --- a/src/renderer/src/model/App.tsx +++ b/src/renderer/src/model/App.tsx @@ -24,6 +24,7 @@ import { AppDataManager } from './AppDataManager/AppDataManager'; import PerformanceMonitor from './Performance/PerformanceMonitor'; import PerformanceTester, { PerformanceTestSuiteResult } from './Performance/PerformanceTester'; import { ConnController } from './ConnController/ConnController'; +import { SoundPlayer } from './Util/SoundPlayer'; declare global { interface String { @@ -130,6 +131,16 @@ export class App { */ connController: ConnController; + /** + * Sound player for playing audio feedback based on received data. + */ + soundPlayer: SoundPlayer; + + /** + * Buffer for detecting pass/fail strings across data chunks. + */ + private soundDetectionBuffer: string = ''; + private readonly SOUND_DETECTION_BUFFER_MAX_LENGTH = 100; constructor(testing = false) { initLogging(); @@ -155,6 +166,8 @@ export class App { this.connController = new ConnController(this); + this.soundPlayer = new SoundPlayer(); + this.terminals = new Terminals(this); this.numBytesReceived = 0; @@ -208,6 +221,7 @@ export class App { this.connController.cleanup(); this.stopRateCalculation(); this.stopCpuMonitoring(); + this.soundPlayer.cleanup(); // Clean up auto-updater listeners if ((window as any).electronAPI?.updater) { @@ -578,6 +592,11 @@ export class App { this.logging.handleRxData(rxData); + // Check for pass/fail strings and play sounds if enabled + if (this.settings.soundsSettings.playSoundsOnPassFail) { + this.detectAndPlaySounds(rxData); + } + // End performance monitoring and record metrics const totalProcessingTime = this.performanceMonitor.endTiming('dataProcessing'); this.performanceMonitor.recordDataProcessing(rxData.length, totalProcessingTime); @@ -587,6 +606,37 @@ export class App { this.recordRxDataPoint(rxData.length); } + /** + * Detects "pass" and "fail" strings in received data and plays appropriate sounds. + * Uses a buffer to handle detection across data chunks. + * + * @param rxData The received data as a Uint8Array + */ + private detectAndPlaySounds(rxData: Uint8Array) { + // Convert received data to string (lowercase for case-insensitive matching) + const dataStr = new TextDecoder().decode(rxData).toLowerCase(); + + // Add new data to buffer + this.soundDetectionBuffer += dataStr; + + // Check for "pass" or "fail" in the buffer + if (this.soundDetectionBuffer.includes('pass')) { + this.soundPlayer.playDing(); + // Clear buffer after detection to avoid repeated sounds + this.soundDetectionBuffer = ''; + } else if (this.soundDetectionBuffer.includes('fail')) { + this.soundPlayer.playBuzzer(); + // Clear buffer after detection to avoid repeated sounds + this.soundDetectionBuffer = ''; + } + + // Keep buffer length manageable + if (this.soundDetectionBuffer.length > this.SOUND_DETECTION_BUFFER_MAX_LENGTH) { + // Keep only the last portion of the buffer to catch strings split across chunks + this.soundDetectionBuffer = this.soundDetectionBuffer.slice(-this.SOUND_DETECTION_BUFFER_MAX_LENGTH); + } + } + /** * Run performance tests to measure baseline performance and identify bottlenecks diff --git a/src/renderer/src/model/AppDataManager/AppDataManager.ts b/src/renderer/src/model/AppDataManager/AppDataManager.ts index 244a888e..7198176f 100644 --- a/src/renderer/src/model/AppDataManager/AppDataManager.ts +++ b/src/renderer/src/model/AppDataManager/AppDataManager.ts @@ -1,14 +1,15 @@ import { makeAutoObservable } from 'mobx'; +import { VariantType } from 'notistack'; +import { PortInfo } from '@serialport/bindings-interface'; import { ConnState, ConnectionType, PortSettings } from '../Settings/PortSettings/PortSettings'; import { App } from '../App'; -import { VariantType } from 'notistack'; import { AppData } from './DataClasses/AppData'; import { Profile } from './DataClasses/Profile'; import DisplaySettings, { TerminalHeightMode } from '../Settings/DisplaySettings/DisplaySettings'; import { TimestampFormat } from '../Settings/RxSettings/RxSettings'; import { DEFAULT_BACKGROUND_COLOR, DEFAULT_TX_COLOR, DEFAULT_RX_COLOR } from './DataClasses/DisplaySettingsData'; -import { PortInfo } from '@serialport/bindings-interface'; +import { log } from '@/model/Util/Log'; export class LastUsedSerialPort { path: string = ''; @@ -54,20 +55,20 @@ export class AppDataManager { * @returns */ onStorageEvent = (event: StorageEvent) => { - console.log('Caught storage event. event.key: ', event.key, ' event.newValue: ', event.newValue); + log.info('Caught storage event. event.key: ', event.key, ' event.newValue: ', event.newValue); if (event.key === APP_DATA_STORAGE_KEY) { - console.log('App data changed from another process. Checking if profiles changed...'); + log.info('App data changed from another process. Checking if profiles changed...'); // Check if the profiles changed const appDataAsJson = window.localStorage.getItem(APP_DATA_STORAGE_KEY); if (appDataAsJson === null) { - console.error('App data not found in local storage.'); + log.error('App data not found in local storage.'); return; } const appDataInStorage = JSON.parse(appDataAsJson); // Compare the JSON strings of the profiles to work out if they are different if (JSON.stringify(appDataInStorage.profiles) !== JSON.stringify(this.appData.profiles)) { - console.log('Profiles changed. Reloading profiles...'); + log.info('Profiles changed. Reloading profiles...'); // Reload just the profiles, we don't want to overwrite the current app config this.appData.profiles = appDataInStorage.profiles; } @@ -84,7 +85,7 @@ export class AppDataManager { let appData: AppData; if (appDataAsJson === null) { // No config key found in users store, create one! - console.log('App data not found in local storage. Creating default app data...'); + log.info('App data not found in local storage. Creating default app data...'); appData = new AppData(); // Save just-created config back to store. window.localStorage.setItem(APP_DATA_STORAGE_KEY, JSON.stringify(appData)); @@ -119,11 +120,11 @@ export class AppDataManager { // VERSION 1 -> VERSION 2 //============================================================================= if (updatedAppData.version === 1) { - console.log('Updating app data from version 1 to version 2...'); + log.info('Updating app data from version 1 to version 2...'); // Convert to v2 // Port settings got a new field, display settings got two new fields let upgradeRootConfig = (rootConfig: any) => { - console.log('Upgrading profile: ', rootConfig); + log.info('Upgrading profile: ', rootConfig); rootConfig.settings.portSettings.allowSettingsChangesWhenOpen = false; rootConfig.settings.displaySettings.terminalHeightMode = TerminalHeightMode.AUTO_HEIGHT; rootConfig.settings.displaySettings.terminalHeightChars = 25; @@ -140,7 +141,7 @@ export class AppDataManager { // VERSION 2 -> VERSION 3 //============================================================================= if (updatedAppData.version === 2) { - console.log('Updating app data from version 2 to version 3...'); + log.info('Updating app data from version 2 to version 3...'); let updateRootConfig = (rootConfig: any) => { // Add timestamp settings rootConfig.settings.rxSettings.addTimestamps = false; @@ -174,7 +175,7 @@ export class AppDataManager { // VERSION 3 -> VERSION 4 //============================================================================= if (updatedAppData.version === 3) { - console.log('Updating app data from version 3 to version 4...'); + log.info('Updating app data from version 3 to version 4...'); // Add auto-updates setting to app data (global setting, not per profile) updatedAppData.autoUpdatesEnabled = true; @@ -220,7 +221,7 @@ export class AppDataManager { // VERSION 4 -> VERSION 5 //============================================================================= if (updatedAppData.version === 4) { - console.log('Updating app data from version 4 to version 5...'); + log.info('Updating app data from version 4 to version 5...'); // Add detection mode to graphing settings for all profiles for (let i = 0; i < updatedAppData.profiles.length; i++) { const graphingSettings = updatedAppData.profiles[i].rootConfig.settings.graphingSettings; @@ -236,7 +237,7 @@ export class AppDataManager { // VERSION 5 -> VERSION 6 //============================================================================= if (updatedAppData.version === 5) { - console.log('Updating app data from version 5 to version 6...'); + log.info('Updating app data from version 5 to version 6...'); // Rename bufferDelimiter to processingTrigger in graphing settings for all profiles for (let i = 0; i < updatedAppData.profiles.length; i++) { const graphingSettings = updatedAppData.profiles[i].rootConfig.settings.graphingSettings; @@ -259,7 +260,7 @@ export class AppDataManager { // VERSION 6 -> VERSION 7 //============================================================================= if (updatedAppData.version === 6) { - console.log('Updating app data from version 6 to version 7...'); + log.info('Updating app data from version 6 to version 7...'); // Add flow control settings to app data let updateProfileConfig = (rootConfig: any) => { rootConfig.terminal.rightDrawer.flowControlIsExpanded = true; @@ -276,7 +277,7 @@ export class AppDataManager { // VERSION 7 -> VERSION 8 //============================================================================= if (updatedAppData.version === 7) { - console.log('Updating app data from version 7 to version 8...'); + log.info('Updating app data from version 7 to version 8...'); // Add new flow control parameters and remove old flowControl property let updateProfileConfig = (rootConfig: any) => { // Remove the old flowControl property @@ -313,7 +314,7 @@ export class AppDataManager { // VERSION 8 -> VERSION 9 //============================================================================= if (updatedAppData.version === 8) { - console.log('Updating app data from version 8 to version 9...'); + log.info('Updating app data from version 8 to version 9...'); // Create new logSettings structure and move any existing log directory let updateProfileConfig = (rootConfig: any) => { // Create the new logSettings object with defaults @@ -341,7 +342,7 @@ export class AppDataManager { // VERSION 9 -> VERSION 10 //============================================================================= if (updatedAppData.version === 9) { - console.log('Updating app data from version 9 to version 10...'); + log.info('Updating app data from version 9 to version 10...'); // Add socket connection settings to port configuration let updateProfileConfig = (rootConfig: any) => { rootConfig.settings.portSettings.connectionType = ConnectionType.SERIAL_PORT; @@ -361,7 +362,7 @@ export class AppDataManager { // VERSION 10 -> VERSION 11 //============================================================================= if (updatedAppData.version === 10) { - console.log('Updating app data from version 10 to version 11...'); + log.info('Updating app data from version 10 to version 11...'); // Add tooltip settings to display settings for all profiles let updateProfileConfig = (rootConfig: any) => { rootConfig.settings.displaySettings.tooltipsEnabled = DisplaySettings.DEFAULT_TOOLTIPS_ENABLED; @@ -375,13 +376,33 @@ export class AppDataManager { wasChanged = true; } - if (updatedAppData.version !== 11) { - console.error('Unknown app data version found: ', appData.version); + //============================================================================= + // VERSION 11 -> VERSION 12 + //============================================================================= + // Sound settings were added for the first time in this version. + if (updatedAppData.version === 11) { + log.info('Updating app data from version 11 to version 12...'); + // Add sounds settings to settings for all profiles + let updateProfileConfig = (rootConfig: any) => { + rootConfig.settings.soundsSettings = { + playSoundsOnPassFail: false + }; + } + for (let i = 0; i < updatedAppData.profiles.length; i++) { + updateProfileConfig(updatedAppData.profiles[i].rootConfig); + } + updateProfileConfig(updatedAppData.currentAppConfig); + updatedAppData.version = 12; + wasChanged = true; + } + + if (updatedAppData.version !== 12) { + log.error('Unknown app data version found: ', appData.version); updatedAppData = new AppData(); wasChanged = true; } - console.log('Updated app data to latest version.'); + log.info('Updated app data to latest version.'); return { appData: updatedAppData, wasChanged }; } @@ -454,7 +475,7 @@ export class AppDataManager { snackbarMessage += '\nAlready connected port matches one specified in profile. Leaving port connected.'; } else { // They are both different and the profile one is non-empty. Check to see if the profile ports is available - console.log('Port infos are both different and non-empty. Checking if ports are available...'); + log.info('Port infos are both different and non-empty. Checking if ports are available...'); // const availablePorts = await navigator.serial.getPorts(); const availablePortsResult = await window.electronAPI.serial.listPorts(); if (!availablePortsResult.success) { @@ -516,7 +537,7 @@ export class AppDataManager { * @param profileIdx The index of the profile to save the current app config to. */ saveCurrentAppConfigToProfile = (profileIdx: number, noSnackbar = false) => { - console.log('Saving current app config to profile...'); + log.info('Saving current app config to profile...'); const profile = this.appData.profiles[profileIdx]; profile.rootConfig = JSON.parse(JSON.stringify(this.appData.currentAppConfig)); this.saveAppData(); diff --git a/src/renderer/src/model/AppDataManager/DataClasses/AppData.ts b/src/renderer/src/model/AppDataManager/DataClasses/AppData.ts index 0dfc41bc..da6236eb 100644 --- a/src/renderer/src/model/AppDataManager/DataClasses/AppData.ts +++ b/src/renderer/src/model/AppDataManager/DataClasses/AppData.ts @@ -3,7 +3,7 @@ import { makeAutoObservable } from "mobx"; import { Profile } from "./Profile"; import { ProfileConfig } from "./ProfileConfig"; -export const LATEST_VERSION = 11; +export const LATEST_VERSION = 12; export class AppData { // Version of the AppData class. diff --git a/src/renderer/src/model/AppDataManager/DataClasses/SettingsData.ts b/src/renderer/src/model/AppDataManager/DataClasses/SettingsData.ts index cf820bcd..d8728947 100644 --- a/src/renderer/src/model/AppDataManager/DataClasses/SettingsData.ts +++ b/src/renderer/src/model/AppDataManager/DataClasses/SettingsData.ts @@ -5,6 +5,7 @@ import { TxSettingsData } from './TxSettingsData'; import { GeneralSettingsConfig } from './GeneralSettingsData'; import { GraphingSettingsData } from './GraphingSettingsData'; import { LogSettingsData } from './LogSettingsData'; +import { SoundsSettingsData } from './SoundsSettingsData'; /** * Encapsulates all application settings data. @@ -18,4 +19,5 @@ export class SettingsData { generalSettings = new GeneralSettingsConfig(); graphingSettings = new GraphingSettingsData(); logSettings = new LogSettingsData(); + soundsSettings = new SoundsSettingsData(); } diff --git a/src/renderer/src/model/AppDataManager/DataClasses/SoundsSettingsData.ts b/src/renderer/src/model/AppDataManager/DataClasses/SoundsSettingsData.ts new file mode 100644 index 00000000..5244e654 --- /dev/null +++ b/src/renderer/src/model/AppDataManager/DataClasses/SoundsSettingsData.ts @@ -0,0 +1,7 @@ +/** + * Encapsulates sounds settings data. + * Everything in this class must be POD (plain old data) and serializable to JSON. + */ +export class SoundsSettingsData { + playSoundsOnPassFail = false; +} diff --git a/src/renderer/src/model/FakePorts/FakePortsController.tsx b/src/renderer/src/model/FakePorts/FakePortsController.tsx index 2c17066b..ea500b7e 100644 --- a/src/renderer/src/model/FakePorts/FakePortsController.tsx +++ b/src/renderer/src/model/FakePorts/FakePortsController.tsx @@ -202,6 +202,39 @@ export default class FakePortsController { ); //================================================================================= + // pass/fail alternating, 0.2items/s (for testing sound functionality) + //================================================================================= + this.fakePorts.push( + new FakePort( + 'pass/fail alternating, 0.2items/s', + 'Alternates between "pass" and "fail" every 5 seconds. Useful for testing sound notifications.', + () => { + let stringIdx = 0; + const strings = ['pass\n', 'fail\n']; + const intervalId = setInterval(() => { + const textToSend = strings[stringIdx]; + let bytesToSend = []; + for (let i = 0; i < textToSend.length; i++) { + bytesToSend.push(textToSend.charCodeAt(i)); + } + app.parseRxData(Uint8Array.from(bytesToSend)); + + stringIdx += 1; + if (stringIdx === strings.length) { + stringIdx = 0; + } + }, 5000); + return intervalId; + }, + (intervalId: NodeJS.Timeout | null) => { + // Stop the interval + if (intervalId !== null) { + clearInterval(intervalId); + } + } + ) + ); + // red green, 0.2lps //================================================================================= this.fakePorts.push( diff --git a/src/renderer/src/model/Settings/Settings.tsx b/src/renderer/src/model/Settings/Settings.tsx index b6f1835a..60453450 100644 --- a/src/renderer/src/model/Settings/Settings.tsx +++ b/src/renderer/src/model/Settings/Settings.tsx @@ -9,6 +9,7 @@ import DisplaySettings from './DisplaySettings/DisplaySettings'; import { PortSettings } from './PortSettings/PortSettings'; import GeneralSettings from './GeneralSettings/GeneralSettings'; import ProfilesSettings from './ProfileSettings/ProfileSettings'; +import SoundsSettings from './SoundsSettings/SoundsSettings'; import { App } from '../App'; export enum SettingsCategories { @@ -18,6 +19,7 @@ export enum SettingsCategories { DISPLAY, GENERAL, PROFILES, + SOUNDS, } export class Settings { @@ -39,6 +41,8 @@ export class Settings { profilesSettings: ProfilesSettings; + soundsSettings: SoundsSettings; + /** * Constructor for the Settings class. * @@ -54,6 +58,7 @@ export class Settings { this.displaySettings = new DisplaySettings(this.app.profileManager); this.generalSettings = new GeneralSettings(this.app.profileManager); this.profilesSettings = new ProfilesSettings(this.app.profileManager); + this.soundsSettings = new SoundsSettings(this.app.profileManager); makeAutoObservable(this); // Make sure this is at the end of the constructor } diff --git a/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts b/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts new file mode 100644 index 00000000..22046506 --- /dev/null +++ b/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts @@ -0,0 +1,36 @@ +import { makeAutoObservable } from "mobx"; +import { AppDataManager } from "src/model/AppDataManager/AppDataManager"; + +export default class SoundsSettings { + profileManager: AppDataManager; + + playSoundsOnPassFail = false; + + constructor(profileManager: AppDataManager) { + this.profileManager = profileManager; + this._loadConfig(); + this.profileManager.registerOnProfileLoad(() => { + this._loadConfig(); + }); + makeAutoObservable(this); // Make sure this is at the end of the constructor + } + + setPlaySoundsOnPassFail = (value: boolean) => { + this.playSoundsOnPassFail = value; + this._saveConfig(); + }; + + _saveConfig = () => { + let config = this.profileManager.appData.currentAppConfig.settings.soundsSettings; + + config.playSoundsOnPassFail = this.playSoundsOnPassFail; + + this.profileManager.saveAppData(); + }; + + _loadConfig = () => { + let configToLoad = this.profileManager.appData.currentAppConfig.settings.soundsSettings; + + this.playSoundsOnPassFail = configToLoad.playSoundsOnPassFail; + }; +} diff --git a/src/renderer/src/model/Util/SoundPlayer.ts b/src/renderer/src/model/Util/SoundPlayer.ts new file mode 100644 index 00000000..70ecea06 --- /dev/null +++ b/src/renderer/src/model/Util/SoundPlayer.ts @@ -0,0 +1,74 @@ +/** + * Utility class for playing simple sounds in the application. + * + * Uses the Web Audio API to generate simple tones for success/failure feedback. + */ +export class SoundPlayer { + private audioContext: AudioContext | null = null; + + constructor() { + // Lazy initialization of AudioContext to avoid autoplay policy issues + } + + private getAudioContext(): AudioContext { + if (!this.audioContext) { + this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + return this.audioContext; + } + + /** + * Plays a success "ding" sound (pleasant high-pitched tone). + */ + playDing() { + const audioContext = this.getAudioContext(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Pleasant "ding" sound - two tones in sequence + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); + oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1); + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + } + + /** + * Plays a failure "buzzer" sound (lower pitched, less pleasant tone). + */ + playBuzzer() { + const audioContext = this.getAudioContext(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Buzzer sound - lower frequency, harsher tone + oscillator.type = 'sawtooth'; + oscillator.frequency.setValueAtTime(200, audioContext.currentTime); + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.4); + } + + /** + * Clean up audio context resources. + */ + cleanup() { + if (this.audioContext) { + this.audioContext.close(); + this.audioContext = null; + } + } +} diff --git a/src/renderer/src/view/Settings/SettingsView.tsx b/src/renderer/src/view/Settings/SettingsView.tsx index fe98c6c8..36917390 100644 --- a/src/renderer/src/view/Settings/SettingsView.tsx +++ b/src/renderer/src/view/Settings/SettingsView.tsx @@ -14,6 +14,7 @@ import DisplaySettingsView from './DisplaySettings/DisplaySettingsView'; import TxSettingsView from './TxSettings/TxSettingsView'; import GeneralSettingsView from './GeneralSettings/GeneralSettingsView'; import ProfileSettingsView from './ProfileSettings/ProfileSettingsView'; +import SoundsSettingsView from './SoundsSettings/SoundsSettingsView'; interface Props { app: App; @@ -41,6 +42,9 @@ function SettingsDialog(props: Props) { [SettingsCategories.PROFILES]: ( ), + [SettingsCategories.SOUNDS]: ( + + ), }; return ( @@ -158,6 +162,23 @@ function SettingsDialog(props: Props) { > Profiles + {/* ================================================ */} + {/* SOUNDS */} + {/* ================================================ */} + { + app.settings.setActiveSettingsCategory( + SettingsCategories.SOUNDS + ); + }} + selected={ + app.settings.activeSettingsCategory === + SettingsCategories.SOUNDS + } + data-testid="sounds-settings-button" + > + Sounds + diff --git a/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx b/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx new file mode 100644 index 00000000..284fc477 --- /dev/null +++ b/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx @@ -0,0 +1,56 @@ +import { Checkbox, FormControlLabel, Tooltip } from "@mui/material"; +import { observer } from "mobx-react-lite"; +import React from "react"; +import SoundsSettings from "src/model/Settings/SoundsSettings/SoundsSettings"; +import { App } from "src/model/App"; + +import BorderedSection from "src/view/Components/BorderedSection"; + +interface Props { + soundsSettings: SoundsSettings; + app: App; +} + +function SoundsSettingsView(props: Props) { + const { soundsSettings, app } = props; + + return ( +
+ {/* =============================================================================== */} + {/* SOUND NOTIFICATIONS */} + {/* =============================================================================== */} + +
+ + { + soundsSettings.setPlaySoundsOnPassFail(e.target.checked); + }} + data-testid="play-sounds-on-pass-fail" + /> + } + label='Play ding sound on "pass" and incorrect buzzer sound on "fail"' + sx={{ marginBottom: "10px" }} + /> + +
+
+
+ ); +} + +export default observer(SoundsSettingsView); From 0bc450c9008256cd17d2f9f886ae41224c173b62 Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 14:39:56 +1300 Subject: [PATCH 2/7] Sounds now being found correctly in packaged app. --- .gitattributes | 1 + README.md | 8 + electron.vite.config.ts | 3 + package.json | 2 +- public/assets/sounds/README.md | 8 + public/assets/sounds/fail.mp3 | Bin 0 -> 22821 bytes public/assets/sounds/pass.mp3 | Bin 0 -> 16541 bytes .../model/FakePorts/FakePortsController.tsx | 14 +- src/renderer/src/model/Util/SoundPlayer.ts | 148 +++++++++++++----- 9 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 public/assets/sounds/README.md create mode 100644 public/assets/sounds/fail.mp3 create mode 100644 public/assets/sounds/pass.mp3 diff --git a/.gitattributes b/.gitattributes index c03bfb9f..77dd00f4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,4 @@ *.gif binary *.fcp binary *.webm binary +*.mp3 binary diff --git a/README.md b/README.md index c1bf6dad..d033f5d4 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,14 @@ npm run dev npm run build ``` +## To Build A Single Executable + +```bash +npm run dist +``` + +This will be generated in the `dist` directory. + ## Testing Both unit tests and end-to-end tests can be run with: diff --git a/electron.vite.config.ts b/electron.vite.config.ts index db5beea7..dcbdd378 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -24,6 +24,9 @@ export default defineConfig({ }, renderer: { root: 'src/renderer', + // publicDir is where assets are served from that are accessible to the renderer process + // One use is for sounds, + publicDir: resolve(__dirname, 'public'), optimizeDeps: { include: [ '@emotion/react', diff --git a/package.json b/package.json index 713562c5..7d59dd1a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ninjaterm", "productName": "NinjaTerm", - "version": "5.7.1", + "version": "5.8.0", "description": "A modern, powerful serial terminal for developers and engineers.", "author": { "name": "Geoffrey Hunter", diff --git a/public/assets/sounds/README.md b/public/assets/sounds/README.md new file mode 100644 index 00000000..33c6661d --- /dev/null +++ b/public/assets/sounds/README.md @@ -0,0 +1,8 @@ +# Sound Files + +Place your sound files in this directory: + +- **pass.mp3** - Played when "pass" is detected in received data +- **fail.mp3** - Played when "fail" is detected in received data + +These files are used by the Sounds settings feature in NinjaTerm. diff --git a/public/assets/sounds/fail.mp3 b/public/assets/sounds/fail.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c4f75a96c63713e63b2c756e9df2d08c7314d982 GIT binary patch literal 22821 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 z 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 PQwsR69q0cO*Yke>IU%1N literal 0 HcmV?d00001 diff --git a/src/renderer/src/model/FakePorts/FakePortsController.tsx b/src/renderer/src/model/FakePorts/FakePortsController.tsx index ea500b7e..203261e6 100644 --- a/src/renderer/src/model/FakePorts/FakePortsController.tsx +++ b/src/renderer/src/model/FakePorts/FakePortsController.tsx @@ -46,6 +46,7 @@ export default class FakePortsController { constructor(app: App) { this.app = app; + //================================================================================= // hello world, 0.1lps //================================================================================= this.fakePorts.push( @@ -72,6 +73,7 @@ export default class FakePortsController { ) ); + //================================================================================= // hello world, 1lps //================================================================================= this.fakePorts.push( @@ -98,6 +100,7 @@ export default class FakePortsController { ) ); + //================================================================================= // hello world, 5lps //================================================================================= this.fakePorts.push( @@ -124,6 +127,7 @@ export default class FakePortsController { ) ); + //================================================================================= // hello world, 10lps //================================================================================= this.fakePorts.push( @@ -150,6 +154,7 @@ export default class FakePortsController { ) ); + //================================================================================= // hello world, 20lps //================================================================================= this.fakePorts.push( @@ -178,6 +183,7 @@ export default class FakePortsController { ) ); + //================================================================================= // 50 numbered lines all at once //================================================================================= this.fakePorts.push( @@ -206,8 +212,8 @@ export default class FakePortsController { //================================================================================= this.fakePorts.push( new FakePort( - 'pass/fail alternating, 0.2items/s', - 'Alternates between "pass" and "fail" every 5 seconds. Useful for testing sound notifications.', + 'pass/fail alternating, 0.5items/s', + 'Alternates between "pass" and "fail" every 2 seconds. Useful for testing sound notifications.', () => { let stringIdx = 0; const strings = ['pass\n', 'fail\n']; @@ -223,7 +229,7 @@ export default class FakePortsController { if (stringIdx === strings.length) { stringIdx = 0; } - }, 5000); + }, 2000); return intervalId; }, (intervalId: NodeJS.Timeout | null) => { @@ -235,6 +241,7 @@ export default class FakePortsController { ) ); + //================================================================================= // red green, 0.2lps //================================================================================= this.fakePorts.push( @@ -268,6 +275,7 @@ export default class FakePortsController { ) ); + //================================================================================= // all colors, 5cps //================================================================================= this.fakePorts.push( diff --git a/src/renderer/src/model/Util/SoundPlayer.ts b/src/renderer/src/model/Util/SoundPlayer.ts index 70ecea06..55e6b3a3 100644 --- a/src/renderer/src/model/Util/SoundPlayer.ts +++ b/src/renderer/src/model/Util/SoundPlayer.ts @@ -1,74 +1,140 @@ +import { log } from '@/model/Util/Log'; + /** - * Utility class for playing simple sounds in the application. + * Utility class for playing sound files in the application. * - * Uses the Web Audio API to generate simple tones for success/failure feedback. + * Uses the HTML5 Audio API to play MP3 files for success/failure feedback. */ export class SoundPlayer { - private audioContext: AudioContext | null = null; + private passAudio: HTMLAudioElement | null = null; + private failAudio: HTMLAudioElement | null = null; constructor() { - // Lazy initialization of AudioContext to avoid autoplay policy issues + // Preload audio files for better performance + this.loadAudioFiles(); } - private getAudioContext(): AudioContext { - if (!this.audioContext) { - this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + /** + * Preloads the audio files so they're ready to play. + * Audio files should be placed in public/assets/sounds/ + */ + private loadAudioFiles() { + try { + // Try to construct the proper path for Electron + // In development, files are served from public/ + // In production, they should be in the resources + const basePath = this.getBasePath(); + + log.info('SoundPlayer: Loading audio files from base path:', basePath); + + this.passAudio = new Audio(`${basePath}/assets/sounds/pass.mp3`); + this.passAudio.preload = 'auto'; + this.passAudio.volume = 0.1; // Set to 100% volume + + this.failAudio = new Audio(`${basePath}/assets/sounds/fail.mp3`); + this.failAudio.preload = 'auto'; + this.failAudio.volume = 1.0; // Set to 70% volume + + // Handle loading errors gracefully + this.passAudio.addEventListener('error', (e) => { + log.error('Failed to load pass.mp3 sound file. Path tried:', `${basePath}/assets/sounds/pass.mp3`, 'Error:', e); + }); + + this.failAudio.addEventListener('error', (e) => { + log.error('Failed to load fail.mp3 sound file. Path tried:', `${basePath}/assets/sounds/fail.mp3`, 'Error:', e); + }); + + // Add load success handlers for debugging + this.passAudio.addEventListener('canplaythrough', () => { + log.info('SoundPlayer: pass.mp3 loaded successfully'); + }); + + this.failAudio.addEventListener('canplaythrough', () => { + log.info('SoundPlayer: fail.mp3 loaded successfully'); + }); + } catch (error) { + log.error('Error initializing sound player:', error); } - return this.audioContext; } /** - * Plays a success "ding" sound (pleasant high-pitched tone). + * Gets the base path for loading assets. + * In Electron, this handles both development and production paths. */ - playDing() { - const audioContext = this.getAudioContext(); - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); + private getBasePath(): string { + // Use relative path (./assets) instead of absolute path (/assets) + // This works in both dev and production for Electron + // In dev: served from public/ folder by Vite + // In production: bundled into out/renderer/ folder + return '.'; + } - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); + /** + * Plays the "pass" sound (pass.mp3). + */ + playDing() { + if (!this.passAudio) { + log.warn('Pass audio not loaded'); + return; + } - // Pleasant "ding" sound - two tones in sequence - oscillator.type = 'sine'; - oscillator.frequency.setValueAtTime(800, audioContext.currentTime); - oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1); + try { + // Clone the audio element if we need to play multiple sounds simultaneously + const audio = this.passAudio.cloneNode() as HTMLAudioElement; - gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + // Play and handle potential autoplay restrictions + const playPromise = audio.play(); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.3); + if (playPromise !== undefined) { + playPromise.catch((error) => { + log.warn('Failed to play pass sound:', error); + }); + } + } catch (error) { + log.error('Error playing pass sound:', error); + } } /** - * Plays a failure "buzzer" sound (lower pitched, less pleasant tone). + * Plays the "fail" sound (fail.mp3). */ playBuzzer() { - const audioContext = this.getAudioContext(); - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); + if (!this.failAudio) { + log.warn('Fail audio not loaded'); + return; + } - // Buzzer sound - lower frequency, harsher tone - oscillator.type = 'sawtooth'; - oscillator.frequency.setValueAtTime(200, audioContext.currentTime); + try { + // Clone the audio element if we need to play multiple sounds simultaneously + const audio = this.failAudio.cloneNode() as HTMLAudioElement; - gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4); + // Play and handle potential autoplay restrictions + const playPromise = audio.play(); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.4); + if (playPromise !== undefined) { + playPromise.catch((error) => { + log.warn('Failed to play fail sound:', error); + }); + } + } catch (error) { + log.error('Error playing fail sound:', error); + } } /** - * Clean up audio context resources. + * Clean up audio resources. */ cleanup() { - if (this.audioContext) { - this.audioContext.close(); - this.audioContext = null; + if (this.passAudio) { + this.passAudio.pause(); + this.passAudio.src = ''; + this.passAudio = null; + } + + if (this.failAudio) { + this.failAudio.pause(); + this.failAudio.src = ''; + this.failAudio = null; } } } From ec5a1ef9dc150d7537a83a0572be8899f5475ff9 Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 15:23:30 +1300 Subject: [PATCH 3/7] Add buttons for user to test sounds work. --- .github/workflows/build-and-test.yml | 5 +- README.md | 2 +- public/assets/sounds/fail.mp3 | Bin 22821 -> 21101 bytes public/assets/sounds/pass.mp3 | Bin 16541 -> 17867 bytes src/main/index.ts | 15 ++++- src/renderer/index.html | 2 +- src/renderer/src/model/App.tsx | 53 +++++++++++++----- .../Settings/SoundsSettings/SoundsSettings.ts | 1 + .../src/view/Settings/SettingsView.tsx | 2 +- .../SoundsSettings/SoundsSettingsView.tsx | 37 ++++++++---- 10 files changed, 86 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index de8763ea..d104859d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,8 +3,9 @@ name: Build and Test on: push: branches: [ main, dev ] - tags: - - 'v*' + # Don't need to run on tags anymore, we always push artifacts to a draft release now. + # tags: + # - 'v*' # pull_request: # branches: [ main, dev ] diff --git a/README.md b/README.md index d033f5d4..a4bd82dc 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Arduino sketches in `arduino-serial` allow you to program different applications 1. If you have updated the app data structure, save a copy of the default app data created by the app to `local-storage-data/`. You can do this by running the app, clearing app data in `Settings > General Settings`, loading up the Chrome dev. tools, and copying the key `appData` from local storage. 1. Create pull request on GitHub merging your branch into `main`. 1. Once the build on `main` has been successfully run, merge your branch into `main` via the merge request. -1. Tag the branch on main with the version number, e.g. `v4.1.0`. +1. Tag the branch on main with the version number, e.g. `v4.1.0`. Wait for the GitHub build and publish action spawned from the merge into main to complete (so that the artifacts from the build are used in the release in the next step). 1. Find the draft release on GitHub and publish it. Enter the CHANGELOG contents into the release body text. The app is built by GitHub Actions on every commit. If the build is successful and there is not already a non-draft release for this version number, the build artifacts will be uploaded to the release (files in existing draft releases are overwritten). diff --git a/public/assets/sounds/fail.mp3 b/public/assets/sounds/fail.mp3 index c4f75a96c63713e63b2c756e9df2d08c7314d982..516704c94934c320f34e4613084b3f40b9dfb9ac 100644 GIT binary patch literal 21101 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 z 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 PQwsR69q0cO*Yke>IU%1N diff --git a/src/main/index.ts b/src/main/index.ts index d278d888..a09e1627 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, dialog, Menu, shell } from 'electron'; +import { app, BrowserWindow, ipcMain, dialog, Menu, shell, session } from 'electron'; import pkg from 'electron-updater'; const { autoUpdater } = pkg; import * as path from 'path'; @@ -165,6 +165,19 @@ app.whenReady().then(async () => { log.info('Main process started.'); + // Uncomment this if we want to retry applying CSP correctly + // Apply Content Security Policy (CSP) + // session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + // callback({ + // responseHeaders: { + // ...details.responseHeaders, + // 'Content-Security-Policy': [ + // "default-src 'self'; script-src 'self' 'unsafe-inline' https://accounts.google.com https://*.gstatic.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://*.gstatic.com; img-src 'self' data: https://*.gstatic.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://accounts.google.com https://www.googleapis.com; frame-src https://accounts.google.com;" + // ], + // }, + // }); + // }); + createWindow(); // Initialize serial handlers diff --git a/src/renderer/index.html b/src/renderer/index.html index 94bf4e87..279fa699 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -10,4 +10,4 @@
- \ No newline at end of file + diff --git a/src/renderer/src/model/App.tsx b/src/renderer/src/model/App.tsx index 1be29d63..f5ec01fd 100644 --- a/src/renderer/src/model/App.tsx +++ b/src/renderer/src/model/App.tsx @@ -569,12 +569,9 @@ export class App { } /** - * In normal operation this is called from the readUntilClose() function above. + * This is called from whatever connection type is currently being used. All data should be funnelled through this function no matter what the connection type is. * - * Unit tests call this instead of mocking out the serial port read() function - * as setting up the deferred promise was too tricky. - * - * @param rxData + * @param rxData The received data. */ parseRxData(rxData: Uint8Array) { // Start performance monitoring for data processing @@ -609,6 +606,7 @@ export class App { /** * Detects "pass" and "fail" strings in received data and plays appropriate sounds. * Uses a buffer to handle detection across data chunks. + * Finds all occurrences of both patterns and plays them in order. * * @param rxData The received data as a Uint8Array */ @@ -619,15 +617,42 @@ export class App { // Add new data to buffer this.soundDetectionBuffer += dataStr; - // Check for "pass" or "fail" in the buffer - if (this.soundDetectionBuffer.includes('pass')) { - this.soundPlayer.playDing(); - // Clear buffer after detection to avoid repeated sounds - this.soundDetectionBuffer = ''; - } else if (this.soundDetectionBuffer.includes('fail')) { - this.soundPlayer.playBuzzer(); - // Clear buffer after detection to avoid repeated sounds - this.soundDetectionBuffer = ''; + // Find all occurrences of both patterns + const foundPatterns: Array<{index: number, type: 'pass' | 'fail', length: number}> = []; + + // Find all "pass" occurrences + let searchIndex = 0; + while ((searchIndex = this.soundDetectionBuffer.indexOf('pass', searchIndex)) !== -1) { + foundPatterns.push({index: searchIndex, type: 'pass', length: 4}); + searchIndex += 4; // Move past this occurrence + } + + // Find all "fail" occurrences + searchIndex = 0; + while ((searchIndex = this.soundDetectionBuffer.indexOf('fail', searchIndex)) !== -1) { + foundPatterns.push({index: searchIndex, type: 'fail', length: 4}); + searchIndex += 4; // Move past this occurrence + } + + // Sort by index to play sounds in order they appear + foundPatterns.sort((a, b) => a.index - b.index); + + // Play sounds in the order they appear + for (const pattern of foundPatterns) { + if (pattern.type === 'pass') { + this.soundPlayer.playDing(); + } else { + this.soundPlayer.playBuzzer(); + } + } + + // Clear buffer only up to the end of the last found pattern + // This preserves any partial patterns at the end of the buffer + if (foundPatterns.length > 0) { + const lastPattern = foundPatterns[foundPatterns.length - 1]; + const endOfLastPattern = lastPattern.index + lastPattern.length; + // Keep everything after the last pattern to preserve partial matches + this.soundDetectionBuffer = this.soundDetectionBuffer.slice(endOfLastPattern); } // Keep buffer length manageable diff --git a/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts b/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts index 22046506..9f325ddf 100644 --- a/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts +++ b/src/renderer/src/model/Settings/SoundsSettings/SoundsSettings.ts @@ -4,6 +4,7 @@ import { AppDataManager } from "src/model/AppDataManager/AppDataManager"; export default class SoundsSettings { profileManager: AppDataManager; + // THESE DEFAULTS DON'T MATTER AS THEY ARE OVERRIDDEN WHEN CONFIG IS LOADED playSoundsOnPassFail = false; constructor(profileManager: AppDataManager) { diff --git a/src/renderer/src/view/Settings/SettingsView.tsx b/src/renderer/src/view/Settings/SettingsView.tsx index 36917390..e551a968 100644 --- a/src/renderer/src/view/Settings/SettingsView.tsx +++ b/src/renderer/src/view/Settings/SettingsView.tsx @@ -43,7 +43,7 @@ function SettingsDialog(props: Props) { ), [SettingsCategories.SOUNDS]: ( - + ), }; diff --git a/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx b/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx index 284fc477..621bd234 100644 --- a/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx +++ b/src/renderer/src/view/Settings/SoundsSettings/SoundsSettingsView.tsx @@ -1,25 +1,23 @@ -import { Checkbox, FormControlLabel, Tooltip } from "@mui/material"; +import { Checkbox, FormControlLabel, Tooltip, Button } from "@mui/material"; import { observer } from "mobx-react-lite"; -import React from "react"; -import SoundsSettings from "src/model/Settings/SoundsSettings/SoundsSettings"; -import { App } from "src/model/App"; +import { App } from "src/model/App"; import BorderedSection from "src/view/Components/BorderedSection"; interface Props { - soundsSettings: SoundsSettings; app: App; } function SoundsSettingsView(props: Props) { - const { soundsSettings, app } = props; + const { app } = props; + const soundsSettings = app.settings.soundsSettings return (
{/* =============================================================================== */} {/* SOUND NOTIFICATIONS */} {/* =============================================================================== */} - +
} - label='Play ding sound on "pass" and incorrect buzzer sound on "fail"' + label='Play ding sound on "pass" and incorrect buzzer sound on "fail" (case-insensitive)' sx={{ marginBottom: "10px" }} /> + + {/* Test Sound Buttons */} +
+ + +
From 476f6d295672763fd48f8a21c295a2546f04cd6e Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 15:24:13 +1300 Subject: [PATCH 4/7] Update CHANGELOG for release of v5.8.0. --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7563a614..096dcc78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +## [5.8.0] - 2025-10-13 + +### Added + +- Added the ability to play sounds when "pass" or "fail" is received from the serial connection. + ## [5.7.1] - 2025-10-11 ### Fixed @@ -945,7 +951,8 @@ Fixed bug where pressing Ctrl-Shift-C to copy text from a terminal would enable - Added auto-scroll to TX pane, closes #89. - Added special delete behaviour for backspace button when in "send on enter" mode, closes #90. -[unreleased]: https://github.com/gbmhunter/NinjaTerm/compare/v5.7.1...HEAD +[unreleased]: https://github.com/gbmhunter/NinjaTerm/compare/v5.8.0...HEAD +[5.8.0]: https://github.com/gbmhunter/NinjaTerm/compare/v5.7.1...v5.8.0 [5.7.1]: https://github.com/gbmhunter/NinjaTerm/compare/v5.7.0...v5.7.1 [5.7.0]: https://github.com/gbmhunter/NinjaTerm/compare/v5.6.0...v5.7.0 [5.6.0]: https://github.com/gbmhunter/NinjaTerm/compare/v5.5.0...v5.6.0 From 390d9a9541955c1d8599b1677b7325ef2de1f60c Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 15:58:35 +1300 Subject: [PATCH 5/7] Set main process logging verbosity to file to info. --- src/main/Logging.ts | 1 + src/main/MainBluetoothService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/Logging.ts b/src/main/Logging.ts index aecc4cc6..7ff6c9f4 100644 --- a/src/main/Logging.ts +++ b/src/main/Logging.ts @@ -6,6 +6,7 @@ export function initLogging() { mainLogger.initialize(); // {scope} is in the form "(main)" or "(renderer)". Doesn't need square brackets as already has brackets. mainLogger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] {scope} [{level}] {text}'; + mainLogger.transports.file.level = 'info'; // Setting the IPC level in the main process here results in main log messages being forwarded to the renderer process and shown in the devtools console (useful for debugging without having to dig up the log file). // Set to false to disable. diff --git a/src/main/MainBluetoothService.ts b/src/main/MainBluetoothService.ts index ea73a716..6950a7be 100644 --- a/src/main/MainBluetoothService.ts +++ b/src/main/MainBluetoothService.ts @@ -465,7 +465,7 @@ export class MainBluetoothService { } }); txCharacteristic.on('data', (data: Buffer) => { - log.info(`Received data from ${peripheral.id} on write characteristic. data: ${data.toString('hex')}`); + log.debug(`Received data from ${peripheral.id} on write characteristic. data: ${data.toString('hex')}`); // Send data to renderer this.mainWindow?.webContents.send('bluetooth:data-received', peripheral.id, data); }); From b320cc936c00f47f9b074f67816fe4693518d4bc Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 13 Oct 2025 16:15:13 +1300 Subject: [PATCH 6/7] Remove npm install command from README. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index a4bd82dc..e29553fd 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,6 @@ The files with `default` in the name are the default data for that app version. * Prettier ESLint: Provides formatting of .tsx files. * Playwright: Provides useful add-ons for running and debugging the Playwright E2E tests. - -npm install @abandonware/noble --save --target=37.2.4 --runtime=electron --dist-url=https://electronjs.org/headers - [github-actions-status]: https://github.com/gbmhunter/NinjaTerm/actions/workflows/build-and-test.yml/badge.svg?branch=main [github-actions-url]: https://github.com/gbmhunter/NinjaTerm/actions [github-tag-image]: https://img.shields.io/github/tag/gbmhunter/NinjaTerm.svg?label=version From 3f0e17574d75dbc90895cf10a0bcb6fd3a127881 Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Wed, 15 Oct 2025 07:55:21 +1300 Subject: [PATCH 7/7] Improve BLE disconnection logic by de-registering event handlers. --- CHANGELOG.md | 4 ++++ src/main/Logging.ts | 1 + src/main/MainBluetoothService.ts | 13 ++++++++++--- .../model/ConnController/BluetoothLEController.ts | 14 ++++++++------ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 096dcc78..a4433fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added the ability to play sounds when "pass" or "fail" is received from the serial connection. +### Fixed + +- Improved Bluetooth disconnection logic by de-registering peripheral event listeners when the connection is lost. + ## [5.7.1] - 2025-10-11 ### Fixed diff --git a/src/main/Logging.ts b/src/main/Logging.ts index 7ff6c9f4..73581e04 100644 --- a/src/main/Logging.ts +++ b/src/main/Logging.ts @@ -7,6 +7,7 @@ export function initLogging() { // {scope} is in the form "(main)" or "(renderer)". Doesn't need square brackets as already has brackets. mainLogger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] {scope} [{level}] {text}'; mainLogger.transports.file.level = 'info'; + mainLogger.transports.console.level = 'info'; // Setting the IPC level in the main process here results in main log messages being forwarded to the renderer process and shown in the devtools console (useful for debugging without having to dig up the log file). // Set to false to disable. diff --git a/src/main/MainBluetoothService.ts b/src/main/MainBluetoothService.ts index 6950a7be..65920ecd 100644 --- a/src/main/MainBluetoothService.ts +++ b/src/main/MainBluetoothService.ts @@ -23,6 +23,8 @@ enum ConnectionState { * Provide a Bluetooth service running in the Electron main process for the renderer process to use. * * Uses the noble library under the hood to communicate with Bluetooth devices. + * + * This should probably be refactored to use a finite state machine, as the connection/disconnection logic is a bit messy, especially with all the event handler callbacks. */ export class MainBluetoothService { @@ -283,6 +285,7 @@ export class MainBluetoothService { const peripheral = this.discoveredDevices.find(p => p.id === deviceId); if (!peripheral) { log.error(`Device not found in discovered peripherals. deviceId=${deviceId}`); + this.connectionState = ConnectionState.DISCONNECTED; return { error: 'Device not found in discovered peripherals.' }; } @@ -318,6 +321,7 @@ export class MainBluetoothService { this.connectionAttemptTimeout = null; } this.connectionState = ConnectionState.DISCONNECTED; + peripheral.removeAllListeners(); // Emit a IPC connection attempt complete message, indicating failure this.mainWindow!.webContents.send('bluetooth:connection-attempt-complete', error, null); return; @@ -362,7 +366,10 @@ export class MainBluetoothService { deviceId: this.peripheral!.id, services: this.convertServicesToSerializable(services) }; - this.mainWindow!.webContents.send('bluetooth:connection-attempt-complete', error, bluetoothConnectionAttemptSuccess); + this.mainWindow!.webContents.send( + 'bluetooth:connection-attempt-complete', + error, + bluetoothConnectionAttemptSuccess); } async disconnectFromDevice(deviceId: string): Promise<{ success: boolean; error?: string }> { @@ -382,7 +389,7 @@ export class MainBluetoothService { } }); }); - + peripheral.removeAllListeners(); return { success: true }; } catch (error) { log.error(`Failed to disconnect from Bluetooth device ${deviceId}:`, error); @@ -407,7 +414,6 @@ export class MainBluetoothService { // but also does not trigger an error (fails silently). This event gets triggered, so in this case we need to set connectionState to DISCONNECTED. if (this.connectionState === ConnectionState.CONNECTING) { log.info('Got disconnect event for peripheral, but we are still connecting to it. Setting connectionState to DISCONNECTED.'); - this.connectionState = ConnectionState.DISCONNECTED; // Emit a IPC connection attempt complete message, indicating failure this.mainWindow!.webContents.send('bluetooth:connection-attempt-complete', 'Device disconnected while still connecting and scanning for services and characteristics.', null); } @@ -415,6 +421,7 @@ export class MainBluetoothService { const deviceId = peripheral.id; log.info(`Bluetooth device disconnected: ${deviceId}`); this.connectionState = ConnectionState.DISCONNECTED; + peripheral.removeAllListeners(); this.peripheral = null; this.discoveredServices = []; this.txCharacteristic = null; diff --git a/src/renderer/src/model/ConnController/BluetoothLEController.ts b/src/renderer/src/model/ConnController/BluetoothLEController.ts index c3d1193a..1e1e2c67 100644 --- a/src/renderer/src/model/ConnController/BluetoothLEController.ts +++ b/src/renderer/src/model/ConnController/BluetoothLEController.ts @@ -191,6 +191,7 @@ export class BluetoothLEController { sortDirection: 'asc' | 'desc' = 'desc'; constructor(app: App) { + log.info('BluetoothLEController constructor called.'); this.app = app; // Reset the main process Bluetooth state, in case the renderer was reloaded but the main process was not @@ -209,6 +210,11 @@ export class BluetoothLEController { await this.onIpcBluetoothConnectionAttemptComplete(error, bluetoothConnectionAttemptSuccess); }); + // Listen for RX data. We should get any data until a device is connected. + window.electronAPI.bluetooth.onDataReceived((deviceId: string, data: Buffer) => { + this.app.parseRxData(data); + }); + this.validateAndApplyScanDurationMs(); // Make sure to do this at the end of the constructor @@ -611,11 +617,6 @@ export class BluetoothLEController { return; } - // Setup listener for RX data - window.electronAPI.bluetooth.onDataReceived((deviceId: string, data: Buffer) => { - this.app.parseRxData(data); - }); - // If we get here, we have connected to the device and have found valid services and characteristics for the selected serial protocol // We can consider our connection attempt successful this.app.snackbar.sendToSnackbar( @@ -698,7 +699,8 @@ export class BluetoothLEController { this.app.connController.connState = portState; }); - window.electronAPI.bluetooth.removeAllListeners('bluetooth:data-received'); + // We don't need to remove the listener here as it is only added once in the constructor, not on every device connection. + // window.electronAPI.bluetooth.removeAllListeners('bluetooth:data-received'); } /**