Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ jobs:
run: yarn install
- name: Linting
run: |
npm run lint
- name: Coverage
yarn run lint
- name: Unit tests & coverage
run: |
npm run coverage
yarn run coverage
- name: Integration tests
run: |
yarn run test ./tests/integration
14 changes: 12 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type { Config } from 'jest';

const config: Config = {
preset: "ts-jest",
extensionsToTreatAsEsm: ['.ts'],
transform: {
"^.+\\.ts?$": [
"ts-jest",
{
useESM: true,
},
],
},
setupFilesAfterEnv: ['./tests/jest.setup.ts'],
transform: { '\\.[jt]s$': ['ts-jest', { tsconfig: { allowJs: true } }] },
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.[jt]s$': '$1',
},
testEnvironment: "node",
coverageDirectory: "./build/coverage",
coverageReporters: ['lcov', 'text-summary'],
coverageThreshold: {
global: {
branches: 0,
Expand All @@ -16,7 +26,7 @@ const config: Config = {
statements: 0
}
},
"collectCoverageFrom": [
collectCoverageFrom: [
"src/**/*"
]
};
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"express-async-handler": "^1.2.0",
"glob": "^8.0.1",
"node-os-utils": "^1.3.7",
"on-change": "^4.0.1",
"on-change": "^5.0.1",
"pino": "^8.17.2",
"read-last-lines": "^1.8.0",
"reflect-metadata": "^0.1.13",
Expand All @@ -38,6 +38,7 @@
"@types/node-os-utils": "^1.3.1",
"@types/pino": "^7.0.5",
"@types/serialport": "^8.0.2",
"@types/supertest": "^6.0.2",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
Expand All @@ -50,6 +51,7 @@
"jest-mock-extended": "^3.0.4",
"nodemon": "^3.0.2",
"pino-pretty": "^10.3.0",
"supertest": "^6.3.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
Expand All @@ -60,8 +62,8 @@
"scripts": {
"compile": "tsc && node dist/index.js --trace-warnings | pino-pretty",
"dev": "nodemon",
"test": "jest",
"coverage": "jest --collect-coverage",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
"coverage": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest ./tests/unit --collect-coverage",
"lint": "eslint -c .eslintrc.cjs --ext .ts .",
"build": "tsc"
}
Expand Down
157 changes: 157 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import 'dotenv/config';
import 'reflect-metadata';
import cors from 'cors';
import contentTypeMiddleware from './middleware/contentTypeMiddleware.js';
import express from 'express';
import {Pimple} from '@timesplinter/pimple';
import ControllerServiceProvider from './serviceProvider/controllerServiceProvider.js';
import RepositoryServiceProvider from './serviceProvider/repositoryServiceProvider.js';
import SerializationServiceProvider from './serviceProvider/serializationServiceProvider.js';
import FactoryServiceProvider from './serviceProvider/factoryServiceProvider.js';
import DeviceServiceProvider from "./serviceProvider/deviceServiceProvider.js";
import SettingsServiceProvider from "./serviceProvider/settingsServiceProvider.js";
import * as http from 'http'
import SocketServiceProvider from "./serviceProvider/socketServiceProvider.js";
import {DeviceUpdateData} from "./socket/types";
import AutomationServiceProvider from "./serviceProvider/automationServiceProvider.js";
import type Device from "./device/device.js";
import WebSocketEvent from "./device/webSocketEvent.js";
import ServerServiceProvider from "./serviceProvider/serverServiceProvider.js";
import asyncHandler from "express-async-handler"
import AutomationEventType from "./automation/automationEventType.js";
import DeviceManagerEvent from "./device/deviceManagerEvent.js";
import LoggerServiceProvider from "./serviceProvider/loggerServiceProvider.js";
import DeviceDiscriminator from "./serialization/discriminator/deviceDiscriminator.js";
import ServiceMap from "./serviceMap.js";

const container = new Pimple<ServiceMap>();
export const app = express();
const httpServer = http.createServer(app);

container
.register(new LoggerServiceProvider())
.register(new ServerServiceProvider(httpServer))
.register(new SettingsServiceProvider())
.register(new DeviceServiceProvider())
.register(new ControllerServiceProvider())
.register(new SocketServiceProvider())
.register(new RepositoryServiceProvider())
.register(new SerializationServiceProvider())
.register(new AutomationServiceProvider())
.register(new FactoryServiceProvider())
;

export const logger = container.get('logger.default');
const io = container.get('server.websocket');
const deviceManager = container.get('device.manager');
const scriptRuntime = container.get('automation.scriptRuntime');

container.get('device.provider.loader').loadFromSettings();

// Middlewares
app
.use(cors())
.use(contentTypeMiddleware)
.use(express.json())
.use(express.text())
;

// Routes
app.get('/health', asyncHandler((req, res) => {
const controller = container.get('controller.health')
return controller.execute(req, res)
}));

app.get('/devices', (req, res) => {
const controller = container.get('controller.getDevices')
return controller.execute(req, res)
});

app.get('/device/:deviceId', (req, res) => {
const controller = container.get('controller.getDevice')
return controller.execute(req, res)
});

app.patch('/device/:deviceId', (req, res) => {
const controller = container.get('controller.patchDevice')
return controller.execute(req, res)
});

app.get('/automation/scripts', (req, res) => {
const controller = container.get('controller.automation.getScripts')
return controller.execute(req, res)
});

app.get('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => {
const controller = container.get('controller.automation.getScript')
return controller.execute(req, res)
});

