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/.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/CHANGELOG.md b/CHANGELOG.md index 7563a614..a4433fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ 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. + +### Fixed + +- Improved Bluetooth disconnection logic by de-registering peripheral event listeners when the connection is lost. + ## [5.7.1] - 2025-10-11 ### Fixed @@ -945,7 +955,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 diff --git a/README.md b/README.md index c1bf6dad..e29553fd 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: @@ -99,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). @@ -174,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 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 00000000..516704c9 Binary files /dev/null and b/public/assets/sounds/fail.mp3 differ diff --git a/public/assets/sounds/pass.mp3 b/public/assets/sounds/pass.mp3 new file mode 100644 index 00000000..d2b9b20e Binary files /dev/null and b/public/assets/sounds/pass.mp3 differ diff --git a/src/main/Logging.ts b/src/main/Logging.ts index aecc4cc6..73581e04 100644 --- a/src/main/Logging.ts +++ b/src/main/Logging.ts @@ -6,6 +6,8 @@ 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'; + 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 ea73a716..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; @@ -465,7 +472,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); }); 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 @@