mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 06:04:05 +00:00
feat: implement multiple aux timers
This commit is contained in:
committed by
Carlos Valente
parent
d1c58712ae
commit
bf8ed8d017
@@ -149,10 +149,10 @@ describe('parseOutput', () => {
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: 'aux',
|
||||
secondarySource: 'aux1',
|
||||
}),
|
||||
).toMatchObject({
|
||||
secondarySource: 'aux',
|
||||
secondarySource: 'aux1',
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
|
||||
@@ -189,13 +189,17 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
|
||||
|
||||
// we know we have a valid action, deal with special cases
|
||||
|
||||
if (maybeOntimeAction.action === 'aux-set') {
|
||||
if (
|
||||
maybeOntimeAction.action === 'aux1-set' ||
|
||||
maybeOntimeAction.action === 'aux2-set' ||
|
||||
maybeOntimeAction.action === 'aux3-set'
|
||||
) {
|
||||
assert.hasKeys(maybeOntimeAction, ['time']);
|
||||
assert.isString(maybeOntimeAction.time);
|
||||
|
||||
return {
|
||||
type: 'ontime',
|
||||
action: 'aux-set',
|
||||
action: maybeOntimeAction.action,
|
||||
time: parseUserTime(maybeOntimeAction.time),
|
||||
};
|
||||
}
|
||||
@@ -253,7 +257,9 @@ function indeterminateBooleanString(value: string): boolean | undefined {
|
||||
* Helper function to validate the secondary source
|
||||
*/
|
||||
function chooseSecondarySource(value: string): SecondarySource {
|
||||
if (value === 'aux') return 'aux';
|
||||
if (value === 'aux1') return 'aux1';
|
||||
if (value === 'aux2') return 'aux2';
|
||||
if (value === 'aux3') return 'aux3';
|
||||
if (value === 'secondary') return 'secondary';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,32 @@ export function toOntimeAction(action: OntimeAction) {
|
||||
const actionType = action.action;
|
||||
switch (actionType) {
|
||||
// Aux timer actions
|
||||
case 'aux-start':
|
||||
auxTimerService.start();
|
||||
break;
|
||||
case 'aux-stop':
|
||||
auxTimerService.stop();
|
||||
break;
|
||||
case 'aux-pause':
|
||||
auxTimerService.pause();
|
||||
break;
|
||||
case 'aux-set': {
|
||||
auxTimerService.setTime(action.time);
|
||||
break;
|
||||
case 'aux1-start':
|
||||
return auxTimerService.start(1);
|
||||
case 'aux1-stop':
|
||||
return auxTimerService.stop(1);
|
||||
case 'aux1-pause':
|
||||
return auxTimerService.pause(1);
|
||||
case 'aux1-set': {
|
||||
return auxTimerService.setTime(action.time, 1);
|
||||
}
|
||||
case 'aux2-start':
|
||||
return auxTimerService.start(2);
|
||||
case 'aux2-stop':
|
||||
return auxTimerService.stop(2);
|
||||
case 'aux2-pause':
|
||||
return auxTimerService.pause(2);
|
||||
case 'aux2-set': {
|
||||
return auxTimerService.setTime(action.time, 2);
|
||||
}
|
||||
case 'aux3-start':
|
||||
return auxTimerService.start(3);
|
||||
case 'aux3-stop':
|
||||
return auxTimerService.stop(3);
|
||||
case 'aux3-pause':
|
||||
return auxTimerService.pause(3);
|
||||
case 'aux3-set': {
|
||||
return auxTimerService.setTime(action.time, 3);
|
||||
}
|
||||
|
||||
// Message actions
|
||||
|
||||
@@ -18,7 +18,6 @@ import { validateMessage, validateTimerMessage } from '../services/message-servi
|
||||
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import * as assert from '../utils/assert.js';
|
||||
import { isEmptyObject } from '../utils/parserUtils.js';
|
||||
import { parseProperty } from './integration.utils.js';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
import { throttle } from '../utils/throttle.js';
|
||||
@@ -218,45 +217,61 @@ const actionHandlers: Record<ApiAction, ActionHandler> = {
|
||||
runtimeService.addTime(time);
|
||||
return { payload: 'success' };
|
||||
},
|
||||
/* Extra timers */
|
||||
/**
|
||||
* Auxiliary timers, payload can be either:
|
||||
*
|
||||
* 1. a simple playback command
|
||||
* {
|
||||
* "1": "start" | "pause" | "stop"
|
||||
* }
|
||||
*
|
||||
* - or -
|
||||
*
|
||||
* 2. a patch object with properties
|
||||
* {
|
||||
* "1": {
|
||||
* duration: "count-down"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
auxtimer: (payload) => {
|
||||
assert.isObject(payload);
|
||||
if (!('1' in payload)) {
|
||||
const timerIndex = Object.keys(payload).at(0);
|
||||
if (timerIndex !== '1' && timerIndex !== '2' && timerIndex !== '3') {
|
||||
throw new Error('Invalid auxtimer index');
|
||||
}
|
||||
const command = payload['1'];
|
||||
|
||||
const command = payload[timerIndex as keyof typeof payload] as unknown;
|
||||
const index = Number(timerIndex);
|
||||
|
||||
// 1. handle simple playback commands: start, pause, stop
|
||||
if (typeof command === 'string') {
|
||||
if (command === SimplePlayback.Start) {
|
||||
const reply = auxTimerService.start();
|
||||
return { payload: reply };
|
||||
switch (command) {
|
||||
case SimplePlayback.Start:
|
||||
return { payload: auxTimerService.start(index) };
|
||||
case SimplePlayback.Pause:
|
||||
return { payload: auxTimerService.pause(index) };
|
||||
case SimplePlayback.Stop:
|
||||
return { payload: auxTimerService.stop(index) };
|
||||
default:
|
||||
throw new Error('Invalid command');
|
||||
}
|
||||
if (command === SimplePlayback.Pause) {
|
||||
const reply = auxTimerService.pause();
|
||||
return { payload: reply };
|
||||
}
|
||||
if (command === SimplePlayback.Stop) {
|
||||
const reply = auxTimerService.stop();
|
||||
return { payload: reply };
|
||||
}
|
||||
} else if (command && typeof command === 'object') {
|
||||
const reply = { payload: {} };
|
||||
}
|
||||
|
||||
// 2. command can be a patch object: duration, addtime, direction
|
||||
if (command && typeof command === 'object') {
|
||||
if ('duration' in command) {
|
||||
const timeInMs = numberOrError(command.duration);
|
||||
reply.payload = auxTimerService.setTime(timeInMs);
|
||||
return { payload: auxTimerService.setTime(numberOrError(command.duration), index) };
|
||||
}
|
||||
if ('addtime' in command) {
|
||||
const timeInMs = numberOrError(command.addtime);
|
||||
reply.payload = auxTimerService.addTime(timeInMs);
|
||||
return { payload: auxTimerService.addTime(numberOrError(command.addtime), index) };
|
||||
}
|
||||
if ('direction' in command) {
|
||||
if (command.direction === SimpleDirection.CountUp || command.direction === SimpleDirection.CountDown) {
|
||||
reply.payload = auxTimerService.setDirection(command.direction);
|
||||
} else {
|
||||
throw new Error('Invalid direction payload');
|
||||
return { payload: auxTimerService.setDirection(command.direction, index) };
|
||||
}
|
||||
}
|
||||
if (!isEmptyObject(reply.payload)) {
|
||||
return reply;
|
||||
throw new Error('Invalid direction payload');
|
||||
}
|
||||
}
|
||||
throw new Error('No matching method provided');
|
||||
|
||||
@@ -193,6 +193,18 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb
|
||||
playback: SimplePlayback.Stop,
|
||||
direction: SimpleDirection.CountDown,
|
||||
},
|
||||
auxtimer2: {
|
||||
duration: timerConfig.auxTimerDefault,
|
||||
current: timerConfig.auxTimerDefault,
|
||||
playback: SimplePlayback.Stop,
|
||||
direction: SimpleDirection.CountDown,
|
||||
},
|
||||
auxtimer3: {
|
||||
duration: timerConfig.auxTimerDefault,
|
||||
current: timerConfig.auxTimerDefault,
|
||||
playback: SimplePlayback.Stop,
|
||||
direction: SimpleDirection.CountDown,
|
||||
},
|
||||
ping: -1,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,91 +1,165 @@
|
||||
import { SimpleDirection, SimplePlayback, SimpleTimerState } from 'ontime-types';
|
||||
import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||
|
||||
import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
import { timerConfig } from '../../setup/config.js';
|
||||
|
||||
type EmitFn = (state: SimpleTimerState) => void;
|
||||
type AuxTimerStateUpdate = Partial<Pick<RuntimeStore, 'auxtimer1' | 'auxtimer2' | 'auxtimer3'>>;
|
||||
type EmitFn = (state: AuxTimerStateUpdate) => void;
|
||||
type GetTimeFn = () => number;
|
||||
|
||||
export class AuxTimerService {
|
||||
private timer: SimpleTimer;
|
||||
private aux1: SimpleTimer;
|
||||
private aux2: SimpleTimer;
|
||||
private aux3: SimpleTimer;
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
private emit: EmitFn;
|
||||
protected emit: EmitFn;
|
||||
private getTime: GetTimeFn;
|
||||
|
||||
constructor(emit: EmitFn, getTime: GetTimeFn) {
|
||||
this.timer = new SimpleTimer(timerConfig.auxTimerDefault);
|
||||
this.aux1 = new SimpleTimer(timerConfig.auxTimerDefault);
|
||||
this.aux2 = new SimpleTimer(timerConfig.auxTimerDefault);
|
||||
this.aux3 = new SimpleTimer(timerConfig.auxTimerDefault);
|
||||
this.emit = emit;
|
||||
this.getTime = getTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any of the aux timers are currently running
|
||||
*/
|
||||
private hasActiveTimers(): boolean {
|
||||
return (
|
||||
this.aux1.state.playback === SimplePlayback.Start ||
|
||||
this.aux2.state.playback === SimplePlayback.Start ||
|
||||
this.aux3.state.playback === SimplePlayback.Start
|
||||
);
|
||||
}
|
||||
|
||||
private startInterval() {
|
||||
this.interval = setInterval(this.update.bind(this), 500);
|
||||
if (!this.interval) {
|
||||
this.interval = setInterval(this.update.bind(this), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility simplifies guarding against multiple intervals being set
|
||||
*/
|
||||
private stopInterval() {
|
||||
if (this.interval) {
|
||||
if (this.interval && !this.hasActiveTimers()) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
setDirection(direction: SimpleDirection) {
|
||||
return this.timer.setDirection(direction, this.getTime());
|
||||
setDirection(direction: SimpleDirection, index: number) {
|
||||
if (index === 1) return this.aux1.setDirection(direction, this.getTime());
|
||||
if (index === 2) return this.aux2.setDirection(direction, this.getTime());
|
||||
return this.aux3.setDirection(direction, this.getTime());
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
start() {
|
||||
start(index: number) {
|
||||
this.startInterval();
|
||||
return this.timer.start(this.getTime());
|
||||
if (index === 1) return this.aux1.start(this.getTime());
|
||||
if (index === 2) return this.aux2.start(this.getTime());
|
||||
return this.aux3.start(this.getTime());
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
pause() {
|
||||
this.stopInterval();
|
||||
return this.timer.pause(this.getTime());
|
||||
}
|
||||
pause(index: number) {
|
||||
// First pause the timer
|
||||
let result;
|
||||
if (index === 1) result = this.aux1.pause(this.getTime());
|
||||
else if (index === 2) result = this.aux2.pause(this.getTime());
|
||||
else result = this.aux3.pause(this.getTime());
|
||||
|
||||
@broadcastReturn
|
||||
stop() {
|
||||
this.stopInterval();
|
||||
return this.timer.stop();
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
setTime(duration: number) {
|
||||
return this.timer.setTime(duration);
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
addTime(millis: number) {
|
||||
if (this.timer.state.playback === SimplePlayback.Start) {
|
||||
this.timer.addTime(millis);
|
||||
return this.timer.update(this.getTime());
|
||||
// Then check if we need to keep the interval running
|
||||
if (!this.hasActiveTimers()) {
|
||||
this.stopInterval();
|
||||
}
|
||||
return this.timer.addTime(millis);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
stop(index: number) {
|
||||
// First stop the timer
|
||||
let result;
|
||||
if (index === 1) result = this.aux1.stop();
|
||||
else if (index === 2) result = this.aux2.stop();
|
||||
else result = this.aux3.stop();
|
||||
|
||||
// Then check if we need to keep the interval running
|
||||
if (!this.hasActiveTimers()) {
|
||||
this.stopInterval();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
setTime(duration: number, index: number) {
|
||||
if (index === 1) return this.aux1.setTime(duration);
|
||||
if (index === 2) return this.aux2.setTime(duration);
|
||||
return this.aux3.setTime(duration);
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
addTime(millis: number, index: number) {
|
||||
const aux = index === 1 ? this.aux1 : index === 2 ? this.aux2 : this.aux3;
|
||||
if (aux.state.playback === SimplePlayback.Start) {
|
||||
aux.addTime(millis);
|
||||
return aux.update(this.getTime());
|
||||
}
|
||||
return aux.addTime(millis);
|
||||
}
|
||||
|
||||
private update() {
|
||||
return this.timer.update(this.getTime());
|
||||
/**
|
||||
* The update function affects any running timers,
|
||||
* so we decide to emit a patch object rather
|
||||
* than using the decorator which would emit individual updates.
|
||||
*/
|
||||
const patch: AuxTimerStateUpdate = {};
|
||||
|
||||
const timeNow = this.getTime();
|
||||
if (this.aux1.state.playback === SimplePlayback.Start) {
|
||||
patch.auxtimer1 = this.aux1.update(timeNow);
|
||||
}
|
||||
|
||||
if (this.aux2.state.playback === SimplePlayback.Start) {
|
||||
patch.auxtimer2 = this.aux2.update(timeNow);
|
||||
}
|
||||
|
||||
if (this.aux3.state.playback === SimplePlayback.Start) {
|
||||
patch.auxtimer3 = this.aux3.update(timeNow);
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
this.emit(patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastReturn(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
function broadcastReturn(_target: object, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
descriptor.value = function (this: AuxTimerService, ...args: unknown[]) {
|
||||
const result = originalMethod.apply(this, args);
|
||||
// @ts-expect-error -- we can access private properties from the decorator
|
||||
(this as AuxTimerService).emit(result);
|
||||
const index = args[args.length - 1] as number;
|
||||
this.emit({ [`auxtimer${index}`]: result });
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
const emit = (state: SimpleTimerState) => eventStore.set('auxtimer1', state);
|
||||
const emit = (state: AuxTimerStateUpdate) => {
|
||||
for (const [key, value] of Object.entries(state)) {
|
||||
eventStore.set(key as keyof RuntimeStore, value);
|
||||
}
|
||||
};
|
||||
const timeNow = () => Date.now();
|
||||
|
||||
export const auxTimerService = new AuxTimerService(emit, timeNow);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function validateTimerMessage(message: unknown): Partial<TimerMessage> {
|
||||
* Asserts that the secondary value is one of the permitted values
|
||||
*/
|
||||
function assertSecondary(source: unknown): source is TimerMessage['secondarySource'] {
|
||||
return source === 'aux' || source === 'secondary' || source === null;
|
||||
return source === 'aux1' || source === 'aux2' || source === 'aux3' || source === 'secondary' || source === null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user