Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9fdc3da
feat: reload settings when settings.json is changed externally
heavyrubberslave Jul 23, 2026
0186b24
fix: re-announce serial devices after a device source is re-enabled
heavyrubberslave Jul 23, 2026
f329aa1
fix: release serial port on close and let providers re-announce devic…
heavyrubberslave Jul 25, 2026
3f079be
fix: retry a known device that was disabled while connected once it's…
heavyrubberslave Aug 2, 2026
45b71eb
refactor: own connectedDevices internally, merge in its detection info
heavyrubberslave Aug 2, 2026
3ea784a
Own type for ConnectedDevice
heavyrubberslave Aug 2, 2026
31b2d1f
refactor: separate DeviceId and DetectionId as distinct branded types
heavyrubberslave Aug 2, 2026
ae87b55
refactor: let device factories decide whether a detection id doubles …
heavyrubberslave Aug 2, 2026
d8af544
refactor: start/stop settings file watching explicitly in createApp
heavyrubberslave Aug 2, 2026
6e2dc2d
Remove unnecessary comments
heavyrubberslave Aug 2, 2026
b555f21
Some small review fixes
heavyrubberslave Aug 2, 2026
9330de7
fix: prevent a stale in-flight discovery from resurrecting revoked se…
heavyrubberslave Aug 2, 2026
9000f44
fix: close (not just cancel) the write queue on serial port close
heavyrubberslave Aug 2, 2026
1df12ca
fix: notify SynchronousSerialPort onClose subscribers exactly once, o…
heavyrubberslave Aug 2, 2026
672d453
fix: don't emitData() on an already-closed mock port in SlvCtrlPlusDe…
heavyrubberslave Aug 2, 2026
18bc0bd
fix: attach .catch() to fire-and-forget close() calls, add Logger to …
heavyrubberslave Aug 2, 2026
2cf0640
fix: configure Logger.child() mock in Device test fixtures
heavyrubberslave Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/dev/node/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:24-alpine
Comment thread
heavyrubberslave marked this conversation as resolved.

RUN apk --no-cache upgrade && \
apk --no-cache add bash git sudo openssh make
Expand Down
7 changes: 6 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export default [
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-floating-promises": [
"warn",
{
"checkThenables": true
}
],
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-misused-new": "error",
Expand Down
111 changes: 77 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"buttplug": "^3.2.2",
"chokidar": "^5.0.0",
Comment thread
heavyrubberslave marked this conversation as resolved.
"class-transformer": "^0.5.1",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
Expand All @@ -22,7 +23,7 @@
"read-last-lines": "^1.8.0",
"reflect-metadata": "^0.1.13",
"say": "^0.16.0",
"@timesplinter/sequential-task-queue": "^1.3.1",
"@timesplinter/sequential-task-queue": "^1.4.0",
"serialport": "^13.0.0",
"socket.io": "^4.8.3",
"speaker": "https://github.com/SlvCtrlPlus/node-speaker/releases/download/v0.1.0/speaker-v0.1.0.tgz",
Expand Down
20 changes: 17 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export const createApp = (container: Container<ServiceMap>, options: AppOptions)
.use(express.text())
;

const settingsManager = container.get('settings.manager');
settingsManager.load();
settingsManager.startWatching();

configureRoutes(app, container);
configureWebsocket(websocketServer, container);
startDeviceProviders(container);
Expand Down Expand Up @@ -262,14 +266,24 @@ export const createApp = (container: Container<ServiceMap>, options: AppOptions)
const logger = container.get('logger.default');
logger.info('Shutting down...');

await container.get('automation.scriptRuntime').stop();
try {
await container.get('settings.manager').stopWatching();
} catch (e: unknown) {
logError(logger, 'Failed to stop settings file watcher during shutdown', e);
}

try {
await container.get('device.provider.manager').stopProviders();
await container.get('automation.scriptRuntime').stop();
} catch (e: unknown) {
logError(logger, 'Failed to stop device providers during shutdown', e);
logError(logger, 'Failed to stop automation script runtime during shutdown', e);
}

