diff --git a/README.md b/README.md index 24ecd6bc3..94c8c2712 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ nodecg-io is the successor of [ChatOverflow](https://github.com/codeoverflow-org ## Implemented Services and Interfaces -- AutoHotkey - Android (using adb) +- ArtNet +- AutoHotkey - CurseForge - DBus - Discord diff --git a/nodecg-io-artnet/artnet-schema.json b/nodecg-io-artnet/artnet-schema.json new file mode 100644 index 000000000..e8b4c410c --- /dev/null +++ b/nodecg-io-artnet/artnet-schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "host": { + "type": "string", + "description": "The brodcast host, default '0.0.0.0'", + "default": "0.0.0.0" + } + }, + "required": [] +} diff --git a/nodecg-io-artnet/extension/artnetServiceClient.ts b/nodecg-io-artnet/extension/artnetServiceClient.ts new file mode 100644 index 000000000..26dcc5c9d --- /dev/null +++ b/nodecg-io-artnet/extension/artnetServiceClient.ts @@ -0,0 +1,26 @@ +import { ArtNetController } from "artnet-protocol/dist"; +import { ArtDmx } from "artnet-protocol/dist/protocol"; +import { ArtNetServiceConfig } from "./index"; + +export class ArtNetServiceClient extends ArtNetController { + constructor(config: ArtNetServiceConfig) { + super(); + this.nameShort = "nodecg-io"; + this.nameLong = "https://github.com/codeoverflow-org/nodecg-io"; + this.bind(config.host); + } + + /** + * Little simplification to receive `dmx` data. + */ + onDMX(listener: (packet: ArtDmx) => void): ArtNetServiceClient { + return this.on("dmx", listener); + } + + /** + * Little simplification to send `dmx` data. + */ + public send(universe: number, data: number[]): void { + this.sendBroadcastPacket(new ArtDmx(0, 0, universe, data)); + } +} diff --git a/nodecg-io-artnet/extension/index.ts b/nodecg-io-artnet/extension/index.ts new file mode 100644 index 000000000..ef6e50d92 --- /dev/null +++ b/nodecg-io-artnet/extension/index.ts @@ -0,0 +1,37 @@ +import { NodeCG } from "nodecg-types/types/server"; +import { Result, emptySuccess, success, ServiceBundle } from "nodecg-io-core"; +export { ArtNetServiceClient } from "./artnetServiceClient"; +import { ArtNetServiceClient } from "./artnetServiceClient"; + +export interface ArtNetServiceConfig { + host: string; + port?: number | string; + refresh?: number | string; + iface?: string; + sendAll?: boolean; +} + +module.exports = (nodecg: NodeCG) => { + new ArtNetService(nodecg, "artnet", __dirname, "../artnet-schema.json").register(); +}; + +class ArtNetService extends ServiceBundle { + async validateConfig(): Promise> { + return emptySuccess(); + } + + async createClient(config: ArtNetServiceConfig): Promise> { + const client = new ArtNetServiceClient(config); + + return success(client); + } + + stopClient(client: ArtNetServiceClient): void { + client.close(); + this.nodecg.log.info("Successfully stopped the Art-Net service."); + } + + removeHandlers(client: ArtNetServiceClient): void { + client.removeAllListeners(); + } +} diff --git a/nodecg-io-artnet/package.json b/nodecg-io-artnet/package.json new file mode 100644 index 000000000..7d2ec2665 --- /dev/null +++ b/nodecg-io-artnet/package.json @@ -0,0 +1,47 @@ +{ + "name": "nodecg-io-artnet", + "version": "0.2.0", + "description": "Allows you to send DMX512 data over Art-Netâ„¢ to to Art-Net nodes i.e. professional lighting fixtures.", + "homepage": "https://nodecg.io/RELEASE/samples/artnet", + "author": { + "name": "Tim-Tech-Dev", + "url": "https://github.com/Tim-Tech-Dev" + }, + "repository": { + "type": "git", + "url": "https://github.com/codeoverflow-org/nodecg-io.git", + "directory": "nodecg-io-artnet" + }, + "files": [ + "**/*.js", + "**/*.js.map", + "**/*.d.ts", + "*.json" + ], + "main": "extension/index", + "scripts": { + "build": "tsc -b", + "watch": "tsc -b -w", + "clean": "tsc -b --clean" + }, + "keywords": [ + "nodecg-io", + "nodecg-bundle" + ], + "nodecg": { + "compatibleRange": "^1.1.1", + "bundleDependencies": { + "nodecg-io-core": "^0.2.0" + } + }, + "license": "MIT", + "devDependencies": { + "@types/node": "^15.0.2", + "nodecg-types": "^1.8.2", + "typescript": "^4.2.4" + }, + "dependencies": { + "nodecg-io-core": "^0.2.0", + "artnet-protocol": "^0.2.1" + } +} diff --git a/nodecg-io-artnet/tsconfig.json b/nodecg-io-artnet/tsconfig.json new file mode 100644 index 000000000..1c8405620 --- /dev/null +++ b/nodecg-io-artnet/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.common.json" +} diff --git a/nodecg-io-core/dashboard/bundles.ts b/nodecg-io-core/dashboard/bundles.ts index e2530b646..08825e4ce 100644 --- a/nodecg-io-core/dashboard/bundles.ts +++ b/nodecg-io-core/dashboard/bundles.ts @@ -77,9 +77,8 @@ export function renderInstanceSelector(): void { return; } - const currentInstance = config.data.bundles[bundle]?.find( - (dep) => dep.serviceType === serviceType, - )?.serviceInstance; + const currentInstance = config.data.bundles[bundle]?.find((dep) => dep.serviceType === serviceType) + ?.serviceInstance; let index = 0; for (let i = 0; i < selectBundleInstance.options.length; i++) { diff --git a/nodecg-io-core/extension/serviceBundle.ts b/nodecg-io-core/extension/serviceBundle.ts index 3efa09789..0399435c7 100644 --- a/nodecg-io-core/extension/serviceBundle.ts +++ b/nodecg-io-core/extension/serviceBundle.ts @@ -37,7 +37,7 @@ export abstract class ServiceBundle implements Service { this.schema = this.readSchema(pathSegments); this.nodecg.log.info(this.serviceType + " bundle started."); - this.core = this.nodecg.extensions["nodecg-io-core"] as unknown as NodeCGIOCore | undefined; + this.core = (this.nodecg.extensions["nodecg-io-core"] as unknown) as NodeCGIOCore | undefined; if (this.core === undefined) { this.nodecg.log.error( "nodecg-io-core isn't loaded! " + this.serviceType + " bundle won't function without it.", diff --git a/nodecg-io-core/extension/serviceProvider.ts b/nodecg-io-core/extension/serviceProvider.ts index 1b71abd1c..8fccbace3 100644 --- a/nodecg-io-core/extension/serviceProvider.ts +++ b/nodecg-io-core/extension/serviceProvider.ts @@ -66,7 +66,7 @@ export class ServiceProvider { * or undefined if the core wasn't loaded or the service type doesn't exist. */ export function requireService(nodecg: NodeCG, serviceType: string): ServiceProvider | undefined { - const core = nodecg.extensions["nodecg-io-core"] as unknown as NodeCGIOCore | undefined; + const core = (nodecg.extensions["nodecg-io-core"] as unknown) as NodeCGIOCore | undefined; if (core === undefined) { nodecg.log.error( `nodecg-io-core isn't loaded! Can't require ${serviceType} service for bundle ${nodecg.bundleName}.`, diff --git a/package-lock.json b/package-lock.json index 5e0048ddf..9a0240a0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/twitter": "^1.7.0", "@types/ws": "^7.4.2", "ajv": "^8.2.0", + "artnet-protocol": "^0.2.1", "clean-webpack-plugin": "^3.0.0", "crypto-js": "^4.0.0", "dbus-next": "^0.10.2", @@ -5132,6 +5133,14 @@ "node": ">=0.10.0" } }, + "node_modules/artnet-protocol": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/artnet-protocol/-/artnet-protocol-0.2.1.tgz", + "integrity": "sha512-sHU275w7kfKfn8GnwQ2rt5j2I1XxgoeWj0M3IcyR35vtC9l3snLWNc1p/ilqcZws2+XjYlaCWkK5R83+JtDcvQ==", + "dependencies": { + "ip6addr": "^0.2.3" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -11065,6 +11074,15 @@ "node": ">=8" } }, + "node_modules/ip6addr": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.3.tgz", + "integrity": "sha512-qA9DXRAUW+lT47/i/4+Q3GHPwZjGt/atby1FH/THN6GVATA6+Pjp2nztH7k6iKeil7hzYnBwfSsxjthlJ8lJKw==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.4.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -24427,6 +24445,14 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "artnet-protocol": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/artnet-protocol/-/artnet-protocol-0.2.1.tgz", + "integrity": "sha512-sHU275w7kfKfn8GnwQ2rt5j2I1XxgoeWj0M3IcyR35vtC9l3snLWNc1p/ilqcZws2+XjYlaCWkK5R83+JtDcvQ==", + "requires": { + "ip6addr": "^0.2.3" + } + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -29174,6 +29200,15 @@ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" }, + "ip6addr": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.3.tgz", + "integrity": "sha512-qA9DXRAUW+lT47/i/4+Q3GHPwZjGt/atby1FH/THN6GVATA6+Pjp2nztH7k6iKeil7hzYnBwfSsxjthlJ8lJKw==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.4.0" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/samples/artnet-console/extension/index.ts b/samples/artnet-console/extension/index.ts new file mode 100644 index 000000000..a1a57481f --- /dev/null +++ b/samples/artnet-console/extension/index.ts @@ -0,0 +1,36 @@ +import { NodeCG } from "nodecg-types/types/server"; +import { requireService } from "nodecg-io-core"; +import { ArtNetServiceClient } from "nodecg-io-artnet"; + +module.exports = function (nodecg: NodeCG) { + nodecg.log.info("Sample bundle for Art-Net started"); + + const service = requireService(nodecg, "artnet"); + service?.onAvailable((client) => { + // From this point on is the artnet client available + nodecg.log.info("Art-Net console has been updated, setting up interval for sending test payloads."); + + // Receive DMX data + client.onDMX((dmx) => { + // dmx contains an ArtDmx object + nodecg.log.info(dmx.universe, dmx.data); + }); + + // Send DMX data to every channel and universe. + let value = 0; + setInterval(() => { + // send new data every 0,8 seconds. + // This is the official timing for re-transmiting data in the artnet specifciation. + if (++value > 255) value = 0; + for (let universe = 0; universe < 8; universe++) { + client.send( + universe, + // the values of the 512 channels + Array(512).fill(value), + ); + } + }, 800); + }); + + service?.onUnavailable(() => nodecg.log.info("Art-Net console has been unset.")); +}; diff --git a/samples/artnet-console/package.json b/samples/artnet-console/package.json new file mode 100644 index 000000000..24dab33bb --- /dev/null +++ b/samples/artnet-console/package.json @@ -0,0 +1,34 @@ +{ + "name": "artnet-console", + "homepage": "https://nodecg.io/RELEASE/samples/artnet", + "author": { + "name": "Tim-Tech-Dev", + "url": "https://github.com/Tim-Tech-Dev" + }, + "repository": { + "type": "git", + "url": "https://github.com/codeoverflow-org/nodecg-io.git", + "directory": "samples/artnet-console" + }, + "version": "0.2.0", + "private": true, + "nodecg": { + "compatibleRange": "^1.1.1", + "bundleDependencies": { + "nodecg-io-artnet": "^0.2.0" + } + }, + "scripts": { + "build": "tsc -b", + "watch": "tsc -b -w", + "clean": "tsc -b --clean" + }, + "license": "MIT", + "dependencies": { + "@types/node": "^15.0.2", + "nodecg-types": "^1.8.2", + "nodecg-io-core": "^0.2.0", + "nodecg-io-artnet": "^0.2.0", + "typescript": "^4.2.4" + } +} diff --git a/samples/artnet-console/tsconfig.json b/samples/artnet-console/tsconfig.json new file mode 100644 index 000000000..c8bb01bee --- /dev/null +++ b/samples/artnet-console/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.common.json" +}