Skip to content

Commit

Permalink
feat(effects-controller): add support for simple effects controller w…
Browse files Browse the repository at this point in the history
…ith preconfigured buttons (#40)

* chore(lights): make pattern enum string-based

* chore(lights): rename predefined effect to track effect

* feat(effects-controller): add endpoints for effect controller buttons

* fix(effects-controller): small issues and mistakes

* feat(lights-controller): add reset button

* feat(lights-effects): allow changing colors mid-effect

* fix(lights-effects): deadlock when passing undefined as color
  • Loading branch information
Yoronex committed Feb 5, 2025
1 parent 455acea commit aaac975
Show file tree
Hide file tree
Showing 25 changed files with 427 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/helpers/security-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const securityGroups = {
},
effects: {
base: baseSecurityGroups,
privileged: [SecurityGroup.ADMIN],
},
poster: {
base: allSecuritySubscriberGroups,
Expand Down
6 changes: 3 additions & 3 deletions src/modules/handlers/lights/effect-sequence-handler.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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 },
Expand Down
30 changes: 30 additions & 0 deletions src/modules/handlers/lights/effects-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LightsGroup, LightsEffect | LightsEffect[] | null>;

Expand Down Expand Up @@ -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);
}
}
101 changes: 99 additions & 2 deletions src/modules/handlers/lights/set-effects-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from '@tsoa/runtime';
import { Body, 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';
Expand All @@ -9,6 +9,17 @@ 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';
import { RgbColor } from '../../lights/color-definitions';
import { HttpStatusCode } from 'axios';

interface ColorsRequest {
colors: RgbColor[];
}

@Route('handler/lights/set-effects')
@Tags('Handlers')
Expand Down Expand Up @@ -50,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<HttpStatusCode.NotFound, { message: string }>,
) {
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
Expand Down Expand Up @@ -86,4 +132,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<LightsPredefinedEffectResponse[]> {
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<LightsPredefinedEffectResponse> {
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<LightsPredefinedEffectResponse> {
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<void> {
await new SetEffectsService().deletePredefinedEffect(id);

logger.audit(req.user, `Delete predefined effect with id "${id}"`);
}
}
1 change: 1 addition & 0 deletions src/modules/handlers/lights/set-effects-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down
105 changes: 105 additions & 0 deletions src/modules/handlers/lights/set-effects-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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;
icon?: string | null;
name?: string | null;
properties: LightsPredefinedEffectProperties;
}

export interface LightsPredefinedEffectCreateParams
extends Pick<LightsPredefinedEffect, 'buttonId' | 'properties' | 'icon' | 'name'> {}

export interface LightsPredefinedEffectUpdateParams
extends Partial<LightsPredefinedEffectCreateParams> {}

export default class SetEffectsService {
private repo: Repository<LightsPredefinedEffect>;

constructor(repo?: Repository<LightsPredefinedEffect>) {
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,
name: e.name,
icon: e.icon,
properties: e.properties,
};
}

public async getAllPredefinedEffects(): Promise<LightsPredefinedEffect[]> {
return this.repo.find();
}

public async getSinglePredefinedEffect({
id,
buttonId,
}: {
id?: number;
buttonId?: number;
}): Promise<LightsPredefinedEffect | null> {
return this.repo.findOne({ where: { id, buttonId } });
}

public async createPredefinedEffect(
params: LightsPredefinedEffectCreateParams,
): Promise<LightsPredefinedEffect> {
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<LightsPredefinedEffect> {
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;
if (params.name) existing.name = params.name;
if (params.icon) existing.icon = params.icon;

return this.repo.save(existing);
}

public async deletePredefinedEffect(id: number): Promise<void> {
await this.repo.delete(id);
}
}
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/beat-fade-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -92,6 +93,10 @@ export default class BeatFadeOut extends LightsEffect<BeatFadeOutProps> {
return (lightsGroup: LightsGroup) => new BeatFadeOut(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.colors = colors;
}

destroy(): void {}

beat(event: BeatEvent): void {
Expand Down
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/sparkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -64,6 +65,10 @@ export default class Sparkle extends LightsEffect<SparkleProps> {
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
Expand Down
4 changes: 4 additions & 0 deletions src/modules/lights/effects/color/static-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export default class StaticColor extends LightsEffect<StaticColorProps> {
return (lightsGroup) => new StaticColor(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.color = colors[0];
}

beat(): void {
if (!this.props.beatToggle) return;

Expand Down
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/wave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -56,6 +57,10 @@ export default class Wave extends LightsEffect<WaveProps> {
return (lightsGroup) => new Wave(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.colors = colors;
}

destroy(): void {}

beat(): void {}
Expand Down
Loading

0 comments on commit aaac975

Please sign in to comment.