diff --git a/src/index.ts b/src/index.ts index ccfb458..9e215e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import EmitterStore from './modules/events/emitter-store'; import Types from './types'; import { OrderManager } from './modules/orders'; import TimedEventsService from './modules/timed-events/timed-events-service'; +import LightsSwitchManager from './modules/root/lights-switch-manager'; async function createApp(): Promise { // Fix for production issue where a Docker volume overwrites the contents of a folder instead of merging them @@ -50,18 +51,22 @@ async function createApp(): Promise { ArtificialBeatGenerator.getInstance().init(emitterStore.musicEmitter); + const lightsSwitchManager = LightsSwitchManager.getInstance(); const handlerManager = HandlerManager.getInstance(io, emitterStore); await handlerManager.init(); const socketConnectionManager = new SocketConnectionManager( handlerManager, + lightsSwitchManager, io, emitterStore.backofficeSyncEmitter, ); await socketConnectionManager.clearSavedSocketIds(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO should this be used somewhere? const lightsControllerManager = new LightsControllerManager( io.of(SocketioNamespaces.LIGHTS), handlerManager, + lightsSwitchManager, emitterStore.musicEmitter, ); diff --git a/src/modules/lights/entities/index.ts b/src/modules/lights/entities/index.ts index 2e42dc7..4b2d4fa 100644 --- a/src/modules/lights/entities/index.ts +++ b/src/modules/lights/entities/index.ts @@ -14,6 +14,7 @@ import LightsParShutterOptions from './lights-par-shutter-options'; import LightsMovingHeadRgbShutterOptions from './lights-moving-head-rgb-shutter-options'; import LightsMovingHeadWheelShutterOptions from './lights-moving-head-wheel-shutter-options'; import LightsWheelRotateChannelValue from './lights-wheel-rotate-channel-value'; +import LightsSwitch from './lights-switch'; export { default as LightsGroup } from './lights-group'; export { default as LightsPar } from './lights-par'; @@ -22,6 +23,7 @@ export { default as LightsMovingHeadWheel } from './lights-moving-head-wheel'; export { default as LightsGroupPars } from './lights-group-pars'; export { default as LightsGroupMovingHeadRgbs } from './lights-group-moving-head-rgbs'; export { default as LightsGroupMovingHeadWheels } from './lights-group-moving-head-wheels'; +export { default as LightsSwitch } from './lights-switch'; export const Entities = [ LightsGroup, @@ -37,6 +39,7 @@ export const Entities = [ LightsGroupPars, LightsGroupMovingHeadRgbs, LightsGroupMovingHeadWheels, + LightsSwitch, LightsScene, LightsSceneEffect, LightsPredefinedEffect, diff --git a/src/modules/lights/entities/lights-switch.ts b/src/modules/lights/entities/lights-switch.ts new file mode 100644 index 0000000..266551e --- /dev/null +++ b/src/modules/lights/entities/lights-switch.ts @@ -0,0 +1,25 @@ +import BaseEntity from '../../root/entities/base-entity'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import LightsController from '../../root/entities/lights-controller'; + +@Entity() +export default class LightsSwitch extends BaseEntity { + @ManyToOne(() => LightsController, { eager: true }) + @JoinColumn() + public controller: LightsController; + + /** + * DMX channel this lights switch is on + */ + @Column({ type: 'tinyint', unsigned: true }) + public dmxChannel: number; + + /** + * DMX value to send to the channel to turn on the switch + */ + @Column({ type: 'tinyint', unsigned: true }) + public onValue: number; + + @Column() + public name: string; +} diff --git a/src/modules/root/entities/lights-controller.ts b/src/modules/root/entities/lights-controller.ts index 644e374..9aeae29 100644 --- a/src/modules/root/entities/lights-controller.ts +++ b/src/modules/root/entities/lights-controller.ts @@ -1,10 +1,13 @@ import { Entity, OneToMany } from 'typeorm'; // eslint-disable-next-line import/no-cycle -import { LightsGroup } from '../../lights/entities'; +import { LightsGroup, LightsSwitch } from '../../lights/entities'; import SubscribeEntity from './subscribe-entity'; @Entity() export default class LightsController extends SubscribeEntity { @OneToMany(() => LightsGroup, (group) => group.controller) public lightsGroups: LightsGroup[]; + + @OneToMany(() => LightsSwitch, (lightsSwitch) => lightsSwitch.controller) + public lightsSwitches: LightsSwitch[]; } diff --git a/src/modules/root/lights-controller-manager.ts b/src/modules/root/lights-controller-manager.ts index f3a3024..f865301 100644 --- a/src/modules/root/lights-controller-manager.ts +++ b/src/modules/root/lights-controller-manager.ts @@ -7,9 +7,11 @@ import { LightsGroupMovingHeadRgbs, LightsGroupMovingHeadWheels, LightsGroup, + LightsSwitch, } from '../lights/entities'; import HandlerManager from './handler-manager'; import { SocketioNamespaces } from '../../socketio-namespaces'; +import LightsSwitchManager from './lights-switch-manager'; const DMX_VALUES_LENGTH = 512; @@ -42,6 +44,7 @@ export default class LightsControllerManager { constructor( protected websocket: Namespace, protected handlerManager: HandlerManager, + protected lightsSwitchManager: LightsSwitchManager, musicEmitter: MusicEmitter, lightControllers: LightsController[] = [], ) { @@ -78,7 +81,9 @@ export default class LightsControllerManager { if (packet.length !== oldPacket.length) return true; for (let i = 0; i < packet.length; i += 1) { - if (packet[i] !== oldPacket[i]) return true; + if (packet[i] !== oldPacket[i]) { + return true; + } } return false; } @@ -87,6 +92,10 @@ export default class LightsControllerManager { return this.handlerManager.getHandlers(LightsGroup) as BaseLightsHandler[]; } + private get lightsSwitches(): LightsSwitch[] { + return this.lightsSwitchManager.getEnabledSwitches(); + } + /** * Given a fixture, the old DMX packet and the new DMX packet, * copy the old values to the new packet @@ -124,6 +133,22 @@ export default class LightsControllerManager { return packet; } + /** + * Given a lights switch, enable that switch in the DMX packet + * @param lightsSwitch + * @param packet Array of at least 512 integers in the range [0, 255] + * @private + */ + private enableLightsSwitch(lightsSwitch: LightsSwitch, packet: number[]): number[] { + const c = lightsSwitch.dmxChannel - 1; + const existingValue = packet[c] || 0; + + // Bitwise OR, as there might be multiple switches on the same DMX channel + packet[c] = existingValue | lightsSwitch.onValue; + + return packet; + } + /** * * @private @@ -159,6 +184,7 @@ export default class LightsControllerManager { */ private tick() { const lightGroups = this.lightsHandlers.map((h) => h.tick()); + const lightsSwitches = this.lightsSwitches; // Create a new mapping from DMX controller ID to DMX packet const newControllerValues = new Map(); @@ -206,6 +232,22 @@ export default class LightsControllerManager { newControllerValues.set(g.controller.id, newValues); }); + // Turn on the lights switches + lightsSwitches.forEach((s) => { + const controllerId = s.controller.id; + if (!this.lightsControllers.has(controllerId)) { + // Update the reference to the controller + this.lightsControllers.set(controllerId, s.controller); + } + + let newValues = newControllerValues.get(controllerId); + if (!newValues) { + newValues = this.constructNewValuesArray(); + } + newValues = this.enableLightsSwitch(s, newValues); + newControllerValues.set(controllerId, newValues); + }); + const controllersToRemove: number[] = []; Array.from(this.lightsControllers.keys()).forEach((controllerId) => { if (!newControllerValues.has(controllerId)) { diff --git a/src/modules/root/lights-switch-manager.ts b/src/modules/root/lights-switch-manager.ts new file mode 100644 index 0000000..b30bfd1 --- /dev/null +++ b/src/modules/root/lights-switch-manager.ts @@ -0,0 +1,47 @@ +import { LightsSwitch } from '../lights/entities'; + +export default class LightsSwitchManager { + private static instance: LightsSwitchManager; + + private enabledSwitches: LightsSwitch[] = []; + + private constructor() {} + + public static getInstance(): LightsSwitchManager { + if (this.instance == null) { + this.instance = new LightsSwitchManager(); + } + return this.instance; + } + + /** + * Turn on the given switch if it is turned off + * @param lightsSwitch + */ + public enableSwitch(lightsSwitch: LightsSwitch): void { + const index = this.enabledSwitches.findIndex((s) => s.id === lightsSwitch.id); + // Switch is already turned on + if (index >= 0) return; + + this.enabledSwitches.push(lightsSwitch); + } + + /** + * Turn off the given switch if it is turned on + * @param lightsSwitch + */ + public disableSwitch(lightsSwitch: LightsSwitch) { + const index = this.enabledSwitches.findIndex((s) => s.id === lightsSwitch.id); + if (index >= 0) { + // Remove the switch from the list if it is turned on + this.enabledSwitches.splice(index, 1); + } + } + + /** + * Get all lights switches that are turned on + */ + public getEnabledSwitches(): LightsSwitch[] { + return this.enabledSwitches; + } +} diff --git a/src/modules/root/root-lights-controller.ts b/src/modules/root/root-lights-controller.ts index 252e638..e1f2bf6 100644 --- a/src/modules/root/root-lights-controller.ts +++ b/src/modules/root/root-lights-controller.ts @@ -8,6 +8,8 @@ import RootLightsService, { LightsMovingHeadRgbCreateParams, LightsMovingHeadWheelCreateParams, LightsParCreateParams, + LightsSwitchCreateParams, + LightsSwitchResponse, MovingHeadRgbResponse, MovingHeadWheelResponse, ParResponse, @@ -22,6 +24,7 @@ import { import { SecurityGroup, SecurityNames } from '../../helpers/security'; import { securityGroups } from '../../helpers/security-groups'; import { Request as ExpressRequest } from 'express'; +import { LightsSwitch } from '../lights/entities'; interface LightsColorResponse { color: RgbColor; @@ -111,6 +114,38 @@ export class RootLightsController extends Controller { return RootLightsService.toLightsGroupResponse(group); } + @Security(SecurityNames.LOCAL, securityGroups.light.subscriber) + @Get('controller/{id}/switches') + public async getControllerLightsSwitches( + @Request() req: ExpressRequest, + id: number, + ): Promise { + if ( + !req.user || + (!req.user.roles.includes(SecurityGroup.ADMIN) && req.user.lightsControllerId !== id) + ) { + this.setStatus(403); + return undefined; + } + + const switches = await new RootLightsService().getAllLightsSwitches(id); + return switches.map((s) => RootLightsService.toLightsSwitchResponse(s)); + } + + @Security(SecurityNames.LOCAL, securityGroups.light.privileged) + @Post('controller/{id}/switches') + public async createLightsSwitch( + id: number, + @Body() params: LightsSwitchCreateParams, + ): Promise { + const lightsSwitch = await new RootLightsService().createLightsSwitch(id, params); + if (!lightsSwitch) { + this.setStatus(404); + return undefined; + } + return RootLightsService.toLightsSwitchResponse(lightsSwitch); + } + @Security(SecurityNames.LOCAL, securityGroups.light.privileged) @Get('fixture/par') public async getAllLightsPars(): Promise { diff --git a/src/modules/root/root-lights-operations-controller.ts b/src/modules/root/root-lights-operations-controller.ts index 3dfba3f..0825206 100644 --- a/src/modules/root/root-lights-operations-controller.ts +++ b/src/modules/root/root-lights-operations-controller.ts @@ -1,12 +1,14 @@ -import { Body, Post, Request, Route, Security, Tags } from 'tsoa'; +import { Body, Get, Post, Request, Route, Security, Tags } from 'tsoa'; import { Controller } from '@tsoa/runtime'; import { Request as ExpressRequest } from 'express'; import HandlerManager from './handler-manager'; -import { LightsGroup } from '../lights/entities'; +import { LightsGroup, LightsSwitch } from '../lights/entities'; import { StrobeProps } from '../lights/effects/color/strobe'; import { SecurityNames } from '../../helpers/security'; import logger from '../../logger'; import { securityGroups } from '../../helpers/security-groups'; +import dataSource from '../../database'; +import LightsSwitchManager from './lights-switch-manager'; interface GroupFixtureOverrideParams { /** @@ -28,6 +30,10 @@ export class RootLightsOperationsController extends Controller { .flat() as LightsGroup[]; } + private async getLightsSwitch(id: number): Promise { + return dataSource.getRepository(LightsSwitch).findOne({ where: { id } }); + } + /** * Enable the strobe for all fixtures in the given group */ @@ -376,4 +382,32 @@ export class RootLightsOperationsController extends Controller { chosenMovingHead.fixture.unfreezeDmx(); } + + @Security(SecurityNames.LOCAL, securityGroups.lightOperation.base) + @Post('switch/{id}/on') + public async turnOnLightsSwitch(@Request() req: ExpressRequest, id: number): Promise { + const lightsSwitch = await this.getLightsSwitch(id); + if (!lightsSwitch) { + this.setStatus(404); + return; + } + + LightsSwitchManager.getInstance().enableSwitch(lightsSwitch); + + logger.audit(req.user, `Turn on lights switch "${lightsSwitch.name}"`); + } + + @Security(SecurityNames.LOCAL, securityGroups.lightOperation.base) + @Post('switch/{id}/off') + public async turnOffLightsSwitch(@Request() req: ExpressRequest, id: number): Promise { + const lightsSwitch = await this.getLightsSwitch(id); + if (!lightsSwitch) { + this.setStatus(404); + return; + } + + LightsSwitchManager.getInstance().disableSwitch(lightsSwitch); + + logger.audit(req.user, `Turn off lights switch "${lightsSwitch.name}"`); + } } diff --git a/src/modules/root/root-lights-service.ts b/src/modules/root/root-lights-service.ts index 46b9c19..c0e2020 100644 --- a/src/modules/root/root-lights-service.ts +++ b/src/modules/root/root-lights-service.ts @@ -1,10 +1,11 @@ -import { Repository } from 'typeorm'; +import { FindOptionsWhere, Repository } from 'typeorm'; import { LightsController } from './entities'; import { LightsGroup, LightsMovingHeadRgb, LightsMovingHeadWheel, LightsPar, + LightsSwitch, } from '../lights/entities'; import dataSource from '../../database'; import LightsFixture from '../lights/entities/lights-fixture'; @@ -23,6 +24,7 @@ import LightsFixtureShutterOptions, { } from '../lights/entities/lights-fixture-shutter-options'; import { WheelColor } from '../lights/color-definitions'; import { IColorsWheel } from '../lights/entities/colors-wheel'; +import LightsSwitchManager from './lights-switch-manager'; export interface LightsControllerResponse extends Pick {} @@ -93,6 +95,12 @@ export interface LightsGroupResponse extends BaseLightsGroupResponse { movingHeadWheels: FixtureInGroupResponse[]; } +export interface LightsSwitchResponse + extends Pick< + LightsSwitch, + 'id' | 'createdAt' | 'updatedAt' | 'name' | 'dmxChannel' | 'onValue' + > {} + export interface ColorParams { masterDimChannel?: number; shutterChannel?: number; @@ -190,6 +198,9 @@ export interface LightsGroupCreateParams extends Pick {} + export interface LightsControllerCreateParams extends Pick {} export default class RootLightsService { @@ -349,6 +360,17 @@ export default class RootLightsService { }; } + public static toLightsSwitchResponse(s: LightsSwitch): LightsSwitchResponse { + return { + id: s.id, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + name: s.name, + dmxChannel: s.dmxChannel, + onValue: s.onValue, + }; + } + public async getAllControllers(): Promise { return this.controllerRepository.find(); } @@ -590,4 +612,27 @@ export default class RootLightsService { )) as LightsMovingHeadWheelShutterOptions[]; return movingHead; } + + public async getAllLightsSwitches(controllerId?: number): Promise { + let whereClause: FindOptionsWhere = {}; + if (controllerId) { + whereClause = { controller: { id: controllerId } }; + } + + return dataSource.getRepository(LightsSwitch).find({ where: whereClause }); + } + + public async createLightsSwitch( + controllerId: number, + params: LightsSwitchCreateParams, + ): Promise { + const controller = await this.controllerRepository.findOne({ where: { id: controllerId } }); + if (controller == null) return null; + + const repository = dataSource.getRepository(LightsSwitch); + return repository.save({ + controller, + ...params, + }); + } } diff --git a/src/modules/root/socket-connection-manager.ts b/src/modules/root/socket-connection-manager.ts index e5eb467..ef32227 100644 --- a/src/modules/root/socket-connection-manager.ts +++ b/src/modules/root/socket-connection-manager.ts @@ -12,6 +12,7 @@ import SubscribeEntity from './entities/subscribe-entity'; import BaseHandler from '../handlers/base-handler'; import logger from '../../logger'; import { BackofficeSyncEmitter } from '../events/backoffice-sync-emitter'; +import LightsSwitchManager from './lights-switch-manager'; export default class SocketConnectionManager { /** @@ -26,6 +27,7 @@ export default class SocketConnectionManager { constructor( private handlerManager: HandlerManager, + private lightsSwitchManager: LightsSwitchManager, private ioServer: Server, private backofficeEmitter: BackofficeSyncEmitter, ) { @@ -161,6 +163,11 @@ export default class SocketConnectionManager { // eslint-disable-next-line no-param-reassign g.controller.socketIds = controller.socketIds; }); + this.lightsSwitchManager.getEnabledSwitches().forEach((s) => { + if (s.controller.id !== user.lightsControllerId) return; + // eslint-disable-next-line no-param-reassign + s.controller.socketIds = controller.socketIds; + }); } this.backofficeEmitter.emit('connect_lightsgroup'); }