diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e71bf27..a4ab2ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,8 @@ name: Build app -#on: -# push: -# branches: -# - main +on: + push: + branches: + - main jobs: build: diff --git a/.gitignore b/.gitignore index 68a956c..b5d6421 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist out/ .idea/ yarn-error.log +appconfig.json \ No newline at end of file diff --git a/README.md b/README.md index 01c70cc..5cb7c36 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,74 @@ $ sudo udevadm control --reload-rules - Technically any interface that uses the [usbdmx driver](https://github.com/fx5/usbdmx) by Frank Sievertsen - Won't work out of the box, vendor ID and product ID need to be [added manually](./src/usbdmx/index.ts) +# Automatic restart + +To restart the program automatically in case of a crash or another exception, add `--autoretry` as a command line argument. +This does not apply in the following cases: +- Intentional exit (Crtl+C, "kill" command, closing the terminal) +- Invalid configuration file +# Configuration File +You can optionally add a configuration file containing parameters for the default USBDMX interface +and options regarding the ArtNet transceiver. + +To use a configuration file, create a file on your system with the options below. Then start the program with `--config=` +as a command line argument. Replace ` ` with the absolute path to the file. + +## Full config + +You can omit any options that you don't want to change, the program will use the default options instead (except for `"interface"`). +```json lines +{ + "interface": {}, // see below + "dmxnet": { // config for ArtNet transceiver + "main": { // general options + "log": { + "level": "info", // available: error, warn, info, verbose, debug, silly + "oem": 0, // OEM Code from artisticlicense, default to dmxnet OEM. + "sName": "Text", // 17 char long node description, default to "usbdmx" + "lName": "Long description", // 63 char long node description, default to "ArtNet-USBDMX-Converter" + "hosts": ["127.0.0.1"] // Interfaces to listen to, defaults to ["0.0.0.0"] + } + }, + "transmitter": { // ArtNet transmitter options (USBDMX In) + "ip": "127.0.0.1", // IP to send to, default 255.255.255.255 + "subnet": 0, // Destination subnet, default 0 + "universe": 0, // Destination universe, default 0 + "net": 0, // Destination net, default 0 + "port": 6454, // Destination UDP Port, default 6454 + "base_refresh_interval": 1000 // Default interval for sending unchanged ArtDmx + }, + "receiver": { // ArtNet receiver options (USBDMX Out) + "subnet": 0, //Destination subnet, default 0 + "universe": 0, //Destination universe, default 0 + "net": 0, //Destination net, default 0 + } + } +} +``` +## Generate config for interface +To automatically select a USBDMX Interface on startup you need to set its parameters in the configuration file under `"interfaces"`. + +You can generate these parameters by running: +```bash +$ ./artnet-usbdmx-converter outputconfig +``` + +Select the interface and mode to use and copy the output to `"interface"` the configuration file. + +Example: +```json lines +{ + "interface": { + "serial": "0000000010000492", + "mode": "6", + "manufacturer": "Digital Enlightenment", + "product": "USB DMX" + }, + [...] +} +``` + # Develop & Build ## Development diff --git a/package.json b/package.json index 36af658..3b43d8f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dmxnet": "^0.9.0", "figlet": "^1.6.0", "inquirer": "8.0.0", + "minimist": "^1.2.8", "node-hid": "^2.1.2" }, "devDependencies": { @@ -41,6 +42,7 @@ "@types/clui": "^0.3.2", "@types/figlet": "^1.5.6", "@types/inquirer": "^9.0.3", + "@types/minimist": "^1.2.5", "@types/node": "^20.8.3", "@types/node-hid": "^1.3.4", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/src/ConvertHandler.ts b/src/ConvertHandler.ts index ea47b6b..c428f81 100644 --- a/src/ConvertHandler.ts +++ b/src/ConvertHandler.ts @@ -1,6 +1,7 @@ import {dmxnet, receiver, sender} from "dmxnet"; import {DetectedInterface, DMXInterface, getConnectedInterfaces} from "./usbdmx"; import {clearInterval} from "timers"; +import {defaultConfigStorage} from "./index"; /** * Responsible for converting incoming Art-Net data to an USBDMX output @@ -33,20 +34,9 @@ export default class ConvertHandler { * Starts up the Art-Net receiver */ startArtNetReceiver = () => { - this.dmxnetManager = new dmxnet({ - log: {level: "error"}, - sName: "usbdmx", - lName: "ArtNet-USBDMX-Converter", - }); - this.artNetReceiver = this.dmxnetManager.newReceiver(); - this.artNetSender = this.dmxnetManager.newSender({ - ip: "255.255.255.255", //IP to send to, default 255.255.255.255 - subnet: 0, //Destination subnet, default 0 - universe: 0, //Destination universe, default 0 - net: 0, //Destination net, default 0 - port: 6454, //Destination UDP Port, default 6454 - base_refresh_interval: 1000 // Default interval for sending unchanged ArtDmx - }); + this.dmxnetManager = new dmxnet(defaultConfigStorage.getDmxNetConfig()); + this.artNetReceiver = this.dmxnetManager.newReceiver(defaultConfigStorage.getDmxNetReceiverConfig()); + this.artNetSender = this.dmxnetManager.newSender(defaultConfigStorage.getDmxNetSenderConfig()); this.artNetReceiver.on("data", this.handleIncomingArtNetData); } diff --git a/src/config/ConfigStorage.ts b/src/config/ConfigStorage.ts new file mode 100644 index 0000000..e3a7964 --- /dev/null +++ b/src/config/ConfigStorage.ts @@ -0,0 +1,70 @@ +/*********************************** + * src/config/ConfigStorage.ts + * + * Created on 08.05.24 by adrian + * Project: artnet-usbdmx-converter + *************************************/ + +import * as fs from "fs"; +import {StartupScreenResponse} from "../startupscreen"; +import {DmxnetOptions, ReceiverOptions, SenderOptions} from "dmxnet"; + +export default class ConfigStorage { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private data: any + + loadConfig(path: string) { + let filedata: string + try { + filedata = fs.readFileSync(path, "utf-8") + } catch (e) { + console.error("Failed to read file!") + console.error(e) + process.exit(1) + } + + try { + this.data = JSON.parse(filedata); + } catch (e) { + console.error("JSON file contains error!") + console.error(e) + process.exit(1) + } + } + + getInterface(): StartupScreenResponse | undefined { + return this.data?.interface as StartupScreenResponse ?? undefined; + } + + getDmxNetConfig(): DmxnetOptions { + const mainConfig = this.data?.dmxnet?.main; + return { + log: mainConfig?.log ?? {level: "error"}, + oem: mainConfig?.oem ?? undefined, + sName: mainConfig?.sName ?? "usbdmx", + lName: mainConfig?.lName ?? "ArtNet-USBDMX-Converter", + hosts: mainConfig?.hosts ?? ["0.0.0.0"] + } + } + + getDmxNetReceiverConfig(): ReceiverOptions { + const receiverConfig = this.data?.dmxnet?.receiver; + return { + subnet: receiverConfig?.subnet ?? 0, + universe: receiverConfig?.universe ?? 0, + net: receiverConfig?.net ?? 0, + } + } + + getDmxNetSenderConfig(): SenderOptions { + const transmitterConfig = this.data?.dmxnet?.transmitter; + return { + ip: transmitterConfig?.ip ?? "255.255.255.255", + subnet: transmitterConfig?.subnet ?? 0, + universe: transmitterConfig?.universe ?? 0, + net: transmitterConfig?.net ?? 0, + port: transmitterConfig?.port ?? 6454, + base_refresh_interval: transmitterConfig?.base_refresh_interval ?? 1000 + } + } +} \ No newline at end of file diff --git a/src/controlscreen.ts b/src/controlscreen.ts index a1e62fd..93f1183 100644 --- a/src/controlscreen.ts +++ b/src/controlscreen.ts @@ -52,6 +52,6 @@ export default async function renderControlScreen() { console.log("\n"); console.log(chalk.yellow( - "Close the window or click Ctrl+C to terminate the program" + "Close the window or press Ctrl+C to terminate the program" )) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a855be8..a0b3dca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,16 +4,34 @@ import renderStartupScreen from "./startupscreen"; import renderControlScreen from "./controlscreen"; import {clearInterval} from "timers"; import {exec} from "child_process"; +import ConfigStorage from "./config/ConfigStorage"; + +import minimist from "minimist"; +const argv = minimist(process.argv.slice(2)); export const defaultConvertHandler = new ConvertHandler(); +export const defaultConfigStorage = new ConfigStorage(); let renderControlScreenTimer: NodeJS.Timeout; /** * Entry point of the program */ async function main() { + + if (typeof argv["config"] === "string") { + defaultConfigStorage.loadConfig(argv["config"]); + } + setTerminalTitle("ArtNet => USBDMX") - const selectedInfo = await renderStartupScreen(); + + defaultConvertHandler.scanForInterfaces(); + + const selectedInfo = defaultConfigStorage.getInterface() ?? await renderStartupScreen(); + + if (argv._.includes("outputconfig")) { + console.log(selectedInfo); + return; + } console.log(chalk.blueBright( "Starting ArtNet..." )); @@ -34,7 +52,7 @@ async function main() { openInterfaceResponse ) ) - return process.exit(1); + throw "MissingInterfaceError" } else { console.log( @@ -73,16 +91,24 @@ process.on('uncaughtException', (err) => { chalk.red(`⛔ A fatal error has occurred`) ); console.log(err); - exec("pause press [enter]"); - process.exit(0); + if (argv["autoretry"] === true) { + console.log("Auto-restart enabled, will restart now...") + setTimeout(() => { + main(); + }, 2000); + } else { + exec("pause press [enter]"); + process.exit(0); + } }) + main(); /** * Sets the title of the terminal * @param title new title */ -function setTerminalTitle(title: string){ +function setTerminalTitle(title: string) { process.stdout.write( String.fromCharCode(27) + "]0;" + title + String.fromCharCode(7) ); diff --git a/src/startupscreen.ts b/src/startupscreen.ts index 3e47a97..d7d314d 100644 --- a/src/startupscreen.ts +++ b/src/startupscreen.ts @@ -4,13 +4,14 @@ import figlet from "figlet"; import inquirer from "inquirer"; import {defaultConvertHandler} from "./index"; import modeToString from "./helpers/modeToString"; +import {DetectedInterface} from "./usbdmx"; // eslint-disable-next-line @typescript-eslint/no-var-requires const pjson = require('../package.json'); /** * Data from the user selection about which interface to use */ -interface StartupScreenResponse { +export interface StartupScreenResponse { serial: string, mode: string manufacturer: string | undefined, @@ -25,7 +26,7 @@ export default async function renderStartupScreen(): Promise