try {
await container.get('device.provider.manager').stopProviders();
} catch (e: unknown) {
logError(logger, 'Failed to stop device providers during shutdown', e);
}

container.get('health.metricsCollector').stop();

await websocketServer.close();
Expand Down
6 changes: 1 addition & 5 deletions src/device/bleDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export default abstract class BleDevice<
@Expose()
private rssi: number;

protected logger: Logger;

protected constructor(
deviceId: DeviceId,
deviceName: string,
Expand All @@ -40,9 +38,7 @@ export default abstract class BleDevice<
eventEmitter: EventEmitter,
logger: Logger,
) {
super(deviceId, deviceName, provider, connectedSince, controllable, attributes, config, eventEmitter);

this.logger = logger.child({ name: this.constructor.name });
super(deviceId, deviceName, provider, connectedSince, controllable, attributes, config, eventEmitter, logger);

this.peripheral = peripheral;
this.rssi = peripheral.rssi;
Expand Down
17 changes: 9 additions & 8 deletions src/device/detectedDeviceOfferQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DeviceDetectionInfo } from './deviceManager.js';
import DeviceOfferRejectedError from './deviceOfferRejectedError.js';
import Logger from '../logging/Logger.js';
import { logError } from '../util/error.js';
import { DetectionId } from './deviceId.js';

export type OfferResult<D extends AnyDevice> =
| { successful: true, device: D }
Expand All @@ -13,15 +14,15 @@ type DeviceOffer<D extends AnyDevice> = (cancellationToken: CancellationToken) =

export default class DetectedDeviceOfferQueue
{
private readonly queues: Map<string, SequentialTaskQueue> = new Map();
private readonly queues: Map<DetectionId, SequentialTaskQueue> = new Map();

private readonly logger: Logger;

public constructor(logger: Logger) {
this.logger = logger;
}

private getOrCreateQueue(detectionId: string): SequentialTaskQueue
private getOrCreateQueue(detectionId: DetectionId): SequentialTaskQueue
{
let queue = this.queues.get(detectionId);

Expand Down Expand Up @@ -58,7 +59,7 @@ export default class DetectedDeviceOfferQueue

const task = queue.push((cancellationToken: CancellationToken) => this.runOffer(deviceOffer, cancellationToken));

return Promise.resolve(task.then(
return task.then(
(result: OfferResult<D>): OfferResult<D> => {
if (result.successful) {
// Reject every other still-queued offer for this detection id without them
Expand All @@ -75,7 +76,7 @@ export default class DetectedDeviceOfferQueue
successful: false,
reason: reason,
})
));
);
}

private async runOffer<D extends AnyDevice>(
Expand Down Expand Up @@ -111,12 +112,12 @@ export default class DetectedDeviceOfferQueue
* callers that need "is a fresh announce still blocked by a past revoke" must call
* dropIfRevoked() first.
*/
public has(detectionId: string): boolean
public has(detectionId: DetectionId): boolean
{
return this.queues.has(detectionId);
}

public dropIfRevoked(detectionId: string): void
public dropIfRevoked(detectionId: DetectionId): void
{
const queue = this.queues.get(detectionId);

Expand All @@ -125,7 +126,7 @@ export default class DetectedDeviceOfferQueue
}
}

private close(detectionId: string, reason: DeviceOfferRejectedError): void
private close(detectionId: DetectionId, reason: DeviceOfferRejectedError): void
{
const queue = this.queues.get(detectionId);

Expand All @@ -136,7 +137,7 @@ export default class DetectedDeviceOfferQueue
this.queues.delete(detectionId);
}

public revoke(detectionId: string, reason: DeviceOfferRejectedError): void
public revoke(detectionId: DetectionId, reason: DeviceOfferRejectedError): void
{
const queue = this.getOrCreateQueue(detectionId);

Expand Down
Loading
Loading