From 608dccc9ea46ea245ec5398e29ee5c5a89d4a56a Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Wed, 29 Jan 2025 20:20:23 +0100 Subject: [PATCH 1/7] chore(lights): make pattern enum string-based --- .../lights/effects/lights-effect-pattern.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/lights/effects/lights-effect-pattern.ts b/src/modules/lights/effects/lights-effect-pattern.ts index 56f53f6..c67ee16 100644 --- a/src/modules/lights/effects/lights-effect-pattern.ts +++ b/src/modules/lights/effects/lights-effect-pattern.ts @@ -1,14 +1,14 @@ export enum LightsEffectPattern { - HORIZONTAL, - VERTICAL, - DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, - DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, - CENTERED_CIRCULAR, - CENTERED_SQUARED, - ROTATIONAL, + HORIZONTAL = 'horizontal', + VERTICAL = 'vertical', + DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT = 'diagonal_bottom_left_to_top_right', + DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT = 'diagonal_top_left_to_bottom_right', + CENTERED_CIRCULAR = 'centered_circular', + CENTERED_SQUARED = 'centered_squared', + ROTATIONAL = 'rotational', } export enum LightsEffectDirection { - FORWARDS, - BACKWARDS, + FORWARDS = 'forwards', + BACKWARDS = 'backwards', } From e7c2d62e2f8c97b1aff24657eebbe4a307ec6c0a Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Wed, 29 Jan 2025 20:27:02 +0100 Subject: [PATCH 2/7] chore(lights): rename predefined effect to track effect --- src/modules/handlers/lights/effect-sequence-handler.ts | 6 +++--- src/modules/lights/entities/index.ts | 4 ++-- .../{lights-predefined-effect.ts => lights-track-effect.ts} | 2 +- src/seed/seed.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/modules/lights/entities/sequences/{lights-predefined-effect.ts => lights-track-effect.ts} (94%) diff --git a/src/modules/handlers/lights/effect-sequence-handler.ts b/src/modules/handlers/lights/effect-sequence-handler.ts index 9a4bf87..f71c4b7 100644 --- a/src/modules/handlers/lights/effect-sequence-handler.ts +++ b/src/modules/handlers/lights/effect-sequence-handler.ts @@ -1,7 +1,7 @@ import BaseLightsHandler from '../base-lights-handler'; import { BeatEvent, TrackChangeEvent } from '../../events/music-emitter-events'; import { LightsGroup } from '../../lights/entities'; -import { LightsPredefinedEffect } from '../../lights/entities/sequences/lights-predefined-effect'; +import { LightsTrackEffect } from '../../lights/entities/sequences/lights-track-effect'; import LightsEffect from '../../lights/effects/lights-effect'; import dataSource from '../../../database'; import { MusicEmitter } from '../../events'; @@ -26,7 +26,7 @@ interface LightsGroupEffect extends LightsGroupEffectBase { } export default class EffectSequenceHandler extends BaseLightsHandler { - private sequence: LightsPredefinedEffect[] = []; + private sequence: LightsTrackEffect[] = []; private sequenceStart: Date = new Date(0); @@ -171,7 +171,7 @@ export default class EffectSequenceHandler extends BaseLightsHandler { this.stopSequence(true); dataSource - .getRepository(LightsPredefinedEffect) + .getRepository(LightsTrackEffect) .find({ where: { trackUri: event.trackURI }, relations: { lightGroups: true }, diff --git a/src/modules/lights/entities/index.ts b/src/modules/lights/entities/index.ts index 4b2d4fa..db351ef 100644 --- a/src/modules/lights/entities/index.ts +++ b/src/modules/lights/entities/index.ts @@ -9,7 +9,7 @@ import LightsGroupMovingHeadWheels from './lights-group-moving-head-wheels'; import LightsWheelColorChannelValue from './lights-wheel-color-channel-value'; import LightsWheelGoboChannelValue from './lights-wheel-gobo-channel-value'; import { LightsScene, LightsSceneEffect } from './scenes'; -import { LightsPredefinedEffect } from './sequences/lights-predefined-effect'; +import { LightsTrackEffect } from './sequences/lights-track-effect'; 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'; @@ -42,5 +42,5 @@ export const Entities = [ LightsSwitch, LightsScene, LightsSceneEffect, - LightsPredefinedEffect, + LightsTrackEffect, ]; diff --git a/src/modules/lights/entities/sequences/lights-predefined-effect.ts b/src/modules/lights/entities/sequences/lights-track-effect.ts similarity index 94% rename from src/modules/lights/entities/sequences/lights-predefined-effect.ts rename to src/modules/lights/entities/sequences/lights-track-effect.ts index 00ec460..789dbc9 100644 --- a/src/modules/lights/entities/sequences/lights-predefined-effect.ts +++ b/src/modules/lights/entities/sequences/lights-track-effect.ts @@ -4,7 +4,7 @@ import BaseEntity from '../../../root/entities/base-entity'; import LightsGroup from '../lights-group'; @Entity() -export class LightsPredefinedEffect extends BaseEntity { +export class LightsTrackEffect extends BaseEntity { /** * Either a Spotify Track URI (spotify:track:) or a local identifier (local:) */ diff --git a/src/seed/seed.ts b/src/seed/seed.ts index 2f9a2fa..e86c735 100644 --- a/src/seed/seed.ts +++ b/src/seed/seed.ts @@ -6,7 +6,7 @@ import { LightsGroup, LightsMovingHeadWheel } from '../modules/lights/entities'; import { RgbColor, WheelColor } from '../modules/lights/color-definitions'; import { SparkleCreateParams } from '../modules/lights/effects/color/sparkle'; import { StaticColorCreateParams } from '../modules/lights/effects/color/static-color'; -import { LightsPredefinedEffect } from '../modules/lights/entities/sequences/lights-predefined-effect'; +import { LightsTrackEffect } from '../modules/lights/entities/sequences/lights-track-effect'; import { LightsEffectsCreateParams } from '../modules/lights/effects'; import { WaveCreateParams } from '../modules/lights/effects/color/wave'; import { LightsScene, LightsSceneEffect } from '../modules/lights/entities/scenes'; @@ -488,7 +488,7 @@ export async function seedOpeningSequence( movingHeadsGEWIS: LightsGroup, movingHeadsRoy?: LightsGroup, ) { - const repo = dataSource.getRepository(LightsPredefinedEffect); + const repo = dataSource.getRepository(LightsTrackEffect); const trackUri = 'spotify:track:22L7bfCiAkJo5xGSQgmiIO'; const addStep = async ( @@ -504,7 +504,7 @@ export async function seedOpeningSequence( effect: effect.type, effectProps: JSON.stringify(effect.props), lightGroups, - } as LightsPredefinedEffect); + } as LightsTrackEffect); }; const allOfTheLights: SparkleCreateParams = { From c742d16b4cc4816ee48484664db5256b85c47114 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Wed, 29 Jan 2025 21:49:27 +0100 Subject: [PATCH 3/7] feat(effects-controller): add endpoints for effect controller buttons --- src/helpers/security-groups.ts | 1 + .../handlers/lights/set-effects-controller.ts | 60 ++++++++++- .../handlers/lights/set-effects-service.ts | 99 +++++++++++++++++++ .../scenes/lights-predefined-effect.ts | 55 +++++++++++ src/modules/root/lights-switch-manager.ts | 8 ++ src/modules/root/root-lights-controller.ts | 9 +- src/modules/root/root-lights-service.ts | 18 +++- 7 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/modules/handlers/lights/set-effects-service.ts create mode 100644 src/modules/lights/entities/scenes/lights-predefined-effect.ts diff --git a/src/helpers/security-groups.ts b/src/helpers/security-groups.ts index 16bf213..b96f352 100644 --- a/src/helpers/security-groups.ts +++ b/src/helpers/security-groups.ts @@ -84,6 +84,7 @@ export const securityGroups = { }, effects: { base: baseSecurityGroups, + privileged: [SecurityGroup.ADMIN], }, poster: { base: allSecuritySubscriberGroups, diff --git a/src/modules/handlers/lights/set-effects-controller.ts b/src/modules/handlers/lights/set-effects-controller.ts index e79d416..078ed34 100644 --- a/src/modules/handlers/lights/set-effects-controller.ts +++ b/src/modules/handlers/lights/set-effects-controller.ts @@ -1,5 +1,5 @@ -import { Controller } from '@tsoa/runtime'; -import { Body, Post, Request, Route, Security, Tags } from 'tsoa'; +import { Controller, Patch } from '@tsoa/runtime'; +import { Body, Delete, Get, Post, Request, Route, Security, Tags } from 'tsoa'; import { Request as ExpressRequest } from 'express'; import SetEffectsHandler from './set-effects-handler'; import HandlerManager from '../../root/handler-manager'; @@ -9,6 +9,11 @@ import { LightsEffectsColorCreateParams } from '../../lights/effects/color'; import { LightsEffectsMovementCreateParams } from '../../lights/effects/movement'; import logger from '../../../logger'; import { securityGroups } from '../../../helpers/security-groups'; +import SetEffectsService, { + LightsPredefinedEffectCreateParams, + LightsPredefinedEffectResponse, + LightsPredefinedEffectUpdateParams, +} from './set-effects-service'; @Route('handler/lights/set-effects') @Tags('Handlers') @@ -86,4 +91,55 @@ export class SetEffectsController extends Controller { return { message: 'success' }; } + + /** + * Get all existing predefined effects + */ + @Security(SecurityNames.LOCAL, securityGroups.effects.base) + @Get('predefined') + public async getAllPredefinedLightsEffects(): Promise { + const effects = await new SetEffectsService().getAllPredefinedEffects(); + return effects.map(SetEffectsService.toLightsEffectPredefinedEffectResponse); + } + + /** + * Create a new predefined effect + */ + @Security(SecurityNames.LOCAL, securityGroups.effects.privileged) + @Post('predefined') + public async createPredefinedLightsEffect( + @Request() req: ExpressRequest, + @Body() predefinedEffect: LightsPredefinedEffectCreateParams, + ): Promise { + const effect = await new SetEffectsService().createPredefinedEffect(predefinedEffect); + + logger.audit(req.user, `Create new predefined effect on button "${predefinedEffect.buttonId}"`); + + return SetEffectsService.toLightsEffectPredefinedEffectResponse(effect); + } + + @Security(SecurityNames.LOCAL, securityGroups.effects.privileged) + @Patch('predefined/{id}') + public async updatePredefinedLightsEffect( + id: number, + @Request() req: ExpressRequest, + @Body() predefinedEffect: LightsPredefinedEffectUpdateParams, + ): Promise { + const effect = await new SetEffectsService().updatePredefinedEffect(id, predefinedEffect); + + logger.audit(req.user, `Update new predefined effect with id "${id}"`); + + return SetEffectsService.toLightsEffectPredefinedEffectResponse(effect); + } + + @Security(SecurityNames.LOCAL, securityGroups.effects.privileged) + @Delete('predefined/{id}') + public async deletePredefinedLightsEffect( + id: number, + @Request() req: ExpressRequest, + ): Promise { + await new SetEffectsService().deletePredefinedEffect(id); + + logger.audit(req.user, `Delete predefined effect with id "${id}"`); + } } diff --git a/src/modules/handlers/lights/set-effects-service.ts b/src/modules/handlers/lights/set-effects-service.ts new file mode 100644 index 0000000..cc901c2 --- /dev/null +++ b/src/modules/handlers/lights/set-effects-service.ts @@ -0,0 +1,99 @@ +import { Repository } from 'typeorm'; +import LightsPredefinedEffect, { + LightsPredefinedEffectProperties, +} from '../../lights/entities/scenes/lights-predefined-effect'; +import dataSource from '../../../database'; +import { HttpApiException } from '../../../helpers/custom-error'; +import { HttpStatusCode } from 'axios'; + +export interface LightsPredefinedEffectResponse { + id: number; + createdAt: string; + updatedAt: string; + buttonId: number; + properties: LightsPredefinedEffectProperties; +} + +export interface LightsPredefinedEffectCreateParams + extends Pick {} + +export interface LightsPredefinedEffectUpdateParams + extends Partial {} + +export default class SetEffectsService { + private repo: Repository; + + constructor(repo?: Repository) { + this.repo = repo ?? dataSource.getRepository(LightsPredefinedEffect); + } + + public static toLightsEffectPredefinedEffectResponse( + e: LightsPredefinedEffect, + ): LightsPredefinedEffectResponse { + return { + id: e.id, + createdAt: e.createdAt.toISOString(), + updatedAt: e.updatedAt.toISOString(), + buttonId: e.buttonId, + properties: e.properties, + }; + } + + public async getAllPredefinedEffects(): Promise { + return this.repo.find(); + } + + public async getSinglePredefinedEffect({ + id, + buttonId, + }: { + id?: number; + buttonId?: number; + }): Promise { + return this.repo.findOne({ where: { id, buttonId } }); + } + + public async createPredefinedEffect( + params: LightsPredefinedEffectCreateParams, + ): Promise { + const existing = await this.getSinglePredefinedEffect({ buttonId: params.buttonId }); + if (existing) { + throw new HttpApiException( + HttpStatusCode.BadRequest, + `Effect with button ID "${params.buttonId}" already exists.`, + ); + } + + return this.repo.save(params); + } + + public async updatePredefinedEffect( + id: number, + params: LightsPredefinedEffectUpdateParams, + ): Promise { + const existing = await this.getSinglePredefinedEffect({ id }); + if (!existing) { + throw new HttpApiException(HttpStatusCode.NotFound, `Effect with ID "${id}" not found.`); + } + + // New button ID + if (params.buttonId !== undefined && existing.buttonId !== params.buttonId) { + const buttonMatch = await this.getSinglePredefinedEffect({ buttonId: params.buttonId }); + if (buttonMatch) { + throw new HttpApiException( + HttpStatusCode.BadRequest, + `Effect with button ID "${params.buttonId}" already exists."`, + ); + } + existing.buttonId = params.buttonId; + } + + if (params.properties) existing.properties = params.properties; + + return this.repo.save(existing); + } + + public async deletePredefinedEffect(id: number): Promise { + await this.repo.delete(id); + } +} diff --git a/src/modules/lights/entities/scenes/lights-predefined-effect.ts b/src/modules/lights/entities/scenes/lights-predefined-effect.ts new file mode 100644 index 0000000..7bf7105 --- /dev/null +++ b/src/modules/lights/entities/scenes/lights-predefined-effect.ts @@ -0,0 +1,55 @@ +import BaseEntity from '../../../root/entities/base-entity'; +import { Column, Entity } from 'typeorm'; +import { LightsEffectsColorCreateParams } from '../../effects/color'; +import { LightsEffectsMovementCreateParams } from '../../effects/movement'; +import { RgbColor } from '../../color-definitions'; + +export type LightsButtonColors = { + type: 'LightsButtonColors'; + colors: RgbColor[]; +}; + +export type LightsButtonEffectColor = { + type: 'LightsButtonEffectColor'; + effectProps: LightsEffectsColorCreateParams; + lightsGroupIds: number[]; +}; + +export type LightsButtonEffectMovement = { + type: 'LightsButtonEffectMovement'; + effectProps: LightsEffectsMovementCreateParams; + lightsGroupIds: number[]; +}; + +export type LightsButtonSwitch = { + type: 'LightsButtonSwitch'; + switchId: number; +}; + +export type LightsPredefinedEffectProperties = + | LightsButtonColors + | LightsButtonEffectColor + | LightsButtonEffectMovement + | LightsButtonSwitch; + +/** + * Button on the effect controller board, applying an effect, color, or switch + */ +@Entity() +export default class LightsPredefinedEffect extends BaseEntity { + @Column({ type: 'int', unsigned: true, unique: true }) + buttonId: number; + + @Column({ + type: 'varchar', + transformer: { + to(value: LightsPredefinedEffectProperties): string { + return JSON.stringify(value); + }, + from(value: string): LightsPredefinedEffectProperties { + return JSON.parse(value); + }, + }, + }) + properties: LightsPredefinedEffectProperties; +} diff --git a/src/modules/root/lights-switch-manager.ts b/src/modules/root/lights-switch-manager.ts index b30bfd1..e00e4f2 100644 --- a/src/modules/root/lights-switch-manager.ts +++ b/src/modules/root/lights-switch-manager.ts @@ -44,4 +44,12 @@ export default class LightsSwitchManager { public getEnabledSwitches(): LightsSwitch[] { return this.enabledSwitches; } + + /** + * Returns whether the given switch is enabled or disabled + * @param lightsSwitch + */ + public switchEnabled(lightsSwitch: LightsSwitch): boolean { + return this.enabledSwitches.some((s) => s.id === lightsSwitch.id); + } } diff --git a/src/modules/root/root-lights-controller.ts b/src/modules/root/root-lights-controller.ts index e1f2bf6..8ddd6b4 100644 --- a/src/modules/root/root-lights-controller.ts +++ b/src/modules/root/root-lights-controller.ts @@ -1,4 +1,4 @@ -import { Body, Get, Post, Request, Route, Security, Tags } from 'tsoa'; +import { Body, Get, Post, Query, Request, Route, Security, Tags } from 'tsoa'; import { Controller } from '@tsoa/runtime'; import RootLightsService, { LightsControllerCreateParams, @@ -62,6 +62,13 @@ export class RootLightsController extends Controller { return new RootLightsService().createController(params); } + @Security(SecurityNames.LOCAL, securityGroups.light.base) + @Get('switch') + public async getAllLightsSwitches(@Query() enabled?: boolean): Promise { + const switches = await new RootLightsService().getAllLightsSwitches(undefined, enabled); + return switches.map((s) => RootLightsService.toLightsSwitchResponse(s)); + } + @Security(SecurityNames.LOCAL, securityGroups.light.base) @Get('group') public async getLightsGroups(): Promise { diff --git a/src/modules/root/root-lights-service.ts b/src/modules/root/root-lights-service.ts index c0e2020..c236fbe 100644 --- a/src/modules/root/root-lights-service.ts +++ b/src/modules/root/root-lights-service.ts @@ -613,13 +613,27 @@ export default class RootLightsService { return movingHead; } - public async getAllLightsSwitches(controllerId?: number): Promise { + public async getAllLightsSwitches( + controllerId?: number, + enabled?: boolean, + ): Promise { let whereClause: FindOptionsWhere = {}; if (controllerId) { whereClause = { controller: { id: controllerId } }; } - return dataSource.getRepository(LightsSwitch).find({ where: whereClause }); + let switches = await dataSource.getRepository(LightsSwitch).find({ where: whereClause }); + if (enabled != null) { + const manager = LightsSwitchManager.getInstance(); + switches = switches.filter((s) => { + const isEnabled = manager.switchEnabled(s); + if (enabled && isEnabled) return true; + if (!enabled && !isEnabled) return true; + return false; + }); + } + + return switches; } public async createLightsSwitch( From 1f017227b3fead57716eab25a60b06ae40a42e8a Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Sun, 2 Feb 2025 18:18:32 +0100 Subject: [PATCH 4/7] fix(effects-controller): small issues and mistakes --- .../handlers/lights/set-effects-service.ts | 8 ++++++- src/modules/lights/entities/index.ts | 3 ++- src/modules/lights/entities/scenes/index.ts | 1 + .../scenes/lights-predefined-effect.ts | 21 +++++++++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/modules/handlers/lights/set-effects-service.ts b/src/modules/handlers/lights/set-effects-service.ts index cc901c2..f27c239 100644 --- a/src/modules/handlers/lights/set-effects-service.ts +++ b/src/modules/handlers/lights/set-effects-service.ts @@ -11,11 +11,13 @@ export interface LightsPredefinedEffectResponse { createdAt: string; updatedAt: string; buttonId: number; + icon?: string; + name?: string; properties: LightsPredefinedEffectProperties; } export interface LightsPredefinedEffectCreateParams - extends Pick {} + extends Pick {} export interface LightsPredefinedEffectUpdateParams extends Partial {} @@ -35,6 +37,8 @@ export default class SetEffectsService { createdAt: e.createdAt.toISOString(), updatedAt: e.updatedAt.toISOString(), buttonId: e.buttonId, + name: e.name, + icon: e.icon, properties: e.properties, }; } @@ -89,6 +93,8 @@ export default class SetEffectsService { } if (params.properties) existing.properties = params.properties; + if (params.name) existing.name = params.name; + if (params.icon) existing.icon = params.icon; return this.repo.save(existing); } diff --git a/src/modules/lights/entities/index.ts b/src/modules/lights/entities/index.ts index db351ef..d4d9c6c 100644 --- a/src/modules/lights/entities/index.ts +++ b/src/modules/lights/entities/index.ts @@ -8,7 +8,7 @@ import LightsGroupMovingHeadRgbs from './lights-group-moving-head-rgbs'; import LightsGroupMovingHeadWheels from './lights-group-moving-head-wheels'; import LightsWheelColorChannelValue from './lights-wheel-color-channel-value'; import LightsWheelGoboChannelValue from './lights-wheel-gobo-channel-value'; -import { LightsScene, LightsSceneEffect } from './scenes'; +import { LightsPredefinedEffect, LightsScene, LightsSceneEffect } from './scenes'; import { LightsTrackEffect } from './sequences/lights-track-effect'; import LightsParShutterOptions from './lights-par-shutter-options'; import LightsMovingHeadRgbShutterOptions from './lights-moving-head-rgb-shutter-options'; @@ -42,5 +42,6 @@ export const Entities = [ LightsSwitch, LightsScene, LightsSceneEffect, + LightsPredefinedEffect, LightsTrackEffect, ]; diff --git a/src/modules/lights/entities/scenes/index.ts b/src/modules/lights/entities/scenes/index.ts index 432b235..512c1a9 100644 --- a/src/modules/lights/entities/scenes/index.ts +++ b/src/modules/lights/entities/scenes/index.ts @@ -1,3 +1,4 @@ // eslint-disable-next-line import/no-cycle -- cross reference export { default as LightsScene } from './lights-scene'; export { default as LightsSceneEffect } from './lights-scene-effect'; +export { default as LightsPredefinedEffect } from './lights-predefined-effect'; diff --git a/src/modules/lights/entities/scenes/lights-predefined-effect.ts b/src/modules/lights/entities/scenes/lights-predefined-effect.ts index 7bf7105..5e2044d 100644 --- a/src/modules/lights/entities/scenes/lights-predefined-effect.ts +++ b/src/modules/lights/entities/scenes/lights-predefined-effect.ts @@ -23,14 +23,25 @@ export type LightsButtonEffectMovement = { export type LightsButtonSwitch = { type: 'LightsButtonSwitch'; - switchId: number; + switchIds: number[]; +}; + +export type LightsButtonStrobe = { + type: 'LightsButtonStrobe'; + lightsGroupIds: number[]; +}; + +export type LightsButtonNull = { + type: 'LightsButtonNull'; }; export type LightsPredefinedEffectProperties = | LightsButtonColors | LightsButtonEffectColor | LightsButtonEffectMovement - | LightsButtonSwitch; + | LightsButtonSwitch + | LightsButtonStrobe + | LightsButtonNull; /** * Button on the effect controller board, applying an effect, color, or switch @@ -52,4 +63,10 @@ export default class LightsPredefinedEffect extends BaseEntity { }, }) properties: LightsPredefinedEffectProperties; + + @Column({ nullable: true }) + icon?: string; + + @Column({ nullable: true }) + name?: string; } From 062b94f1598ca3995438d8c02aaf8b7b092802aa Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Sun, 2 Feb 2025 18:25:02 +0100 Subject: [PATCH 5/7] feat(lights-controller): add reset button --- .../lights/entities/scenes/lights-predefined-effect.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/modules/lights/entities/scenes/lights-predefined-effect.ts b/src/modules/lights/entities/scenes/lights-predefined-effect.ts index 5e2044d..e97a428 100644 --- a/src/modules/lights/entities/scenes/lights-predefined-effect.ts +++ b/src/modules/lights/entities/scenes/lights-predefined-effect.ts @@ -21,6 +21,11 @@ export type LightsButtonEffectMovement = { lightsGroupIds: number[]; }; +export type LightsButtonReset = { + type: 'LightsButtonReset'; + lightsGroupIds: number[]; +}; + export type LightsButtonSwitch = { type: 'LightsButtonSwitch'; switchIds: number[]; @@ -39,6 +44,7 @@ export type LightsPredefinedEffectProperties = | LightsButtonColors | LightsButtonEffectColor | LightsButtonEffectMovement + | LightsButtonReset | LightsButtonSwitch | LightsButtonStrobe | LightsButtonNull; From 40454aa8286ad1fef7651233518669c8c0dd35b8 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Mon, 3 Feb 2025 09:31:14 +0100 Subject: [PATCH 6/7] feat(lights-effects): allow changing colors mid-effect --- .../handlers/lights/effects-handler.ts | 30 +++++++++++++ .../handlers/lights/set-effects-controller.ts | 45 ++++++++++++++++++- .../handlers/lights/set-effects-handler.ts | 1 + .../handlers/lights/set-effects-service.ts | 4 +- .../lights/effects/color/beat-fade-out.ts | 5 +++ src/modules/lights/effects/color/sparkle.ts | 5 +++ .../lights/effects/color/static-color.ts | 4 ++ src/modules/lights/effects/color/wave.ts | 5 +++ src/modules/lights/effects/lights-effect.ts | 8 ++++ .../lights/entities/lights-moving-head-rgb.ts | 2 +- .../entities/lights-moving-head-wheel.ts | 2 +- src/modules/lights/entities/lights-par.ts | 2 +- .../scenes/lights-predefined-effect.ts | 12 +++-- 13 files changed, 114 insertions(+), 11 deletions(-) diff --git a/src/modules/handlers/lights/effects-handler.ts b/src/modules/handlers/lights/effects-handler.ts index b682eea..3b9c5c7 100644 --- a/src/modules/handlers/lights/effects-handler.ts +++ b/src/modules/handlers/lights/effects-handler.ts @@ -2,6 +2,7 @@ import BaseLightsHandler from '../base-lights-handler'; import { LightsGroup } from '../../lights/entities'; import LightsEffect from '../../lights/effects/lights-effect'; import { BeatEvent } from '../../events/music-emitter-events'; +import { RgbColor } from '../../lights/color-definitions'; export type GroupEffectsMap = Map; @@ -112,4 +113,33 @@ export default abstract class EffectsHandler extends BaseLightsHandler { } }); } + + /** + * Change the color of the given lights group's effects + * @param entityCopy + * @param colors + */ + updateColors(entityCopy: LightsGroup, colors: RgbColor[]): void { + const entity: LightsGroup | undefined = this.entities.find((e) => e.id === entityCopy.id); + if (!entity) return; + + const effect = this.groupColorEffects.get(entity); + if (Array.isArray(effect)) { + effect.forEach((e) => e.setColors(colors)); + } else { + effect?.setColors(colors); + } + } + + /** + * Change the color of the effects of the lights group with the given ID + * @param id + * @param colors + */ + updateColorsById(id: number, colors: RgbColor[]): void { + const entity: LightsGroup | undefined = this.entities.find((e) => e.id === id); + if (!entity) return; + + this.updateColors(entity, colors); + } } diff --git a/src/modules/handlers/lights/set-effects-controller.ts b/src/modules/handlers/lights/set-effects-controller.ts index 078ed34..3abcf25 100644 --- a/src/modules/handlers/lights/set-effects-controller.ts +++ b/src/modules/handlers/lights/set-effects-controller.ts @@ -1,5 +1,5 @@ -import { Controller, Patch } from '@tsoa/runtime'; -import { Body, Delete, Get, Post, Request, Route, Security, Tags } from 'tsoa'; +import { Controller, Patch, TsoaResponse } from '@tsoa/runtime'; +import { Body, Delete, Get, Post, Request, Res, Route, Security, Tags } from 'tsoa'; import { Request as ExpressRequest } from 'express'; import SetEffectsHandler from './set-effects-handler'; import HandlerManager from '../../root/handler-manager'; @@ -14,6 +14,12 @@ import SetEffectsService, { LightsPredefinedEffectResponse, LightsPredefinedEffectUpdateParams, } from './set-effects-service'; +import { RgbColor } from '../../lights/color-definitions'; +import { HttpStatusCode } from 'axios'; + +interface ColorsRequest { + colors: RgbColor[]; +} @Route('handler/lights/set-effects') @Tags('Handlers') @@ -55,6 +61,41 @@ export class SetEffectsController extends Controller { return { message: 'success' }; } + /** + * Change the colors of the given lights group's color effects + * @param id + * @param req + * @param colors + * @param notFoundResponse + */ + @Security(SecurityNames.LOCAL, securityGroups.effects.base) + @Post('{id}/color/colors') + public async updateLightsEffectColorColors( + id: number, + @Request() req: ExpressRequest, + @Body() colors: ColorsRequest, + @Res() notFoundResponse: TsoaResponse, + ) { + const handler: SetEffectsHandler | undefined = HandlerManager.getInstance() + .getHandlers(LightsGroup) + .find((h) => h.constructor.name === SetEffectsHandler.name) as SetEffectsHandler | undefined; + if (!handler) throw new Error('SetEffectsHandler not found'); + + const lightsGroup = handler.entities.find((e) => e.id === id); + if (lightsGroup === undefined) { + return notFoundResponse(HttpStatusCode.NotFound, { + message: 'LightsGroup not found in SetEffectsHandler', + }); + } + + logger.audit( + req.user, + `Change colors of lights group "${lightsGroup?.name}"'s effects (id: ${id}).`, + ); + + handler.updateColors(lightsGroup, colors.colors); + } + /** * Given a list of movement effects to create, add the given effects to the lightsgroup with the * given ID. Remove all movement effects if an empty array is given diff --git a/src/modules/handlers/lights/set-effects-handler.ts b/src/modules/handlers/lights/set-effects-handler.ts index be85ebd..bc4a22e 100644 --- a/src/modules/handlers/lights/set-effects-handler.ts +++ b/src/modules/handlers/lights/set-effects-handler.ts @@ -4,6 +4,7 @@ import { LightsEffectBuilder } from '../../lights/effects/lights-effect'; import { LIGHTS_EFFECTS, LightsEffectsCreateParams } from '../../lights/effects'; import { LightsEffectsColorCreateParams } from '../../lights/effects/color'; import { LightsEffectsMovementCreateParams } from '../../lights/effects/movement'; +import { RgbColor } from '../../lights/color-definitions'; export default class SetEffectsHandler extends EffectsHandler { /** diff --git a/src/modules/handlers/lights/set-effects-service.ts b/src/modules/handlers/lights/set-effects-service.ts index f27c239..559a9ef 100644 --- a/src/modules/handlers/lights/set-effects-service.ts +++ b/src/modules/handlers/lights/set-effects-service.ts @@ -11,8 +11,8 @@ export interface LightsPredefinedEffectResponse { createdAt: string; updatedAt: string; buttonId: number; - icon?: string; - name?: string; + icon?: string | null; + name?: string | null; properties: LightsPredefinedEffectProperties; } diff --git a/src/modules/lights/effects/color/beat-fade-out.ts b/src/modules/lights/effects/color/beat-fade-out.ts index 515dddd..cbc2922 100644 --- a/src/modules/lights/effects/color/beat-fade-out.ts +++ b/src/modules/lights/effects/color/beat-fade-out.ts @@ -19,6 +19,7 @@ import { import EffectProgressionStrategy from '../progression-strategies/effect-progression-strategy'; import LightsGroupFixture from '../../entities/lights-group-fixture'; import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory'; +import { RgbColor } from '../../color-definitions'; export interface BeatFadeOutProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps { /** @@ -92,6 +93,10 @@ export default class BeatFadeOut extends LightsEffect { return (lightsGroup: LightsGroup) => new BeatFadeOut(lightsGroup, props); } + setColors(colors: RgbColor[]) { + this.props.colors = colors; + } + destroy(): void {} beat(event: BeatEvent): void { diff --git a/src/modules/lights/effects/color/sparkle.ts b/src/modules/lights/effects/color/sparkle.ts index 5cfc5ed..c9fd7fd 100644 --- a/src/modules/lights/effects/color/sparkle.ts +++ b/src/modules/lights/effects/color/sparkle.ts @@ -5,6 +5,7 @@ import LightsEffect, { } from '../lights-effect'; import { LightsGroup } from '../../entities'; import { ColorEffects } from './color-effects'; +import { RgbColor } from '../../color-definitions'; export interface SparkleProps extends BaseLightsEffectProps { /** @@ -64,6 +65,10 @@ export default class Sparkle extends LightsEffect { return (lightsGroup: LightsGroup) => new Sparkle(lightsGroup, props); } + setColors(colors: RgbColor[]) { + this.props.colors = colors; + } + /** * Enable a subset of lights, i.e. set their brightness back to 1 * @private diff --git a/src/modules/lights/effects/color/static-color.ts b/src/modules/lights/effects/color/static-color.ts index 04e867f..c287be6 100644 --- a/src/modules/lights/effects/color/static-color.ts +++ b/src/modules/lights/effects/color/static-color.ts @@ -77,6 +77,10 @@ export default class StaticColor extends LightsEffect { return (lightsGroup) => new StaticColor(lightsGroup, props); } + setColors(colors: RgbColor[]) { + this.props.color = colors[0]; + } + beat(): void { if (!this.props.beatToggle) return; diff --git a/src/modules/lights/effects/color/wave.ts b/src/modules/lights/effects/color/wave.ts index c72901c..c7752ec 100644 --- a/src/modules/lights/effects/color/wave.ts +++ b/src/modules/lights/effects/color/wave.ts @@ -8,6 +8,7 @@ import { LightsGroup, LightsGroupMovingHeadRgbs, LightsGroupPars } from '../../e import { ColorEffects } from './color-effects'; import { EffectProgressionTickStrategy } from '../progression-strategies'; import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory'; +import { RgbColor } from '../../color-definitions'; export interface WaveProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps { /** @@ -56,6 +57,10 @@ export default class Wave extends LightsEffect { return (lightsGroup) => new Wave(lightsGroup, props); } + setColors(colors: RgbColor[]) { + this.props.colors = colors; + } + destroy(): void {} beat(): void {} diff --git a/src/modules/lights/effects/lights-effect.ts b/src/modules/lights/effects/lights-effect.ts index ac16c44..10322ed 100644 --- a/src/modules/lights/effects/lights-effect.ts +++ b/src/modules/lights/effects/lights-effect.ts @@ -71,6 +71,14 @@ export default abstract class LightsEffect

{ return this.progressionMapperStrategy.getProgression(progression, fixture); } + /** + * Update the colors of the effect, if applicable. + * Intended to be overriden by any effect that + * uses colors + * @param colors + */ + public setColors(colors: RgbColor[]): void {} + /** * Clean up effect when it is destroyed */ diff --git a/src/modules/lights/entities/lights-moving-head-rgb.ts b/src/modules/lights/entities/lights-moving-head-rgb.ts index 3676556..79c7bb3 100644 --- a/src/modules/lights/entities/lights-moving-head-rgb.ts +++ b/src/modules/lights/entities/lights-moving-head-rgb.ts @@ -13,7 +13,7 @@ export default class LightsMovingHeadRgb extends LightsMovingHead { @Column(() => ColorsRgb) public color: ColorsRgb; - public setColor(color: RgbColor) { + public setColor(color?: RgbColor) { this.valuesUpdatedAt = new Date(); this.color.setColor(color); } diff --git a/src/modules/lights/entities/lights-moving-head-wheel.ts b/src/modules/lights/entities/lights-moving-head-wheel.ts index 1af0121..7f3014c 100644 --- a/src/modules/lights/entities/lights-moving-head-wheel.ts +++ b/src/modules/lights/entities/lights-moving-head-wheel.ts @@ -12,7 +12,7 @@ export default class LightsMovingHeadWheel extends LightsMovingHead { @Column(() => ColorsWheel) public wheel: ColorsWheel; - public setColor(color: RgbColor) { + public setColor(color?: RgbColor) { this.valuesUpdatedAt = new Date(); this.wheel.setColor(color); } diff --git a/src/modules/lights/entities/lights-par.ts b/src/modules/lights/entities/lights-par.ts index b6f042f..5146e99 100644 --- a/src/modules/lights/entities/lights-par.ts +++ b/src/modules/lights/entities/lights-par.ts @@ -13,7 +13,7 @@ export default class LightsPar extends LightsFixture { @Column(() => ColorsRgb) public color: ColorsRgb; - public setColor(color: RgbColor) { + public setColor(color?: RgbColor) { this.valuesUpdatedAt = new Date(); this.color.setColor(color); } diff --git a/src/modules/lights/entities/scenes/lights-predefined-effect.ts b/src/modules/lights/entities/scenes/lights-predefined-effect.ts index e97a428..d4b735c 100644 --- a/src/modules/lights/entities/scenes/lights-predefined-effect.ts +++ b/src/modules/lights/entities/scenes/lights-predefined-effect.ts @@ -7,6 +7,10 @@ import { RgbColor } from '../../color-definitions'; export type LightsButtonColors = { type: 'LightsButtonColors'; colors: RgbColor[]; + /** + * Lights groups to which these colors should be immediately applied to + */ + lightsGroupIds?: number[]; }; export type LightsButtonEffectColor = { @@ -70,9 +74,9 @@ export default class LightsPredefinedEffect extends BaseEntity { }) properties: LightsPredefinedEffectProperties; - @Column({ nullable: true }) - icon?: string; + @Column({ nullable: true, type: 'varchar' }) + icon?: string | null; - @Column({ nullable: true }) - name?: string; + @Column({ nullable: true, type: 'varchar' }) + name?: string | null; } From f579913a4c7013d6e0c0e4cc1cae997d6c70fd72 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Mon, 3 Feb 2025 09:32:49 +0100 Subject: [PATCH 7/7] fix(lights-effects): deadlock when passing undefined as color --- src/modules/lights/entities/colors-rgb.ts | 15 +++++++++++++-- src/modules/lights/entities/colors-wheel.ts | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/modules/lights/entities/colors-rgb.ts b/src/modules/lights/entities/colors-rgb.ts index b031c4c..fa5e76b 100644 --- a/src/modules/lights/entities/colors-rgb.ts +++ b/src/modules/lights/entities/colors-rgb.ts @@ -3,6 +3,7 @@ import { RgbColor, rgbColorDefinitions } from '../color-definitions'; import LightsFixtureShutterOptions, { ShutterOption } from './lights-fixture-shutter-options'; import Colors from './colors'; import { IColorsWheel } from './colors-wheel'; +import logger from '../../../logger'; export type ColorChannel = keyof ColorsRgb; @@ -56,8 +57,18 @@ export default class ColorsRgb extends Colors implements IColorsRgb { uvChannel: 0, }; - public setColor(color: RgbColor): void { - this.currentValues = rgbColorDefinitions[color].definition; + public setColor(color?: RgbColor): void { + if (!color) { + this.reset(); + return; + } + const spec = rgbColorDefinitions[color]; + if (!spec) { + logger.error(`Color "${color}" not found in rgbColorDefinitions.`); + this.reset(); + return; + } + this.currentValues = spec.definition; } public setCustomColor(color: IColorsRgb): void { diff --git a/src/modules/lights/entities/colors-wheel.ts b/src/modules/lights/entities/colors-wheel.ts index fe0d8c5..c48da5b 100644 --- a/src/modules/lights/entities/colors-wheel.ts +++ b/src/modules/lights/entities/colors-wheel.ts @@ -6,6 +6,7 @@ import LightsWheelRotateChannelValue from './lights-wheel-rotate-channel-value'; import { RgbColor, rgbColorDefinitions, WheelColor } from '../color-definitions'; import Colors from './colors'; import LightsFixtureShutterOptions, { ShutterOption } from './lights-fixture-shutter-options'; +import logger from '../../../logger'; export interface IColorsWheel { colorChannel: number; @@ -46,8 +47,19 @@ export default class ColorsWheel extends Colors implements IColorsWheel { goboRotateChannel: 0, }; - public setColor(color: RgbColor): void { - const wheelColor = rgbColorDefinitions[color].alternative; + public setColor(color?: RgbColor): void { + if (!color) { + this.reset(); + return; + } + const spec = rgbColorDefinitions[color]; + if (!spec) { + logger.error(`Color "${color}" not found in rgbColorDefinitions.`); + this.reset(); + return; + } + + const wheelColor = spec.alternative; const channelValueObj = this.colorChannelValues.find((v) => v.name === wheelColor); this.currentValues = { ...this.currentValues,