app.post('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => {
const controller = container.get('controller.automation.createScript')
return controller.execute(req, res)
});

app.delete('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => {
const controller = container.get('controller.automation.deleteScript')
return controller.execute(req, res)
});

app.get('/automation/log', asyncHandler((req, res) => {
const controller = container.get('controller.automation.getLog')
return controller.execute(req, res)
}));

app.post('/automation/run', (req, res) => {
const controller = container.get('controller.automation.runScript')
return controller.execute(req, res)
});

app.get('/automation/stop', (req, res) => {
const controller = container.get('controller.automation.stopScript')
return controller.execute(req, res)
});

app.get('/automation/status', (req, res) => {
const controller = container.get('controller.automation.statusScript')
return controller.execute(req, res)
});

// Whenever someone connects this gets executed
io.on('connection', socket => {
logger.debug(`Client connected: ${socket.id}`);

socket.on('disconnect', () => {
logger.debug(`Client disconnected: ${socket.id}`);
});

const deviceUpdateHandler = container.get('socket.deviceUpdateHandler');

socket.on(WebSocketEvent.deviceUpdateReceived, (data) => deviceUpdateHandler.handle(data as DeviceUpdateData));
});

const serializer = container.get('serializer.classToPlain');

const deviceDiscriminator = DeviceDiscriminator.createClassTransformerTypeDiscriminator('type');

deviceManager.on(DeviceManagerEvent.deviceConnected, (device: Device) => {
io.emit(WebSocketEvent.deviceConnected, serializer.transform(device, deviceDiscriminator));
scriptRuntime.runForEvent(DeviceManagerEvent.deviceConnected, device);
});

deviceManager.on(DeviceManagerEvent.deviceDisconnected, (device: Device) => {
io.emit(WebSocketEvent.deviceDisconnected, serializer.transform(device, deviceDiscriminator));
scriptRuntime.runForEvent(DeviceManagerEvent.deviceDisconnected, device);
});

deviceManager.on(DeviceManagerEvent.deviceRefreshed, (device: Device) => {
io.emit(WebSocketEvent.deviceRefreshed, serializer.transform(device, deviceDiscriminator));
scriptRuntime.runForEvent(DeviceManagerEvent.deviceRefreshed, device);
});

// Automation events
scriptRuntime.on(AutomationEventType.consoleLog, (data: any) => io.emit(AutomationEventType.consoleLog, data));

process.on('uncaughtException', (err: Error) => {
logger.error('Asynchronous error caught.', err);
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider

this.connectToServer();

setInterval(() => { this.connectToServer() }, 3000);
setInterval(() => { this.connectToServer() }, 3000).unref();

if (true === this.autoScan) {
setInterval(() => { this.discoverButtplugIoDevices() }, 60000);
setInterval(() => { this.discoverButtplugIoDevices() }, 60000).unref();
}

resolve();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider
{
return new Promise<void>((resolve) => {
// Scan for new SlvCtrl+ protocol serial devices every 3 seconds
setInterval(() => { this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) }, 3000);
setInterval(() => { this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) }, 3000).unref();
resolve();
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/device/provider/deviceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export default abstract class DeviceProvider

deviceStatusUpdater();

return setInterval(deviceStatusUpdater, device.getRefreshInterval);
return setInterval(deviceStatusUpdater, device.getRefreshInterval).unref();
}
}
Loading