Skip to content

Commit

Permalink
feat: send osc
Browse files Browse the repository at this point in the history
  • Loading branch information
cpvalente committed Jan 11, 2025
1 parent 73b3a01 commit 53f26de
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isHTTPOutput,
isOSCOutput,
OntimeEvent,
OSCOutput,
} from 'ontime-types';

import { addBlueprint, editBlueprint, testOutput } from '../../../../common/api/automation';
Expand Down Expand Up @@ -93,8 +94,22 @@ export default function BlueprintForm(props: BlueprintFormProps) {
appendOutput({ type: 'http', url: '' });
};

const handleTestOSCOutput = async () => {
console.log('Test OSC output not implemented');
const handleTestOSCOutput = async (index: number) => {
try {
const values = getValues(`outputs.${index}`) as OSCOutput;
if (!values.targetIP || !values.targetPort || !values.address || !values.args) {
return;
}
testOutput({
type: 'osc',
targetIP: values.targetIP,
targetPort: values.targetPort,
address: values.address,
args: values.args,
});
} catch (_error) {
/** we dont handle errors here, users should use the network tab */
}
};

const handleTestHTTPOutput = (index: number) => {
Expand Down Expand Up @@ -331,7 +346,7 @@ export default function BlueprintForm(props: BlueprintFormProps) {
<Panel.Error>{rowErrors?.args?.message}</Panel.Error>
</label>
<Panel.InlineElements relation='inner'>
<Button size='sm' variant='ontime-ghosted' isDisabled={!canTest} onClick={handleTestOSCOutput}>
<Button size='sm' variant='ontime-ghosted' onClick={() => handleTestOSCOutput(index)}>
Test
</Button>
<IconButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TimerLifeCycle } from 'ontime-types';

import { deleteAllAutomations, addAutomation, addBlueprint } from '../automation.dao.js';
import { triggerAction } from '../automation.service.js';
import { triggerAutomations } from '../automation.service.js';

import { makeOSCAction, makeHTTPAction } from './testUtils.js';

Expand Down Expand Up @@ -70,25 +70,25 @@ describe('triggerAction()', () => {
});

it('should trigger automations for a given action', () => {
triggerAction(TimerLifeCycle.onLoad, {});
triggerAutomations(TimerLifeCycle.onLoad, {});
expect(oscSpy).toHaveBeenCalledTimes(1);
expect(httpSpy).not.toBeCalled();
oscSpy.mockReset();
httpSpy.mockReset();

triggerAction(TimerLifeCycle.onStart, {});
triggerAutomations(TimerLifeCycle.onStart, {});
expect(oscClient.emitOSC).not.toBeCalled();
expect(httpSpy).not.toBeCalled();
oscSpy.mockReset();
httpSpy.mockReset();

triggerAction(TimerLifeCycle.onFinish, {});
triggerAutomations(TimerLifeCycle.onFinish, {});
expect(oscSpy).not.toBeCalled();
expect(httpSpy).toHaveBeenCalledTimes(1);
oscSpy.mockReset();
httpSpy.mockReset();

triggerAction(TimerLifeCycle.onStop, {});
triggerAutomations(TimerLifeCycle.onStop, {});
expect(oscSpy).not.toBeCalled();
expect(httpSpy).not.toBeCalled();
});
Expand Down
247 changes: 247 additions & 0 deletions apps/server/src/api-data/automation/__tests__/automation.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';

describe('parseTemplateNested()', () => {
it('parses string with a single-level variable name', () => {
const store = { timer: 10 };
const templateString = '/test/{{timer}}';
const result = parseTemplateNested(templateString, store);
expect(result).toEqual('/test/10');
});

it('parses string with a nested variable name', () => {
const store = { timer: { clock: 10 } };
const templateString = '/timer/{{timer.clock}}';
const result = parseTemplateNested(templateString, store);
expect(result).toEqual('/timer/10');
});

it('parses string with multiple variables', () => {
const mockState = { test1: 'that', test2: 'this' };
const testString = '{{test1}} should replace {{test2}}';
const expected = `${mockState.test1} should replace ${mockState.test2}`;

const result = parseTemplateNested(testString, mockState);
expect(result).toStrictEqual(expected);
});

it('correctly parses a string without templates', () => {
const testString = 'That should replace {test}';

const result = parseTemplateNested(testString, {});
expect(result).toStrictEqual(testString);
});

it('handles scenarios with missing variables', () => {
// by failing to provide a value, we give visibility to
// potential issues in the given string
const mockState = { test1: 'that', test2: 'this' };
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;

const result = parseTemplateNested(testString, mockState);
expect(result).toStrictEqual(expected);
});
});

describe('parseNestedTemplate() -> resolveAliasData()', () => {
it('resolves data through callback', () => {
const data = {
not: {
so: {
easy: '3',
},
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};

const easyParse = parseTemplateNested('{{human.easy}}', data, aliases);
expect(easyParse).toBe('testing-3');
});
it('handles a mixed operation', () => {
const data = {
not: {
so: {
easy: '3',
},
},
other: {
value: 42,
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};

const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}}', data, aliases);
expect(easyParse).toBe('42 to testing-3');
});
it('returns given key when not found', () => {
const data = {
not: {
so: {
easy: '3',
},
},
other: {
value: 5,
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};

const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}} {{human.not.found}}', data, aliases);
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
});
});

describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
it('specific osc requirements', () => {
const data = {
not: {
so: {
easy: 'data with space',
empty: '',
number: 1234,
stringNumber: '1234',
},
},
};

const payloads = [
{
test: '"string with space and {{not.so.easy}}"',
expect: [{ type: 'string', value: 'string with space and data with space' }],
},
{
test: '',
expect: [],
},
{
test: ' ',
expect: [],
},
{
test: '""',
expect: [{ type: 'string', value: '' }],
},
{
test: '"string with space and {{not.so.empty}}"',
expect: [{ type: 'string', value: 'string with space and ' }],
},
{
test: '"string with space and {{not.so.number}}"',
expect: [{ type: 'string', value: 'string with space and 1234' }],
},
{
test: '"string with space and {{not.so.stringNumber}}"',
expect: [{ type: 'string', value: 'string with space and 1234' }],
},
{
test: '"{{not.so.easy}}" 1',
expect: [
{ type: 'string', value: 'data with space' },
{ type: 'integer', value: 1 },
],
},
{
test: '"{{not.so.empty}}" 1',
expect: [
{ type: 'string', value: '' },
{ type: 'integer', value: 1 },
],
},
{
test: '',
expect: [],
},
];

payloads.forEach((payload) => {
const parsedPayload = parseTemplateNested(payload.test, data);
const parsedArguments = stringToOSCArgs(parsedPayload);
expect(parsedArguments).toStrictEqual(payload.expect);
});
});
});

