Skip to content

Commit

Permalink
✨ Add a function to send arbitrary OSC to AbleSet
Browse files Browse the repository at this point in the history
  • Loading branch information
leolabs committed Jan 15, 2025
1 parent 4cd4f7e commit d58f595
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ export const enum Action {
SetCountInSoloClick = 'setCountInSoloClick',
SetCountInDuration = 'setCountInDuration',
SetJumpMode = 'setJumpMode',
SendOscCommand = 'sendOscCommand',
}
39 changes: 39 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { makeRange } from './utils/range'
import { variables } from './variables'
import { getProgressIcon } from './icons'
import { debounce, throttle } from 'lodash'
import { parseOscCommands } from './utils/string-to-osc'

/** The port that AbleSet is listening on */
const SERVER_PORT = 39041
Expand Down Expand Up @@ -871,6 +872,44 @@ class ModuleInstance extends InstanceBase<Config> {
callback: async (event) => this.sendOsc(['/settings/jumpMode', String(event.options.value)]),
},
//#endregion

//#region misc
[Action.SendOscCommand]: {
name: 'Send OSC Commands',
description: 'Sends a given set of OSC commands to AbleSet',
options: [
{
id: 'command',
label: 'OSC Commands',
type: 'textinput',
regex: '^\\/(.+)',
},
],
callback: async (event) => {
const commands = parseOscCommands(String(event.options.command))

for (const command of commands) {
let customSleep: number | null = null

if (!command.host && command.address === '//sleep') {
const firstArg = command.args?.[0]
customSleep = firstArg ? Number(firstArg.value) : null

if (customSleep) {
const delay = Math.max(0, customSleep)
await new Promise((res) => setTimeout(res, delay))
}
} else {
this.sendOsc([command.address, ...(command.args ?? [])])
}

if (!customSleep) {
await new Promise((res) => setTimeout(res, 50))
}
}
},
},
//#endregion
})
}

Expand Down
178 changes: 178 additions & 0 deletions src/utils/string-to-osc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type { Argument } from 'node-osc'

export interface OscCommand {
host?: string
port?: number
address: string
args?: Argument[]
}

enum State {
InHost,
InAddress,
AfterAddress,
InArgument,
InString,
AfterArgument,
AfterCommand,
}

export const parseArgument = (arg: string): Argument => {
if (arg.match(/^[+-]?\d+$/)) {
return { type: 'integer', value: Number(arg) }
} else if (arg.match(/^[+-]?\d+\.\d*$/)) {
return { type: 'float', value: Number(arg) }
} else if (arg === 'true') {
return { type: 'boolean', value: true }
} else if (arg === 'false') {
return { type: 'boolean', value: false }
} else if (isStringQuote(arg[0]!) && isStringQuote(arg[arg.length - 1]!)) {
const stringQuote = arg[0]!
return {
type: 'string',
value: arg.substring(1, arg.length - 1).replaceAll(`\\${stringQuote}`, stringQuote),
}
} else {
return { type: 'string', value: arg }
}
}

const isWhiteSpace = (char: string) => {
return char === ' ' || char === ' ' || char === '\t' || char === '\n'
}

const isStringQuote = (char: string) => {
return char === `"` || char === `'` || char === '`'
}

const parseHost = (input: string): { host?: string; port?: number } => {
if (!input.trim() || !input.includes(':')) {
return {}
} else {
const [host, port] = input.split(':')
return { host, port: Number(port) }
}
}

export const parseOscCommands = (input: string): OscCommand[] => {
const commands: OscCommand[] = []
let state = State.InHost

let currentHost = ''
let currentAddress = ''
let currentArgs: string[] = []
let hostStart = 0
let addressStart = 0
let argumentStart = 0
let stringQuote = ''

// Trim input first
input = input.trim()

for (let i = 0; i < input.length; i++) {
const char = input[i]!

switch (state) {
case State.InHost: {
if (char === '/') {
currentHost = input.substring(hostStart, i)
addressStart = i
state = State.InAddress
}
break
}

case State.InAddress: {
if (isWhiteSpace(char)) {
currentAddress = input.substring(addressStart, i)
state = State.AfterAddress
} else if (char === ';') {
commands.push({
...parseHost(currentHost),
address: input.substring(addressStart, i),
})
currentHost = ''
currentAddress = ''
state = State.AfterCommand
}
break
}

case State.AfterAddress: {
if (isStringQuote(char)) {
state = State.InString
stringQuote = char
argumentStart = i
} else if (!isWhiteSpace(char)) {
state = State.InArgument
argumentStart = i
}
break
}

case State.InArgument: {
if (isWhiteSpace(char) && i > argumentStart) {
currentArgs.push(input.substring(argumentStart, i))
state = State.AfterArgument
} else if (char === ';') {
currentArgs.push(input.substring(argumentStart, i))
commands.push({
...parseHost(currentHost),
address: currentAddress,
args: currentArgs.map(parseArgument),
})
currentArgs = []
currentHost = ''
currentAddress = ''
state = State.AfterCommand
}
break
}

case State.InString: {
if (char === stringQuote && input[i - 1] !== `\\`) {
state = State.InArgument
}
break
}

case State.AfterArgument: {
if (isStringQuote(char)) {
state = State.InString
stringQuote = char
argumentStart = i
} else if (!isWhiteSpace(char)) {
argumentStart = i
state = State.InArgument
}
break
}

case State.AfterCommand: {
if (char === '/') {
addressStart = i
state = State.InAddress
} else if (!isWhiteSpace(char)) {
hostStart = i
state = State.InHost
}
}
}
}

if (state === State.InHost || state === State.InAddress) {
commands.push({
...parseHost(currentHost),
address: input.substring(addressStart),
})
} else if (state === State.InArgument) {
currentArgs.push(input.substring(argumentStart))
commands.push({
...parseHost(currentHost),
address: currentAddress,
args: currentArgs.map(parseArgument),
})
}

return commands
}

0 comments on commit d58f595

Please sign in to comment.