Skip to content

Commit

Permalink
feat(lights) add lights switches to support DMX switchpacks (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoronex committed Feb 5, 2025
1 parent f9dfc7f commit 455acea
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 5 deletions.
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
// Fix for production issue where a Docker volume overwrites the contents of a folder instead of merging them
Expand Down Expand Up @@ -50,18 +51,22 @@ async function createApp(): Promise<void> {

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,
);

Expand Down
3 changes: 3 additions & 0 deletions src/modules/lights/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -37,6 +39,7 @@ export const Entities = [
LightsGroupPars,
LightsGroupMovingHeadRgbs,
LightsGroupMovingHeadWheels,
LightsSwitch,
LightsScene,
LightsSceneEffect,
LightsPredefinedEffect,
Expand Down
25 changes: 25 additions & 0 deletions src/modules/lights/entities/lights-switch.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 4 additions & 1 deletion src/modules/root/entities/lights-controller.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
44 changes: 43 additions & 1 deletion src/modules/root/lights-controller-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +44,7 @@ export default class LightsControllerManager {
constructor(
protected websocket: Namespace,
protected handlerManager: HandlerManager,
protected lightsSwitchManager: LightsSwitchManager,
musicEmitter: MusicEmitter,
lightControllers: LightsController[] = [],
) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<number, number[]>();
Expand Down Expand Up @@ -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)) {
Expand Down
47 changes: 47 additions & 0 deletions src/modules/root/lights-switch-manager.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
35 changes: 35 additions & 0 deletions src/modules/root/root-lights-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import RootLightsService, {
LightsMovingHeadRgbCreateParams,
LightsMovingHeadWheelCreateParams,
LightsParCreateParams,
LightsSwitchCreateParams,
LightsSwitchResponse,
MovingHeadRgbResponse,
MovingHeadWheelResponse,
ParResponse,
Expand All @@ -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;
Expand Down Expand Up @@ -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<LightsSwitchResponse[] | undefined> {
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<LightsSwitchResponse | undefined> {
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<ParResponse[]> {
Expand Down
38 changes: 36 additions & 2 deletions src/modules/root/root-lights-operations-controller.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -28,6 +30,10 @@ export class RootLightsOperationsController extends Controller {
.flat() as LightsGroup[];
}

private async getLightsSwitch(id: number): Promise<LightsSwitch | null> {
return dataSource.getRepository(LightsSwitch).findOne({ where: { id } });
}

/**
* Enable the strobe for all fixtures in the given group
*/
Expand Down Expand Up @@ -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<void> {
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<void> {
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}"`);
}
}
Loading

0 comments on commit 455acea

Please sign in to comment.