Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(release/v1.1.1): code cleanup #9

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# x32-reflector

A simple module that polls x32 to send all parameter changes and then redirects them to a set of configured clients. This allows you to go beyond the 4 client limit imposed by X32 and also allow you to use X32 with software like QLab that has restrictions on OSC data sources.
A simple module that polls x32 to send all parameter changes and then redirects them to a set of configured clients.
This allows you to go beyond the 4 client limit imposed by X32 and also allow you to use X32 with software like QLab
that has restrictions on OSC data sources.

Built at the request of Andrew

Expand All @@ -15,14 +17,59 @@ A summary of the config file is provided but it should be relatively self explan
|`x32`|The ip address and port on the network for X32 - this will be used to direct `/xremote` packets|
|`timeout`|How long, in minutes, a client should remain on the list before being removed automatically. This is suggested to be a long value (such as 1440 for 24 hours) so that clients are not removed part way through a show. |

Annotated details

```typescript
({
"udp": {
// The address on which the UDP sockets should bind. Port is not specified as an ephemeral one is chosen for
// each device
"bind": "0.0.0.0"
},
"http": {
// The address on which the http server should be made available, as normal 0.0.0.0 is for all
"bind": "0.0.0.0",
// The port on which the HTTP server should be made accessible
"port": 1325
},
// The set of X32 instances which are accessible via this reflector. This can either be an array like below or an
// object. If it is an object, it should just be one entry like one in the array. All entries must have a name and
// they must be unique across all entries as it is used for unique identification. Valid names match the regex
// ^[A-Za-z0-9_-]+$
"x32": [
{
"ip": "10.1.10.20",
"port": 10023,
"name": "Primary"
},
{
"ip": "10.1.10.21",
"port": 10023,
"name": "Secondary"
}
],
// The timeout, in minutes, after which an un-renewed client should be removed. In this case it is 24 hours
"timeout": 1440
})
```

## Timeouts

Clients are configured to timeout at a certain point so that the system is not sending packets repeatedly to clients that do not exist. The timeout value should be set high enough to prevent clients from timing out during shows. Additionally it is recommended that clients use the 'Renew' button just before a show which will reset the countdown and make sure they don't expire mid-show.
Clients are configured to timeout at a certain point so that the system is not sending packets repeatedly to clients
that do not exist. The timeout value should be set high enough to prevent clients from timing out during shows.
Additionally it is recommended that clients use the 'Renew' button just before a show which will reset the countdown and
make sure they don't expire mid-show.

## Known Problems

This system does not verify that X32 is online so while running it will constantly send `/xremote` packets every 9 seconds. See issue #1 for more info
This system does not verify that X32 is online so while running it will constantly send `/xremote` packets every 9
seconds. See issue #1 for more info

## Web Interface

The web interface has been designed to be as simple as possible. Simply enter the IP address of the client and the port on which you wish to receive packets and press the button. The page should refresh and your client will be listed and will begin receiving packets. To stop receiving packets, just press Delete or to stop your client timing out just press Renew. The page should refresh every 10 seconds to keep the countdown up to date and the client list accurate. There is a countdown to when each client will timeout which can be used to make sure important clients are not being removed at the wrong time
The web interface has been designed to be as simple as possible. Simply enter the IP address of the client and the port
on which you wish to receive packets and press the button. The page should refresh and your client will be listed and
will begin receiving packets. To stop receiving packets, just press Delete or to stop your client timing out just press
Renew. The page should refresh every 10 seconds to keep the countdown up to date and the client list accurate. There is
a countdown to when each client will timeout which can be used to make sure important clients are not being removed at
the wrong time
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "x32-reflector",
"version": "1.0.0",
"version": "1.1.1",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion res/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ <h1>OSC Reflector</h1>
<h2>Register New Target</h2>
<div>
{{ERROR_INSERT}}
<form action="/register" method="get">
<form action="{{PREFIX}}/register" method="get">
<table>
<tr>
<th>
Expand Down
113 changes: 113 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import os from "os";
import path from "path";
import * as zod from "zod";
import {Socket} from "dgram";
import {promises as fsp} from "fs";
import fs from "fs";

