Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lights) add lights switches to support DMX switchpacks #39

Merged
merged 1 commit into from
Jan 26, 2025
Merged
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
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
Loading