From 3705d8cca70fd850e369c6f084812cf1cc2ee4c3 Mon Sep 17 00:00:00 2001 From: velvettear Date: Mon, 7 Feb 2022 15:41:27 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + .vscode/launch.json | 14 ++ README.md | 7 + config.json | 19 +++ dev/FUZZ.ttl | 354 ++++++++++++++++++++++++++++++++++++++++++++ libs/api.js | 64 ++++++++ libs/commands.js | 27 ++++ libs/constants.js | 10 ++ libs/logger.js | 110 ++++++++++++++ libs/modep.js | 329 ++++++++++++++++++++++++++++++++++++++++ libs/server.js | 151 +++++++++++++++++++ libs/util.js | 78 ++++++++++ package.json | 25 ++++ pbc.js | 12 ++ 14 files changed, 1202 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 config.json create mode 100644 dev/FUZZ.ttl create mode 100644 libs/api.js create mode 100644 libs/commands.js create mode 100644 libs/constants.js create mode 100644 libs/logger.js create mode 100644 libs/modep.js create mode 100644 libs/server.js create mode 100644 libs/util.js create mode 100644 package.json create mode 100644 pbc.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..167ab9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +yarn.lock \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6dd13ce --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-node", + "request": "launch", + "name": "api", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/api.js" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7edc5cb --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# pbc + +pedal board control + +## description + +control your MODEP pedalboard(s) via http requests \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..b41d7db --- /dev/null +++ b/config.json @@ -0,0 +1,19 @@ +{ + "server": { + "listen": "0.0.0.0", + "port": 3000, + "timestamp": "DD.MM.YYYY HH:mm:ss:SS" + }, + "log": { + "level": "debug" + }, + "osc": { + "host": "192.168.1.24", + "port": "12101", + "address": "/midi" + }, + "modep": { + "host": "192.168.1.24", + "port": 80 + } +} \ No newline at end of file diff --git a/dev/FUZZ.ttl b/dev/FUZZ.ttl new file mode 100644 index 0000000..77df301 --- /dev/null +++ b/dev/FUZZ.ttl @@ -0,0 +1,354 @@ +@prefix atom: . +@prefix doap: . +@prefix ingen: . +@prefix lv2: . +@prefix midi: . +@prefix mod: . +@prefix pedal: . +@prefix rdfs: . + +_:b1 + ingen:tail ; + ingen:head . + +_:b2 + ingen:tail ; + ingen:head . + +_:b3 + ingen:tail ; + ingen:head . + +_:b4 + ingen:tail ; + ingen:head . + +_:b5 + ingen:tail ; + ingen:head . + +_:b6 + ingen:tail ; + ingen:head . + +_:b7 + ingen:tail ; + ingen:head . + +_:b8 + ingen:tail ; + ingen:head . + +_:b9 + ingen:tail ; + ingen:head . + +_:b10 + ingen:tail ; + ingen:head . + + + ingen:canvasX 561.0 ; + ingen:canvasY 210.0 ; + ingen:enabled false ; + ingen:polyphonic false ; + lv2:microVersion 0 ; + lv2:minorVersion 0 ; + mod:builderVersion 0 ; + mod:releaseNumber 0 ; + lv2:port , + , + , + , + , + ; + lv2:prototype ; + pedal:preset <> ; + a ingen:Block . + + + a lv2:AudioPort , + lv2:InputPort . + + + a lv2:AudioPort , + lv2:OutputPort . + + + ingen:value 0.250000 ; + midi:binding [ + midi:channel 0 ; + midi:controllerNumber 1 ; + lv2:minimum 0.000000 ; + lv2:maximum 1.000000 ; + a midi:Controller ; + ] ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 0.500000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 0.500000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 1 ; + midi:binding [ + midi:channel 0 ; + midi:controllerNumber 0 ; + a midi:Controller ; + ] ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:canvasX 1061.0 ; + ingen:canvasY 180.0 ; + ingen:enabled true ; + ingen:polyphonic false ; + lv2:microVersion 4 ; + lv2:minorVersion 0 ; + mod:builderVersion 0 ; + mod:releaseNumber 0 ; + lv2:port , + , + , + , + , + , + , + ; + lv2:prototype ; + pedal:preset <> ; + a ingen:Block . + + + a lv2:AudioPort , + lv2:InputPort . + + + a lv2:AudioPort , + lv2:InputPort . + + + a lv2:AudioPort , + lv2:OutputPort . + + + a lv2:AudioPort , + lv2:OutputPort . + + + ingen:value 50.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 25.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 0.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 0 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:canvasX 547.0 ; + ingen:canvasY 740.0 ; + ingen:enabled true ; + ingen:polyphonic false ; + lv2:microVersion 0 ; + lv2:minorVersion 0 ; + mod:builderVersion 0 ; + mod:releaseNumber 0 ; + lv2:port , + , + , + , + , + , + , + , + , + , + ; + lv2:prototype ; + pedal:preset <> ; + a ingen:Block . + + + a lv2:AudioPort , + lv2:InputPort . + + + a lv2:AudioPort , + lv2:InputPort . + + + a lv2:AudioPort , + lv2:OutputPort . + + + a lv2:AudioPort , + lv2:OutputPort . + + + ingen:value 0.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 64.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 20.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 29.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 20.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 64.000000 ; + a lv2:ControlPort , + lv2:InputPort . + + + ingen:value 0 ; + a lv2:ControlPort , + lv2:InputPort . + +<:bpb> + ingen:value 4.000000 ; + lv2:index 0 ; + a lv2:ControlPort , + lv2:InputPort . + +<:bpm> + ingen:value 120.000000 ; + lv2:index 1 ; + a lv2:ControlPort , + lv2:InputPort . + +<:rolling> + ingen:value 0 ; + lv2:index 2 ; + a lv2:ControlPort , + lv2:InputPort . + + + atom:bufferType atom:Sequence ; + lv2:index 3 ; + lv2:name "Control In" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control_in" ; + 4096 ; + a atom:AtomPort , + lv2:InputPort . + + + atom:bufferType atom:Sequence ; + lv2:index 4 ; + lv2:name "Control Out" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control_out" ; + 4096 ; + a atom:AtomPort , + lv2:OutputPort . + + + lv2:index 5 ; + lv2:name "Capture 1" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "capture_1" ; + a lv2:AudioPort , + lv2:InputPort . + + + lv2:index 6 ; + lv2:name "Capture 2" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "capture_2" ; + a lv2:AudioPort , + lv2:InputPort . + + + lv2:index 7 ; + lv2:name "Playback 1" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "playback_1" ; + a lv2:AudioPort , + lv2:OutputPort . + + + lv2:index 8 ; + lv2:name "Playback 2" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "playback_2" ; + a lv2:AudioPort , + lv2:OutputPort . + + + ingen:value 0 ; + lv2:index 9 ; + a atom:AtomPort , + lv2:InputPort . + +<> + doap:name "FUZZ" ; + pedal:unitName "Unknown" ; + pedal:unitModel "Unknown" ; + pedal:width 3788 ; + pedal:height 1546 ; + pedal:addressings ; + pedal:screenshot ; + pedal:thumbnail ; + pedal:version 7 ; + ingen:polyphony 1 ; + ingen:arc _:b1 , + _:b2 , + _:b3 , + _:b4 , + _:b5 , + _:b6 , + _:b7 , + _:b8 , + _:b9 , + _:b10 ; + ingen:block , + , + ; + lv2:port <:bpb> , + <:bpm> , + <:rolling> , + , + , + , + , + , + , + ; + lv2:extensionData ; + a lv2:Plugin , + ingen:Graph , + pedal:Pedalboard . diff --git a/libs/api.js b/libs/api.js new file mode 100644 index 0000000..306a33d --- /dev/null +++ b/libs/api.js @@ -0,0 +1,64 @@ +const logger = require('./logger.js'); +const util = require('./util.js'); +const constants = require('./constants.js'); +const modep = require('./modep.js'); + +const endpoints = new Map(); + +function setEndpoints(url, data) { + var startTime = new Date(); + var index = 0; + var elements = []; + for (var index = 0; index < data.length; index++) { + var element = data[index]; + element.id = index; + setEndpoint(url + '/' + element.id, [constants.HTTP_GET, constants.HTTP_POST], element); + elements.push(element); + } + elements = elements.sort(function (a, b) { + return a.id - b.id; + }); + setEndpoint(url, constants.HTTP_GET, elements); + logger.debug('setting up ' + elements.length + ' endpoint(s) for path \'' + url + '\' took ' + util.timeDiff(startTime) + 'ms'); +} + +function setEndpoint(url, types, data) { + logger.debug('setting up \'' + types + '\' endpoint \'' + url + '\'...') + endpoints.set(url, { types: types, data: data }); +} + +function getEndpoints() { + return endpoints; +} + +function setupEndpoints() { + return new Promise(function (resolve, reject) { + modep.getBanks() + .then(function (data) { + setEndpoints(constants.API_BANKS, data); + }) + .then(modep.getPedalboards) + .then(function (data) { + setEndpoints(constants.API_PEDALBOARDS, data); + }) + .then(modep.getDefaultPedalboard) + .then(function (data) { + setEndpoint(constants.API_PEDALBOARDS_DEFAULT, constants.HTTP_GET, data); + }) + .then(modep.getCurrentPedalboard) + .then(function (data) { + setEndpoint(constants.API_PEDALBOARDS_CURRENT, constants.HTTP_GET, data); + }) + .then(modep.getCurrentPedals) + .then(function (data) { + setEndpoints(constants.API_PEDALS, data); + }) + .then(resolve) + .catch(reject); + }); +} + +module.exports = { + setupEndpoints, + getEndpoints +} \ No newline at end of file diff --git a/libs/commands.js b/libs/commands.js new file mode 100644 index 0000000..2631a9c --- /dev/null +++ b/libs/commands.js @@ -0,0 +1,27 @@ +const logger = require('../libs/logger.js'); +const { spawn } = require('child_process') + +function execute(endpoint) { + if (!endpoint || !endpoint.command) { + logger.warn('no command defined'); + return; + } + logger.debug('executing command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\'...'); + var cmd = spawn(endpoint.command, endpoint.args); + cmd.stdout.on('data', function(data) { + logger.debug(data); + }); + cmd.stderr.on('data', function(data) { + logger.error(data); + }); + cmd.on('close', function(code) { + logger.debug('command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\' finished with exit code ' + code); + }); + cmd.on('error', function(err) { + logger.error('command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\' encountered an error >>> ' + err); + }); +} + +module.exports = { + execute +} \ No newline at end of file diff --git a/libs/constants.js b/libs/constants.js new file mode 100644 index 0000000..8b40d4f --- /dev/null +++ b/libs/constants.js @@ -0,0 +1,10 @@ +exports.HTTP_GET = 'GET'; +exports.HTTP_POST = 'POST'; + +exports.API_BANKS = '/banks'; +exports.API_PEDALBOARDS = '/pedalboards'; +exports.API_PEDALBOARDS_DEFAULT = '/pedalboards/default'; +exports.API_PEDALBOARDS_CURRENT = '/pedalboards/current'; +exports.API_PEDALS = '/pedals'; + +exports.PEDALBOARD_DEFAULT = '/var/modep/pedalboards/default.pedalboard'; \ No newline at end of file diff --git a/libs/logger.js b/libs/logger.js new file mode 100644 index 0000000..bdbdcfa --- /dev/null +++ b/libs/logger.js @@ -0,0 +1,110 @@ +const config = require("../config.json"); +const moment = require("moment"); + +// constants +const LOG_PREFIX_DEBUG = "debug"; +const LOG_PREFIX_INFO = "info"; +const LOG_PREFIX_WARNING = "warning"; +const LOG_PREFIX_ERROR = "error"; +const LOGLEVEL_DEBUG = 0; +const LOGLEVEL_INFO = 1; +const LOGLEVEL_WARNING = 2; +const LOGLEVEL_ERROR = 3; + +// set loglevel on "require" +const loglevel = function() { + switch (config.log.level) { + case LOG_PREFIX_DEBUG: + case LOGLEVEL_DEBUG: + return LOGLEVEL_DEBUG; + case LOG_PREFIX_INFO: + case LOGLEVEL_INFO: + return LOGLEVEL_INFO; + case LOG_PREFIX_WARNING: + case LOGLEVEL_WARNING: + return LOGLEVEL_WARNING; + case LOG_PREFIX_ERROR: + case LOGLEVEL_ERROR: + return LOGLEVEL_ERROR; + default: + return LOGLEVEL_INFO; + } +}(); + +// log a http request +function request(request) { + let message = "[" + request.method + "] url: \"" + request.url + "\""; + let counter = 1; + for (let param in request.body) { + message += ", parameter " + counter + ": \"" + param + "=" + request.body[param] + "\""; + counter++; + } + debug(message.trim()); +} + +// prefix log with "info" +function info(message) { + if (loglevel > LOGLEVEL_INFO) { + return; + } + trace(message); +} + +// prefix log with "info" +function warn(message) { + if (loglevel > LOGLEVEL_WARNING) { + return; + } + trace(message, "warning"); +} + +// prefix log with "debug" +function debug(message) { + if (loglevel > LOGLEVEL_DEBUG) { + return; + } + trace(message, "debug"); +} + +// prefix log with "error" +function error(message) { + if (loglevel > LOGLEVEL_ERROR) { + return; + } + trace(message, "error"); +} + +// default logging function +function trace(message, prefix) { + if (message === undefined || message === null || message.length === 0) { + return; + } + if (prefix === undefined || prefix === null || prefix.length === 0) { + prefix = "info"; + } + let print; + switch (prefix) { + case "error": + print = console.error; + break; + case "debug": + print = console.debug; + break; + case "warning": + print = console.warn; + break; + default: + print = console.log; + } + message = moment().format(config.server.timestamp) + " | " + prefix + " > " + message; + print(message); +} + +// exports +module.exports = { + info, + warn, + debug, + error, + request +}; \ No newline at end of file diff --git a/libs/modep.js b/libs/modep.js new file mode 100644 index 0000000..ff003c7 --- /dev/null +++ b/libs/modep.js @@ -0,0 +1,329 @@ +const config = require('../config.json'); +const util = require('./util.js'); +const constants = require('./constants.js'); +const logger = require('./logger.js'); +const path = require('path'); +const fs = require('fs'); +const { spawn } = require('child_process') +const ttl2jsonld = require('@frogcat/ttl2jsonld').parse; + +var banks = undefined; +var pedalboards = undefined; +var defaultPedalboard = undefined; +var currentPedalboard = undefined; +var currentPedals = undefined; + +function reset() { + return new Promise(function (resolve, reject) { + util.httpGET(config.modep.host, config.modep.port, '/reset') + .then(resolve) + .catch(reject); + }); +} + +function getBanks() { + return new Promise(function (resolve, reject) { + if (banks) { + return resolve(banks); + } + // FAKE DATA + var fake = [{ "title": "The Button", "pedalboards": [{ "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/default.pedalboard/default.ttl", "bundle": "/var/modep/pedalboards/default.pedalboard", "title": "Default", "version": 0 }, { "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/FUZZ.pedalboard/FUZZ.ttl", "bundle": "/var/modep/pedalboards/FUZZ.pedalboard", "title": "FUZZ", "version": 1 }] }]; + for (var index = 0; index < fake.length; index++) { + fake.id = index; + } + banks = util.sortById(fake); + return resolve(fake); + + util.httpGET(config.modep.host, config.modep.port, '/banks') + .then(resolve) + .catch(reject); + }); +} + +function getPedalboards() { + return new Promise(function (resolve, reject) { + if (pedalboards) { + return resolve(pedalboards); + } + // FAKE DATA + var fake = [{ "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/FUZZ.pedalboard/FUZZ.ttl", "bundle": "/var/modep/pedalboards/FUZZ.pedalboard", "title": "FUZZ", "version": 1 }, { "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/default.pedalboard/default.ttl", "bundle": "/var/modep/pedalboards/default.pedalboard", "title": "Default", "version": 0 }]; + var id = 1; + for (var index = 0; index < fake.length; index++) { + var pedalboard = fake[index]; + if (pedalboard.bundle == constants.PEDALBOARD_DEFAULT) { + pedalboard.id = 0; + defaultPedalboard = pedalboard; + continue; + } + pedalboard.id = id; + id++; + } + pedalboards = util.sortById(fake); + return resolve(fake); + + util.httpGET(config.modep.host, config.modep.port, '/pedalboard/list') + .then(function (pedalboards) { + + }) + .catch(reject); + }); +} + +function getDefaultPedalboard() { + return new Promise(function (resolve, reject) { + if (defaultPedalboard) { + return resolve(defaultPedalboard); + } + getPedalboardByBundle(constants.PEDALBOARD_DEFAULT) + .then(resolve) + .catch(reject); + }); +} + +function getCurrentPedalboard() { + return new Promise(function (resolve, reject) { + if (currentPedalboard && currentPedalboard.id) { + return resolve(currentPedalboard); + } + // FAKE DATA + var fake = '/var/modep/pedalboards/FUZZ.pedalboard'; + getPedalboardByBundle(fake) + .then(function (pedalboard) { + currentPedalboard = pedalboard; + return resolve(pedalboard); + }) + .catch(reject); + + // PRODUCTION + // util.httpGET(config.modep.host, config.modep.port, '/pedalboard/current') + // .then(getPedalboardByBundle) + // .then(resolve) + // .catch(reject); + }); +} + +function getPedalboardById(pedalboardId) { + return new Promise(function (resolve, reject) { + getPedalboards() + .then(function (pedalboards) { + for (var index = 0; index < pedalboards.length; index++) { + if (pedalboards[index].id != pedalboardId) { + continue; + } + return resolve(pedalboards[index]); + } + return reject('error: could not find pedalboard by id \'' + controlId + '\''); + }) + .catch(reject); + }); +} + +function getPedalboardByBundle(pedalboardBundle) { + return new Promise(function (resolve, reject) { + getPedalboards() + .then(function (pedalboards) { + for (var index = 0; index < pedalboards.length; index++) { + if (pedalboards[index].bundle != pedalboardBundle) { + continue; + } + return resolve(pedalboards[index]); + } + return reject('error: could not find pedalboard by bundle \'' + pedalboardBundle + '\''); + }) + .catch(reject); + }); +} + +function getPedalControlById(pedalId, controlId) { + return new Promise(function (resolve, reject) { + getCurrentPedalById(pedalId) + .then(function (pedal) { + for (var index = 0; index < pedal.controls.length; index++) { + if (pedal.controls[index].id != controlId) { + continue; + } + return resolve(pedal.controls[index]); + } + return reject('error: could not find control for pedal \'' + pedalId + '\' by id \'' + controlId + '\''); + }) + .catch(reject); + }); +} + +function getCurrentPedalById(id) { + return new Promise(function (resolve, reject) { + getCurrentPedals() + .then(function () { + for (var index = 0; index < currentPedals.length; index++) { + if (currentPedals[index].id != id) { + continue; + } + return resolve(currentPedals[index]); + } + return reject('error: could not find current pedal by id \'' + id + '\''); + }) + .catch(reject); + }); +} + +function getCurrentPedals() { + return new Promise(function (resolve, reject) { + if (currentPedals) { + return resolve(currentPedals); + } + getCurrentPedalboard() + .then(parseCurrentPedalboard) + .then(resolve) + .catch(reject); + }); +} + +function parseCurrentPedalboard(pedalboardBundle) { + return new Promise(function (resolve, reject) { + var startTime = new Date(); + logger.debug('parsing current pedalboard...'); + if (!currentPedalboard || !currentPedalboard.uri) { + for (var index = 0; index < pedalboards.length; index++) { + var pedalboard = pedalboards[index]; + if (!pedalboard.bundle || pedalboardBundle != pedalboard.bundle) { + continue; + } + currentPedalboard = pedalboard; + break; + }; + } + if (!currentPedalboard.uri) { + reject('error: could not determine current pedalboard config file'); + } + // FAKE DATA + var file = path.resolve('./dev/FUZZ.ttl'); + // var file = path.resolve(currentPedalboard.uri.replace('file://', '')); + fs.readFile(file, function (err, data) { + if (err) { + return reject('error: could not parse current pedalboard file \'' + file + '\' >>> ' + err); + } + currentPedals = []; + var json = ttl2jsonld(data.toString())['@graph']; + var id = 0; + for (var index = 0; index < json.length; index++) { + var tmp = json[index]; + if (!tmp['lv2:prototype']) { + continue; + } + var name = tmp['@id']; + currentPedals.push({ id: id, name: name, controls: [] }); + id++; + } + for (var index = 0; index < json.length; index++) { + var tmp = json[index]; + var name = tmp['@id']; + var value = tmp['ingen:value']; + if (value == undefined) { + continue; + } + var pedal = undefined; + for (var pedalIndex = 0; pedalIndex < currentPedals.length; pedalIndex++) { + if (!name.startsWith(currentPedals[pedalIndex].name)) { + continue; + } + pedal = currentPedals[pedalIndex]; + break; + } + if (!pedal) { + continue; + } + id = pedal.controls.length; + name = name.replace(pedal.name + '/', ''); + if (name == ':bypass') { + value = value; + } else { + value = value['@value']; + } + var control = { id: id, name: name, value: value }; + pedal.controls.push(control); + id++; + var midi = tmp['midi:binding']; + if (!midi) { + continue; + } + control.midi = { channel: midi['midi:channel'], controller: midi['midi:controllerNumber'] } + } + logger.debug('parsing current pedalboard file \'' + file + '\' took ' + util.timeDiff(startTime) + 'ms') + resolve(currentPedals); + }); + }); +} + +function sendValueToControl(value, control) { + return new Promise(function (resolve, reject) { + if (!control || !control.midi) { + return reject('error: control \'' + control.name + '\' with id \'' + control.id + '\' has no midi bindings'); + } + if (value > 127) { + value = 127; + } else if (value < 0) { + value = 0; + } + value = '00' + util.toHex(value) + '0' + util.toHex(control.midi.controller) + 'b' + util.toHex(control.midi.channel); + var cmd = 'oscsend'; + var args = [config.osc.host, config.osc.port, config.osc.address, 'm', value]; + logger.debug('executing command \'' + cmd + '\' with args \'' + args + '\'...'); + var spawned = spawn(cmd, args); + spawned.stdout.on('data', function (data) { + logger.debug(data); + }); + spawned.stderr.on('data', function (data) { + logger.error(data); + }); + spawned.on('close', function (code) { + logger.debug('command \'' + cmd + '\' with args \'' + args + '\' finished with exit code ' + code); + resolve(); + }); + spawned.on('error', function (err) { + logger.error('command \'' + cmd + '\' with args \'' + args + '\' encountered an error >>> ' + err); + reject(err); + }); + }); +} + +function setPedalboardById(pedalboardId) { + return new Promise(function (resolve, reject) { + if (!pedalboardId) { + return reject('error: no pedalboard id given'); + } + getPedalboardById(pedalboardId) + .then(setPedalboard) + .then(resolve) + .catch(reject); + }); +} + +function setPedalboard(pedalboard) { + return new Promise(function (resolve, reject) { + if (!pedalboard || !pedalboard.bundle) { + return reject('error: no bundle set for pedalboard'); + } + if (pedalboard.id == currentPedalboard.id) { + return resolve('pedalboard \'' + pedalboard.id + '\' is already active'); + } + reset() + .then(function () { + util.httpPOST(config.modep.host, config.modep.port, '/pedalboard/load_bundle/?bundlepath=' + pedalboard.bundle) + }) + .then(getCurrentPedalboard) + .catch(reject); + }); +} + +module.exports = { + getBanks, + getPedalboards, + getDefaultPedalboard, + getCurrentPedalboard, + getCurrentPedals, + getCurrentPedalById, + getPedalControlById, + sendValueToControl, + setPedalboard, + setPedalboardById, +} \ No newline at end of file diff --git a/libs/server.js b/libs/server.js new file mode 100644 index 0000000..5da4616 --- /dev/null +++ b/libs/server.js @@ -0,0 +1,151 @@ +const config = require('../config.json'); +const logger = require('./logger.js'); +const api = require('./api.js') +const constants = require('./constants.js'); +const modep = require('./modep.js'); +const http = require('http'); + +var server; + +function start() { + return new Promise(function (resolve, reject) { + if (!server) { + server = http.createServer(); + } + server.listen(config.server.port, config.server.listen).on('listening', function () { + logger.debug('server listening on ' + config.server.listen + ':' + config.server.port + '...'); + handleRequests(); + }); + }); +} + +function handleRequests() { + server.on('request', function (request, response) { + logger.request(request); + var endpoint = api.getEndpoints().get(request.url); + if (!endpoint) { + var msg = 'endpoint \'' + request.url + '\' not defined'; + response.writeHead(501); + response.end(msg); + logger.debug(msg); + return; + } + if (!endpoint.types.includes(request.method)) { + var msg = 'endpoint \'' + request.url + '\' does not support ' + request.method + ' requests'; + response.writeHead(405); + response.end(msg); + logger.debug(msg); + return; + } + if (request.method == constants.HTTP_GET) { + handleGET(response, endpoint); + return; + } + if (request.method == constants.HTTP_POST) { + handlePOST(request, response, endpoint); + return; + } + var msg = 'endpoint \'' + request.url + '\' does not have any handlers for ' + request.method + ' requests'; + response.writeHead(405); + response.end(msg); + logger.debug(msg); + }); +} + +function handleGET(response, endpoint) { + var data = endpoint.data; + if (!data) { + response.writeHead(500); + response.end('error: could not get data for endpoint') + return; + } + response.writeHead(200); + response.end(JSON.stringify(data)); +} + +function handlePOST(request, response, endpoint) { + if (request.url.startsWith(constants.API_PEDALS)) { + handlePOSTPedals(request) + .then(function (msg) { + response.writeHead(200); + response.end(msg); + }) + .catch(function (err, code) { + if (!code) { + code = 500; + } + response.writeHead(code); + response.end(err); + logger.error(err); + }); + return; + } + if (request.url.startsWith(constants.API_PEDALBOARDS)) { + handlePOSTPedalboards(request) + .then(function (msg) { + response.writeHead(200); + response.end(msg); + }) + .catch(function (err, code) { + if (!code) { + code = 500; + } + response.writeHead(code); + response.end(err); + logger.error(err); + }); + return; + } + var msg = 'endpoint \'' + request.url + '\' is not yet implemented for ' + request.method + ' requests'; + response.writeHead(405); + response.end(msg); + +} + +function getPOSTParams(request) { + return new Promise(function (resolve, reject) { + var params = ""; + request.on("data", function (data) { + params += data; + }); + request.on("end", function () { + return resolve(new URLSearchParams(params)); + }); + }); +} + +function handlePOSTPedals(request) { + return new Promise(function (resolve, reject) { + getPOSTParams(request) + .then(function (params) { + var pedalId = request.url.substring(request.url.lastIndexOf('/') + 1); + var controlId = params.get('id'); + if (controlId == undefined) { + reject('error: could not handle POST - missing parameter \'id\'', 400) + } + var value = params.get('value'); + if (value == undefined) { + reject('error: could not handle POST - missing parameter \'value\'', 400) + } + modep.getPedalControlById(pedalId, controlId) + .then(function (control) { + modep.sendValueToControl(value, control); + }) + .then(resolve) + .catch(reject); + }) + .catch(reject); + }); +} + +function handlePOSTPedalboards(request) { + return new Promise(function (resolve, reject) { + modep.setPedalboardById(request.url.substring(request.url.lastIndexOf('/') + 1)) + .then(resolve) + .catch(reject); + }); +} + +module.exports = { + start +} \ No newline at end of file diff --git a/libs/util.js b/libs/util.js new file mode 100644 index 0000000..5882445 --- /dev/null +++ b/libs/util.js @@ -0,0 +1,78 @@ +const logger = require('./logger.js'); +const http = require('http'); +const { HTTP_GET } = require('./constants.js'); + +function timeDiff(startTime) { + if (startTime instanceof Date) { + return (new Date().getTime() - startTime.getTime()); + } + return new Date().getTime - startTime; +} + +function clone(object) { + var clone = {}; + for (key in object) { + clone[key] = object[key]; + } + return clone; +} + +function httpGET(host, port, path, args) { + return httpRequest(host, port, path, HTTP_GET, args); +} + +function httpPOST(host, port, path, args) { + return httpRequest(host, port, path, HTTP_POST, args); +} + +function httpRequest(host, port, path, method, args) { + return new Promise(function (resolve, reject) { + if (!path.startsWith("/")) { + path = "/" + path; + } + logger.debug('sending http \'' + method + '\' request to \'' + host + ':' + port + path + '\'...'); + const request = http.request({ + hostname: host, + port: port, + path: path, + method: method + }, function (response) { + if (!response) { + return reject('error: no response from host for http \'' + method + '\' request \'' + host + ':' + port + path + '\''); + } + logger.debug('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned status code ' + response.statusCode); + var responseData = ""; + response.on('data', function (data) { + responseData += data; + }); + response.on('end', function () { + logger.debug('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned data \'' + responseData + '\''); + return resolve(responseData); + }); + }); + request.on('error', function (err) { + return reject('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned an error >>> ' + err); + }); + request.end(); + }); +} + +function toHex(value) { + var hex = Number(value).toString(16); + return hex; +} + +function sortById(array) { + return array.sort(function (a, b) { + return a.id - b.id; + }); +} + +module.exports = { + timeDiff, + clone, + httpGET, + httpPOST, + toHex, + sortById +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c60a04a --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "pbc", + "version": "0.0.1", + "description": "pedal board control", + "main": "pbc.js", + "scripts": { + "start": "node pbc.js" + }, + "keywords": [ + "scripts", + "commands", + "remote", + "api" + ], + "author": "Daniel Sommer ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://git.velvettear.de/velvettear/pbc.git" + }, + "dependencies": { + "@frogcat/ttl2jsonld": "^0.0.6", + "moment": "^2.29.1" + } +} diff --git a/pbc.js b/pbc.js new file mode 100644 index 0000000..bf22f61 --- /dev/null +++ b/pbc.js @@ -0,0 +1,12 @@ +const logger = require('./libs/logger.js'); +const api = require('./libs/api.js'); +const server = require('./libs/server.js') +const packageJSON = require('./package.json'); + +logger.info("launching " + packageJSON.name + " " + packageJSON.version); +api.setupEndpoints() + .then(server.start) + .catch(function (err) { + logger.error(err); + process.exit(1); + }); \ No newline at end of file