export const DEVICE_NAME_REGEX = /^[A-Za-z0-9_-]+$/;

/**
* HTML main site template loaded from file. Content is cached so program will have to be restarted to pick up new
* changes in the file
*/
export const TEMPLATE = fs.readFileSync('res/index.html', {encoding: 'utf8'});

/**
* The valid locations for a configuration on the machine. The linux based path is removed if the platform is not
* identified as linux
*/
const CONFIG_PATHS: string[] = [
os.platform() === 'linux' ? '/etc/ents/x32-reflector.json' : undefined,
os.platform() === 'linux' ? path.join('~', '.x32-reflector.config.json') : undefined,
path.join(__dirname, '..', '..', 'config', 'config.json'),
].filter((e) => e !== undefined) as string[];

/**
* Validator against the x32 instance entries in the configuration file
*/
const X32_INSTANCE_VALIDATOR = zod.object({
name: zod.string().regex(DEVICE_NAME_REGEX),
ip: zod.string(),
port: zod.number(),
});
/**
* A unique instance of X32 connections
*/
export type X32Instance = zod.infer<typeof X32_INSTANCE_VALIDATOR>;
/**
* An x32 connection item along with the socket which is bound to it and its responses
*/
export type X32InstanceWithSocket = X32Instance & { socket: Socket };
/**
* The validator for the configuration which contains udp and http bind and listen ports as well as timeouts for pairs
*/
const CONFIG_VALIDATOR = zod.object({
udp: zod.object({
bind: zod.string(),
}),
http: zod.object({
bind: zod.string(),
port: zod.number(),
prefix: zod.string().optional(),
}),
x32: X32_INSTANCE_VALIDATOR.or(zod.array(X32_INSTANCE_VALIDATOR)),
timeout: zod.number(),
siteRoot: zod.string().regex(/\/$/, {message: 'Path must end in a /'}).default('/'),
});
/**
* The derived type of the configuration from the validator
*/
export type Configuration = zod.infer<typeof CONFIG_VALIDATOR>;


/**
* Attempts to load the configuration from disk and return it if one is found as a safely parsed object. If no config
* can be loaded it will throw an error
* @param paths custom set of locations to test for configuration files, defaults to {@link CONFIG_PATHS}
*/
export async function loadConfiguration(paths: string[] = CONFIG_PATHS): Promise<Configuration> {
for (const file of paths) {
console.log(`[config]: trying to load config from path ${file}`);
let content;

// Try and read file from disk
try {
content = await fsp.readFile(file, {encoding: 'utf8'});
} catch (e) {
console.warn(`[config]: could not load configuration file ${file} due to an error: ${e}`)
continue;
}

// Parse it as JSON and fail it out if its not
try {
content = JSON.parse(content);
} catch (e) {
console.warn(`[config]: Failed to load the JSON data at path ${file} due to error: ${e}`);
continue;
}

// Try and parse it as a config file and reject if the file was not valid with the zod errors7
// Try and be as helpful with the output as possible
let safeParse = CONFIG_VALIDATOR.safeParse(content);
if (!safeParse.success) {
const reasons = safeParse.error.message + safeParse.error.errors.map((e) => `${e.message} (@ ${e.path.join('.')}`).join(', ');
console.warn(`[config]: content in ${file} is not valid: ${reasons}`);
continue;
}

// Last bit of validation about names
if (Array.isArray(safeParse.data.x32)) {
if (!safeParse.data.x32.map((e) => e.name).every((v, i, a) => a.indexOf(v) === i)) {
// If any name is not unique
console.warn(`[config]: config is invalid because multiple X32 instances with the same name were identified`);
continue;
}
}

console.log(`[config]: config loaded from ${file}`);
return safeParse.data;
}

throw new Error(`No valid configuration found, scanned: ${CONFIG_PATHS.join(', ')}`);
}
Loading