describe('test stringToOSCArgs()', () => {
it('all types', () => {
const test = 'test 1111 0.1111 TRUE FALSE';
const expected = [
{ type: 'string', value: 'test' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('empty is nothing', () => {
const test = undefined;
const expected = [];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('empty is nothing', () => {
const test = '';
const expected = [];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('1 space is nothing', () => {
const test = ' ';
const expected = [];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('keep other types in strings', () => {
const test = 'test "1111" "0.1111" "TRUE" "FALSE"';
const expected = [
{ type: 'string', value: 'test' },
{ type: 'string', value: '1111' },
{ type: 'string', value: '0.1111' },
{ type: 'string', value: 'TRUE' },
{ type: 'string', value: 'FALSE' },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('keep spaces in quoted strings', () => {
const test = '"test space" 1111 0.1111 TRUE FALSE';
const expected = [
{ type: 'string', value: 'test space' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('keep spaces escaped quotes', () => {
const test = '"test \\" space" 1111 0.1111 TRUE FALSE';
const expected = [
{ type: 'string', value: 'test " space' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});

it('2 spaces', () => {
const test = '1111 0.1111 TRUE FALSE';
const expected = [
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
});
5 changes: 2 additions & 3 deletions apps/server/src/api-data/automation/automation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ function send(output: AutomationOutput[], state?: RuntimeState) {
const stateSnapshot = state ?? getState();
output.forEach((payload) => {
if (isOSCOutput(payload)) {
emitOSC();
}
if (isHTTPOutput(payload)) {
emitOSC(payload, stateSnapshot);
} else if (isHTTPOutput(payload)) {
emitHTTP(payload, stateSnapshot);
}
});
Expand Down
Loading

0 comments on commit 53f26de

Please sign in to comment.