├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── CHANGELOG.md ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md ├── spec ├── anim.spec.ts └── test.ts ├── src ├── Animation.ts ├── DMX.ts ├── demo │ ├── demo.ts │ ├── demo_simple.ts │ └── demo_socket_client.ts ├── devices.ts ├── drivers │ ├── abstract-serial-driver.ts │ ├── artnet.ts │ ├── bbdmx.ts │ ├── dmx4all.ts │ ├── dmxking-ultra-dmx-pro.ts │ ├── enttec-open-usb-dmx.ts │ ├── enttec-usb-dmx-pro.ts │ ├── null.ts │ ├── sacn.ts │ └── socketio.ts ├── easing.ts ├── index.ts ├── models │ ├── Events.ts │ └── IUniverseDriver.ts └── util │ └── time.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | *.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | 9 | "globals": { 10 | "document": false, 11 | "escape": false, 12 | "navigator": false, 13 | "unescape": false, 14 | "window": false, 15 | "describe": true, 16 | "before": true, 17 | "it": true, 18 | "expect": true, 19 | "sinon": true 20 | }, 21 | 22 | "parser": "@typescript-eslint/parser", 23 | "plugins": [ 24 | "@typescript-eslint" 25 | ], 26 | 27 | "parserOptions": { 28 | "sourceType": "module" 29 | }, 30 | 31 | "rules": { 32 | "block-scoped-var": 2, 33 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 34 | "camelcase": [2, { "properties": "always" }], 35 | "comma-dangle": [2, "always-multiline"], 36 | "comma-spacing": [2, { "before": false, "after": true }], 37 | "comma-style": [2, "last"], 38 | "complexity": 0, 39 | "consistent-return": 2, 40 | "consistent-this": 0, 41 | "curly": [2, "multi-line"], 42 | "default-case": 0, 43 | "dot-location": [2, "property"], 44 | "dot-notation": 0, 45 | "eol-last": 2, 46 | "eqeqeq": [2, "allow-null"], 47 | "func-names": 0, 48 | "func-style": 0, 49 | "generator-star-spacing": [2, "both"], 50 | "guard-for-in": 0, 51 | "handle-callback-err": [0, "^(err|error|anySpecificError)$" ], 52 | "indent": [2, 2, { "SwitchCase": 1 }], 53 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 54 | "keyword-spacing": [2, {"before": true, "after": true}], 55 | "linebreak-style": 0, 56 | "max-depth": 0, 57 | "max-len": [2, 120, 4], 58 | "max-nested-callbacks": 0, 59 | "max-params": 0, 60 | "max-statements": 0, 61 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 62 | "newline-after-var": [2, "always"], 63 | "new-parens": 2, 64 | "no-alert": 0, 65 | "no-array-constructor": 2, 66 | "no-bitwise": 0, 67 | "no-caller": 2, 68 | "no-catch-shadow": 0, 69 | "no-cond-assign": 2, 70 | "no-console": 0, 71 | "no-constant-condition": 0, 72 | "no-continue": 0, 73 | "no-control-regex": 2, 74 | "no-debugger": 2, 75 | "no-delete-var": 2, 76 | "no-div-regex": 0, 77 | "no-dupe-args": 2, 78 | "no-dupe-keys": 2, 79 | "no-duplicate-case": 2, 80 | "no-else-return": 2, 81 | "no-empty": 0, 82 | "no-empty-character-class": 2, 83 | "no-eq-null": 0, 84 | "no-eval": 2, 85 | "no-ex-assign": 2, 86 | "no-extend-native": 2, 87 | "no-extra-bind": 2, 88 | "no-extra-boolean-cast": 2, 89 | "no-extra-parens": 0, 90 | "no-extra-semi": 0, 91 | "no-extra-strict": 0, 92 | "no-fallthrough": 2, 93 | "no-floating-decimal": 2, 94 | "no-func-assign": 2, 95 | "no-implied-eval": 2, 96 | "no-inline-comments": 0, 97 | "no-inner-declarations": [2, "functions"], 98 | "no-invalid-regexp": 2, 99 | "no-irregular-whitespace": 2, 100 | "no-iterator": 2, 101 | "no-label-var": 2, 102 | "no-labels": 2, 103 | "no-lone-blocks": 0, 104 | "no-lonely-if": 0, 105 | "no-loop-func": 0, 106 | "no-mixed-requires": 0, 107 | "no-mixed-spaces-and-tabs": [2, false], 108 | "no-multi-spaces": 2, 109 | "no-multi-str": 2, 110 | "no-multiple-empty-lines": [2, { "max": 1 }], 111 | "no-native-reassign": 2, 112 | "no-negated-in-lhs": 2, 113 | "no-nested-ternary": 0, 114 | "no-new": 2, 115 | "no-new-func": 2, 116 | "no-new-object": 2, 117 | "no-new-require": 2, 118 | "no-new-wrappers": 2, 119 | "no-obj-calls": 2, 120 | "no-octal": 2, 121 | "no-octal-escape": 2, 122 | "no-path-concat": 0, 123 | "no-plusplus": 0, 124 | "no-process-env": 0, 125 | "no-process-exit": 0, 126 | "no-proto": 2, 127 | "no-redeclare": 2, 128 | "no-regex-spaces": 2, 129 | "no-reserved-keys": 0, 130 | "no-restricted-modules": 0, 131 | "no-return-assign": 2, 132 | "no-script-url": 0, 133 | "no-self-compare": 2, 134 | "no-sequences": 2, 135 | "no-shadow": 0, 136 | "no-shadow-restricted-names": 2, 137 | "no-spaced-func": 2, 138 | "no-sparse-arrays": 2, 139 | "no-sync": 0, 140 | "no-ternary": 0, 141 | "no-throw-literal": 2, 142 | "no-trailing-spaces": 2, 143 | "no-undef": 2, 144 | "no-undef-init": 2, 145 | "no-undefined": 0, 146 | "no-underscore-dangle": 0, 147 | "no-unneeded-ternary": 2, 148 | "no-unreachable": 1, 149 | "no-unused-expressions": 0, 150 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 151 | "no-use-before-define": 2, 152 | "no-var": 2, 153 | "no-void": 0, 154 | "no-warning-comments": 0, 155 | "no-with": 2, 156 | "one-var": 0, 157 | "operator-assignment": 0, 158 | "operator-linebreak": [2, "after"], 159 | "padded-blocks": 0, 160 | "prefer-const": 2, 161 | "quote-props": 0, 162 | "quotes": [2, "single", "avoid-escape"], 163 | "radix": 2, 164 | "semi": [2, "always"], 165 | "semi-spacing": 0, 166 | "sort-vars": 0, 167 | "space-before-blocks": [2, "always"], 168 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 169 | "space-in-brackets": 0, 170 | "space-in-parens": [2, "never"], 171 | "space-infix-ops": 2, 172 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 173 | "spaced-comment": [2, "always"], 174 | "strict": 2, 175 | "use-isnan": 2, 176 | "valid-jsdoc": 0, 177 | "valid-typeof": 2, 178 | "vars-on-top": 0, 179 | "wrap-iife": [2, "any"], 180 | "wrap-regex": 0, 181 | "yoda": [2, "never"] 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | name: Build & Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Install NodeJS 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '16' 18 | - name: Install Dependencies 19 | run: npm install 20 | - name: Lint 21 | run: npm run lint 22 | - name: Build 23 | run: npm run build 24 | - name: Test 25 | run: npm run test 26 | - name: NPM Publish 27 | if: startsWith(github.ref, 'refs/tags/v') 28 | uses: JS-DevTools/npm-publish@v1 29 | with: 30 | token: ${{ secrets.NPM_ACCESS_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | *.tgz 11 | 12 | pids 13 | logs 14 | results 15 | .DS_Store 16 | 17 | node_modules 18 | .project 19 | .com.greenworldsoft.syncfolderspro 20 | 21 | # Remove some common IDE working directories 22 | .idea/*.xml 23 | .idea/*.iml 24 | .idea/inspectionProfiles 25 | .vscode 26 | 27 | .DS_Store 28 | coverage 29 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## `v0.4.0` 4 | * Update dependencies 5 | * Add optional options to sACN driver 6 | 7 | ## `v0.3.0` 8 | * Update Dependencies (thanks @barlock) 9 | 10 | ## `v0.2.1` 11 | 12 | * Export `IUniverseDriver` and `UniverseData` 13 | *+* Fixed ArtNet Driver 14 | 15 | ## `v0.2.0` 16 | 17 | * New Artnet lib to support multiple universes, nets and subnets 18 | * Fixed sACN driver not exported 19 | 20 | ## `v0.1.1` 21 | 22 | * Update dependencies 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2018 Sebastian Wiedenroth & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dmx-ts", 3 | "version": "0.4.0", 4 | "author": "Sebastian Wiedenroth ", 5 | "description": "A nodejs DMX library", 6 | "url": "https://github.com/node-dmx/dmx-ts", 7 | "main": "dist/src/index.js", 8 | "scripts": { 9 | "test": "npm run jest", 10 | "lint": "npx eslint . -c .eslintrc --ext .ts", 11 | "jest": "jest", 12 | "build": "tsc", 13 | "prepack": "npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/node-dmx/dmx-ts.git" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "keywords": [ 23 | "DMX", 24 | "light control", 25 | "typescript" 26 | ], 27 | "dependencies": { 28 | "dmxnet": "^0.9.0", 29 | "sacn": "^4.4.0", 30 | "serialport": "^12.0.0", 31 | "socket.io": "^4.7.4" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^29.5.12", 35 | "@types/node": "^20.11.16", 36 | "@typescript-eslint/eslint-plugin": "^6.21.0", 37 | "@typescript-eslint/parser": "^6.21.0", 38 | "eslint": "^8.56.0", 39 | "jest": "^29.7.0", 40 | "ts-loader": "^9.5.1", 41 | "typescript": "^5.3.3" 42 | }, 43 | "license": "MIT", 44 | "engines": { 45 | "node": ">=10.0.0" 46 | }, 47 | "jest": { 48 | "clearMocks": true, 49 | "collectCoverage": true, 50 | "coverageDirectory": "coverage", 51 | "coverageReporters": [ 52 | "lcov", 53 | "text" 54 | ], 55 | "roots": [ 56 | "dist/" 57 | ], 58 | "testEnvironment": "node" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `dmx-ts` 2 | > DMX-512 controller library for node.js 3 | 4 | This is a TypeScript rewrite of [`dmx`](https://github.com/node-dmx/dmx). 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm install dmx-ts 10 | ``` 11 | 12 | ## Library API 13 | ```javascript 14 | // CommonJS style require 15 | const { DMX } = require('dmx-ts'); 16 | // or ES import 17 | import { DMX } from "dmx-ts"; 18 | ``` 19 | 20 | ### Class `DMX` 21 | 22 | #### `new DMX()` 23 | 24 | Create a new DMX instance. This class is used to tie multiple universes together. 25 | 26 | #### dmx.addUniverse(name, driver) 27 | 28 | - name - String 29 | - driver - Instance of a Driver class, see below 30 | 31 | Add a new DMX Universe with a name and driver. 32 | 33 | The following Drivers are available: 34 | 35 | **Development Drivers:** 36 | - `NullDriver()`: a development driver that prints the universe to stdout 37 | 38 | **Network Drivers:** 39 | - `SocketIODriver()`: a driver which sends out the universe via socket.IO as an array (see [demo_socket_client.js](src/demo/demo_socket_client.js) as a client example) 40 | - `ArtnetDriver()`: driver for Artnet 41 | - `SACNDriver()`: driver for sACN 42 | - `BBDMXDriver()`: driver for [BeagleBone-DMX](https://github.com/boxysean/beaglebone-DMX) 43 | 44 | **Serial Drivers (USB):** 45 | - `DMX4AllDriver()`: driver for DMX4ALL devices like the "NanoDMX USB Interface" 46 | - `EnttecUSBDMXProDriver()`: a driver for devices using a Enttec USB DMX Pro chip like the "DMXKing ultraDMX Micro". 47 | - `EnttecOpenUSBDMXDriver()`: driver for "Enttec Open DMX USB". This device is NOT recommended, there are known hardware limitations and this driver is not very stable. (If possible better obtain a device with the "pro" chip) 48 | - `DMXKingUltraDMXProDriver()`: driver for the DMXKing Ultra DMX pro interface. This driver support multiple universe specify the options with Port = A or B 49 | 50 | Each driver has its own options. Example: 51 | ```TypeScript 52 | const universe1 = dmx.addUniverse('demo1', new NullDriver()); 53 | const universe2 = dmx.addUniverse('demo2', new ArtnetDriver("127.0.0.1")); 54 | const universe2 = dmx.addUniverse('demo3', new EnttecUSBDMXProDriver("COM5", { dmxSpeed: 40 })); 55 | ``` 56 | 57 | #### `dmx.update(universe, channels[, extraData])` 58 | 59 | - universe - String, name of the universe 60 | - channels - Object, keys are channel numbers, values the values to set that channel to 61 | - extraData - Object, this data will be passed unmodified to the update Event. (Optional; default value is `{}`) 62 | 63 | Update one or multiple channels of a universe. Also emits a update Event with the same information. 64 | 65 | 66 | #### `DMX.devices` 67 | 68 | A JSON Object describing some Devices and how many channels they use. 69 | Currently not many devices are in there but more can be added to the devices.js file. Pull requests welcome ;-) 70 | 71 | The following Devices are known: 72 | 73 | - generic - a one channel dimmer 74 | - showtec-multidim2 - 4 channel dimmer with 4A per channel 75 | - eurolite-led-bar - Led bar with 3 RGB color segments and some programms 76 | - stairville-led-par-56 - RGB LED Par Can with some programms 77 | 78 | ### Class `Animation` 79 | 80 | #### `new Animation([options])` 81 | 82 | Create a new DMX Animation instance. This can be chained similar to jQuery. 83 | 84 | The options Object takes the following keys: 85 | 86 | - loop - Number, the number of times this animation sequence will loop when run is invoked. This value is overridden if you invoke runLoop. 87 | - filter - Function, allows you to read or modify the values being set to each channel during each animation step. 88 | 89 | If you specify a filter function, it must take a single object parameter in which keys are channel numbers and values are the values to set those channels to. 90 | You may modify the values in the object to override the values in real-time, for example to scale channel brightness based on a master fader. 91 | 92 | #### `animation.add(to, duration, options)` 93 | 94 | - to - Object, keys are channel numbers, values the values to set that channel to 95 | - duration - Number, duration in ms 96 | - options - Object 97 | 98 | Add an animation Step. 99 | The options Object takes an easing key which allows to set a easing function from the following list: 100 | 101 | - `linear` (default) 102 | - `inQuad` 103 | - `outQuad` 104 | - `inOutQuad` 105 | - `inCubic` 106 | - `outCubic` 107 | - `inOutCubic` 108 | - `inQuart` 109 | - `outQuart` 110 | - `inOutQuart` 111 | - `inQuint` 112 | - `outQuint` 113 | - `inOutQuint` 114 | - `inSine` 115 | - `outSine` 116 | - `inOutSine` 117 | - `inExpo` 118 | - `outExpo` 119 | - `inOutExpo` 120 | - `inCirc` 121 | - `outCirc` 122 | - `inOutCirc` 123 | - `inElastic` 124 | - `outElastic` 125 | - `inOutElastic` 126 | - `inBack` 127 | - `outBack` 128 | - `inOutBack` 129 | - `inBounce` 130 | - `outBounce` 131 | - `inOutBounce` 132 | 133 | Returns an `Animation` object with the animation step added. 134 | 135 | 136 | #### `animation.delay(duration)` 137 | 138 | - duration - Number, duration in ms 139 | 140 | Delay the next animation step for duration. 141 | Returns an `Animation` object with the delay step added. 142 | 143 | 144 | #### `animation.run(universe, onFinish)` 145 | 146 | - universe - Object, reference to the universe driver 147 | - onFinish - Function, called when the animation is done 148 | 149 | Run the Animation on the specified universe. 150 | 151 | #### `animation.runLoop(universe)` 152 | 153 | - universe - Object, reference to the universe driver 154 | 155 | Runs an animation constantly until animation.stop() is called 156 | 157 | The example below shows a value being animated for 5 seconds: 158 | ```javascript 159 | const animation = new Animation().add({ 160 | 1: 255, 161 | }, 100).add({ 162 | 1: 0, 163 | }, 100).runLoop(universe) 164 | 165 | 166 | setTimeout(() => { 167 | animation.stop() 168 | }, 5000) 169 | ``` 170 | 171 | #### `update` Event 172 | 173 | - universe - String, name of the universe 174 | - channels - Object, keys are channel numbers, values the values to set that channel to 175 | - extraData - Object, data that was passed to the update method. 176 | 177 | This event is emitted whenever update is called either by the integrating application or by an animation step. 178 | 179 | If triggered by an animation step, extraData.origin will be the string 'animation'. 180 | 181 | ## Develop 182 | 183 | ### Publish to 184 | 1. Run `npm version [patch / minor / major / [custom version number]]` 185 | 1. Run `git tag v[yourVersion]` 186 | 1. Push the tag to GitHub using `git push --tags` 187 | 188 | ## License 189 | [MIT](./LICENSE) 190 | -------------------------------------------------------------------------------- /spec/anim.spec.ts: -------------------------------------------------------------------------------- 1 | import {Animation, DMX} from '../src'; 2 | import {NullDriver} from '../src/drivers/null'; 3 | import {IUniverseDriver} from '../src/models/IUniverseDriver'; 4 | 5 | describe('Animations', () => { 6 | 7 | let dmx: DMX; 8 | let universeDriver: IUniverseDriver; 9 | 10 | beforeAll(async () => { 11 | dmx = new DMX(); 12 | universeDriver = new NullDriver(); 13 | await dmx.addUniverse('test', universeDriver); 14 | }); 15 | 16 | afterAll(async () => { 17 | await universeDriver.close(); 18 | }); 19 | 20 | const ANIM_PRECISION = 50; 21 | 22 | test('fake timers', () => { 23 | const updateMock = jest.fn(); 24 | 25 | universeDriver.update = updateMock; 26 | universeDriver.update({1: 255}); 27 | 28 | jest.useFakeTimers(); 29 | 30 | new Animation().add({ 31 | 1: 255, 32 | }, 100).add({ 33 | 1: 0, 34 | }, 100).run(universeDriver); 35 | 36 | jest.runAllTimers(); 37 | 38 | expect(updateMock).toHaveBeenCalledWith({1: 255}, {origin: 'animation'}); 39 | expect(updateMock).toHaveBeenCalledWith({1: 0}, {origin: 'animation'}); 40 | }); 41 | 42 | test('real timers', done => { 43 | universeDriver.update = jest.fn(); 44 | universeDriver.update({1: 255}); 45 | 46 | jest.useRealTimers(); 47 | 48 | const startAt = Date.now(); 49 | 50 | new Animation().add({ 51 | 1: 255, 52 | }, 250).add({ 53 | 1: 0, 54 | }, 250).run(universeDriver, async () => { 55 | await universeDriver.close(); 56 | const timeTook = Date.now() - startAt; 57 | 58 | expect(timeTook).toBeGreaterThanOrEqual(500 - ANIM_PRECISION); 59 | expect(timeTook).toBeLessThanOrEqual(500 + ANIM_PRECISION); 60 | done(); 61 | }); 62 | 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/test.ts: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('works', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/Animation.ts: -------------------------------------------------------------------------------- 1 | import { IUniverseDriver } from './models/IUniverseDriver'; 2 | import { ease } from './easing'; 3 | 4 | export interface AnimationArgs { 5 | loop?: number; 6 | filter?: any; 7 | } 8 | 9 | export class Animation { 10 | public loops: number; 11 | public frameDelay: number; 12 | public animations: any[]; 13 | public lastAnimation: number; 14 | public timeout: any; 15 | public duration: number; 16 | public startTime: any; 17 | public currentLoop: number; 18 | public filter: any; 19 | 20 | constructor(args: AnimationArgs = {}) { 21 | this.frameDelay = 1; 22 | this.animations = []; 23 | this.lastAnimation = 0; 24 | this.timeout = null; 25 | this.duration = 0; 26 | this.startTime = null; 27 | this.loops = args.loop || 1; 28 | this.currentLoop = 0; 29 | this.filter = args.filter; 30 | } 31 | 32 | add(to: any, duration = 0, options: any = {}): this { 33 | options.easing = options.easing || 'linear'; 34 | 35 | this.animations.push({ 36 | to, 37 | options, 38 | start: this.duration, 39 | end: this.duration + duration, 40 | }); 41 | this.duration += duration; 42 | 43 | return this; 44 | } 45 | 46 | delay(duration: number): this { 47 | this.add({}, duration); 48 | return this; 49 | } 50 | 51 | stop(): void { 52 | if (this.timeout) { 53 | clearTimeout(this.timeout); 54 | } 55 | } 56 | 57 | reset(startTime = new Date().getTime()): void { 58 | this.startTime = startTime; 59 | this.lastAnimation = 0; 60 | } 61 | 62 | runNextLoop(universe: IUniverseDriver, onFinish?: () => void): this { 63 | const runAnimationStep = (): void => { 64 | const now = new Date().getTime(); 65 | const elapsedTime = now - this.startTime; 66 | 67 | this.timeout = setTimeout(runAnimationStep, this.frameDelay); 68 | 69 | // Find the animation for the current point in time, the latest if multiple match 70 | 71 | let currentAnimation = this.lastAnimation; 72 | 73 | while ( 74 | currentAnimation < this.animations.length && 75 | elapsedTime >= this.animations[currentAnimation].end 76 | ) { 77 | currentAnimation++; 78 | } 79 | 80 | // Ensure final state of all newly completed animations have been set 81 | const completedAnimations = this.animations.slice( 82 | this.lastAnimation, 83 | currentAnimation 84 | ); 85 | 86 | // Ensure future animations interpolate from the most recent state 87 | completedAnimations.forEach(completedAnimation => { 88 | delete completedAnimation.from; 89 | }); 90 | 91 | if (completedAnimations.length) { 92 | const completedAnimationStatesToSet = Object.assign( 93 | {}, 94 | ...completedAnimations.map(a => a.to) 95 | ); 96 | 97 | if (typeof this.filter === 'function') { 98 | this.filter(completedAnimationStatesToSet); 99 | } 100 | 101 | universe.update(completedAnimationStatesToSet, {origin: 'animation'}); 102 | } 103 | 104 | this.lastAnimation = currentAnimation; 105 | 106 | if (elapsedTime >= this.duration) { 107 | // This animation loop is complete 108 | this.currentLoop++; 109 | this.stop(); 110 | if (this.currentLoop >= this.loops) { 111 | // All loops complete 112 | if (onFinish) { 113 | onFinish(); 114 | } 115 | } else { 116 | // Run next loop 117 | this.reset(this.startTime + this.duration); 118 | this.runNextLoop(universe); 119 | } 120 | } else { 121 | // Set intermediate channel values during an animation 122 | const animation = this.animations[currentAnimation]; 123 | const easing = (ease as any)[animation.options.easing]; 124 | const duration = animation.end - animation.start; 125 | const animationElapsedTime = elapsedTime - animation.start; 126 | 127 | if (!animation.from) { 128 | animation.from = {}; 129 | for (const k in animation.to) { 130 | animation.from[k] = universe?.get(Number(k)); 131 | } 132 | if (animation.options.from) { 133 | animation.from = Object.assign(animation.from, animation.options.from); 134 | } 135 | } 136 | 137 | if (duration) { 138 | const easeProgress = easing( 139 | Math.min(animationElapsedTime, duration), 140 | 0, 141 | 1, 142 | duration 143 | ); 144 | const intermediateValues: any = {}; 145 | 146 | for (const k in animation.to) { 147 | const startValue = animation.from[k]; 148 | const endValue = animation.to[k]; 149 | 150 | intermediateValues[k] = Math.round( 151 | startValue + easeProgress * (endValue - startValue) 152 | ); 153 | } 154 | 155 | if (typeof this.filter === 'function') { 156 | this.filter(intermediateValues); 157 | } 158 | 159 | universe.update(intermediateValues, {origin: 'animation'}); 160 | } 161 | } 162 | }; 163 | 164 | runAnimationStep(); 165 | 166 | return this; 167 | } 168 | 169 | run(universe: IUniverseDriver, onFinish?: () => void): void { 170 | if ((universe as any).interval) { 171 | // Optimisation to run animation updates at double the rate of driver updates using Nyquist's theorem 172 | this.frameDelay = (universe as any).interval / 2; 173 | } 174 | this.reset(); 175 | this.currentLoop = 0; 176 | this.runNextLoop(universe, onFinish); 177 | } 178 | 179 | runLoop(universe: IUniverseDriver, onFinish?: () => void, loops = Infinity): this { 180 | this.loops = loops; 181 | this.run(universe, onFinish); 182 | return this; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/DMX.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { Devices, PredefinedDevices } from './devices'; 3 | import { Events } from './models/Events'; 4 | import { IUniverseDriver } from './models/IUniverseDriver'; 5 | 6 | export interface DmxArgs { 7 | devices?: any; 8 | } 9 | 10 | export class DMX extends EventEmitter { 11 | private readonly _devices: Devices; 12 | private readonly _universesByName: Map = new Map(); 13 | constructor(options?: DmxArgs) { 14 | super(); 15 | const devices = options?.devices ?? {}; 16 | 17 | this._devices = Object.assign({}, PredefinedDevices, devices); 18 | } 19 | 20 | async addUniverse(name: string, universe: IUniverseDriver): Promise { 21 | await universe.init(); 22 | 23 | universe.on(Events.update, (channels, extraData) => { 24 | this.emit(Events.update, name, channels, extraData); 25 | }); 26 | 27 | this._universesByName.set(name, universe); 28 | 29 | return universe; 30 | } 31 | 32 | update(universeName: string, channels: {[key: number]: number}, extraData?: any): void { 33 | const universe = this._universesByName.get(universeName); 34 | 35 | if (universe === undefined) { 36 | throw new Error(`Universe ${universe} does not exist`); 37 | } 38 | universe.update(channels, extraData || {}); 39 | } 40 | 41 | updateAll(universe: string, value: number): void { 42 | this._universesByName.get(universe)?.updateAll(value); 43 | this.emit(Events.updateAll, universe, value); 44 | } 45 | 46 | universeToObject(universeKey: string): {[key: number]: number} { 47 | const universe = this._universesByName.get(universeKey); 48 | const u: {[key: number]: number} = {}; 49 | 50 | for (let i = 0; i < 512; i++) { 51 | u[i] = universe?.get(i) || 0; 52 | } 53 | 54 | return u; 55 | } 56 | 57 | async close(): Promise { 58 | for (const uni of this._universesByName.values()) { 59 | await uni.close(); 60 | } 61 | this.removeAllListeners(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/demo/demo.ts: -------------------------------------------------------------------------------- 1 | import {Animation} from '../Animation'; 2 | import {NullDriver} from '../drivers/null'; 3 | import {DMX} from '../'; 4 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 5 | 6 | const run = async () => { 7 | 8 | const dmx = new DMX(); 9 | 10 | // var universe = dmx.addUniverse('demo', 'enttec-usb-dmx-pro', '/dev/cu.usbserial-6AVNHXS8') 11 | // var universe = dmx.addUniverse('demo', 'enttec-open-usb-dmx', '/dev/cu.usbserial-6AVNHXS8') 12 | // const universe = dmx.addUniverse('demo', 'socketio', null, {port: 17809, debug: true}); 13 | const universe = await dmx.addUniverse('demo', new NullDriver()); 14 | 15 | universe.update({1: 1, 2: 0}); 16 | universe.update({16: 1, 17: 255}); 17 | universe.update({1: 255, 3: 120, 4: 230, 5: 30, 6: 110, 7: 255, 8: 10, 9: 255, 10: 255, 11: 0}); 18 | 19 | function greenWater(universe: IUniverseDriver, channels: UniverseData, duration: number): void { 20 | const colors = [ 21 | [160, 230, 20], 22 | [255, 255, 0], 23 | [110, 255, 10], 24 | ]; 25 | 26 | for (const c in channels) { 27 | const r = Math.floor((Math.random() * colors.length)); 28 | const u: UniverseData = {}; 29 | 30 | for (let i = 0; i < 3; i++) { 31 | u[channels[c] + i] = colors[r][i]; 32 | } 33 | new Animation().add(u, duration).run(universe); 34 | } 35 | setTimeout(() => greenWater(universe, channels, duration), duration * 2); 36 | } 37 | 38 | function warp(universe: IUniverseDriver, channel: number, min: number, max: number, duration: number): void { 39 | const a: UniverseData = {}; 40 | const b: UniverseData = {}; 41 | 42 | a[channel] = min; 43 | b[channel] = max; 44 | new Animation().add(a, duration).add(b, duration).run(universe, function () { 45 | warp(universe, channel, min, max, duration); 46 | }); 47 | } 48 | 49 | warp(universe, 1, 200, 220, 360); 50 | warp(universe, 1 + 15, 200, 255, 240); 51 | greenWater(universe, [3, 6, 9], 4000); 52 | greenWater(universe, [3 + 15, 6 + 15, 9 + 15], 4000); 53 | 54 | }; 55 | 56 | run() 57 | .catch((err) => console.error(err)); 58 | -------------------------------------------------------------------------------- /src/demo/demo_simple.ts: -------------------------------------------------------------------------------- 1 | import {NullDriver} from '../drivers/null'; 2 | import {DMX} from '../index'; 3 | 4 | const dmx = new DMX(); 5 | 6 | // var universe = dmx.addUniverse('demo', 'enttec-open-usb-dmx', '/dev/cu.usbserial-6AVNHXS8') 7 | // const universe = dmx.addUniverse('demo', 'socketio', null, {port: 17809, debug: true}); 8 | const run = async () => { 9 | const universe = await dmx.addUniverse('demo', new NullDriver()); 10 | 11 | let on = false; 12 | 13 | setInterval(() => { 14 | if (on) { 15 | on = false; 16 | universe.updateAll(0); 17 | console.log('off'); 18 | } else { 19 | on = true; 20 | universe.updateAll(250); 21 | console.log('on'); 22 | } 23 | }, 1000); 24 | }; 25 | 26 | run() 27 | .catch((err) => console.error(err)); 28 | -------------------------------------------------------------------------------- /src/demo/demo_socket_client.ts: -------------------------------------------------------------------------------- 1 | const port = 17809; 2 | const io = require('socket.io-client'); 3 | const client = io.connect(`http://localhost:${port}`); 4 | 5 | client.on('update', (msg: any) => console.info(msg)); 6 | -------------------------------------------------------------------------------- /src/devices.ts: -------------------------------------------------------------------------------- 1 | export interface Device { 2 | channels: string[]|number[]; 3 | ranges?: any; 4 | channelgroups?: string[]; 5 | } 6 | 7 | export type Devices = { [key: string]: Device }; 8 | 9 | export const PredefinedDevices: Devices = { 10 | 'generic': { 11 | channels: ['dimmer'], 12 | }, 13 | 'generic-rgb': { 14 | channels: ['red', 'green', 'blue'], 15 | }, 16 | 'showtec-multidim2': { 17 | channels: ['1', '2', '3', '4'], 18 | }, 19 | 'eurolite-led-bar': { 20 | channels: [ 21 | 'ctrl', 22 | 'dimmer', 23 | 'strobe', 24 | 'red0', 25 | 'green0', 26 | 'blue0', 27 | 'red1', 28 | 'green1', 29 | 'blue1', 30 | 'red2', 31 | 'green2', 32 | 'blue2', 33 | ], 34 | ranges: { 35 | 'ctrl': { 36 | 'type': 'option', 37 | 'options': [ 38 | {'value': 0, 'label': 'Black Out'}, 39 | {'value': 1, 'label': 'Dimmer 1'}, 40 | {'value': 16, 'label': 'Dimmer 2'}, 41 | {'value': 32, 'label': 'Red'}, 42 | {'value': 48, 'label': 'Green'}, 43 | {'value': 64, 'label': 'Blue'}, 44 | {'value': 80, 'label': 'Purple'}, 45 | {'value': 96, 'label': 'Yellow'}, 46 | {'value': 112, 'label': 'Cyan'}, 47 | {'value': 128, 'label': 'White'}, 48 | {'value': 144, 'label': 'Color change'}, 49 | {'value': 160, 'label': 'Color flow'}, 50 | {'value': 176, 'label': 'Color dream'}, 51 | {'value': 192, 'label': 'Multi flow'}, 52 | {'value': 208, 'label': 'Dream flow'}, 53 | {'value': 224, 'label': 'Two color flow'}, 54 | {'value': 240, 'label': 'Sound activity'}, 55 | ], 56 | }, 57 | 'dimmer': { 58 | 'type': 'slider', 59 | 'min': 0, 60 | 'max': 255, 61 | }, 62 | }, 63 | }, 64 | 'stairville-led-par-56': { 65 | channels: ['ctrl', 'red', 'green', 'blue', 'speed'], 66 | ranges: { 67 | 'ctrl': { 68 | 'type': 'option', 69 | 'options': [ 70 | {'value': 0, 'label': 'RGB Control'}, 71 | {'value': 64, 'label': '7 color fade'}, 72 | {'value': 128, 'label': '7 color change'}, 73 | {'value': 192, 'label': '3 color change'}, 74 | ], 75 | }, 76 | }, 77 | }, 78 | 'ultra-pro-24ch-rdm': { 79 | channels: [...Array(25).keys()].slice(1), 80 | }, 81 | 'ultra-pro-6rgbch-rdm': { 82 | channels: [...Array(25).keys()].slice(1), 83 | channelgroups: ['1', '2', '3', '4', '5', '6'], 84 | }, 85 | 'oppsk-cob-uv-par': { 86 | channels: ['dimmer', 'strobe', 'program-speed', 'sound-activity'], 87 | }, 88 | 'lixda-par12-led': { 89 | channels: ['ctrl', 'static-color', 'speed', 'dimmer', 'red', 'green', 'blue', 'white'], 90 | ranges: { 91 | 'ctrl': { 92 | 'type': 'option', 93 | 'options': [ 94 | {'value': 0, 'label': 'Off'}, 95 | {'value': 11, 'label': 'Static Color'}, 96 | {'value': 51, 'label': 'Jump'}, 97 | {'value': 101, 'label': 'Gradual'}, 98 | {'value': 151, 'label': 'Sound Activate'}, 99 | {'value': 200, 'label': 'Strobe'}, 100 | ], 101 | }, 102 | 'static-color': { 103 | 'type': 'option', 104 | 'options': [ 105 | {'value': 0, 'label': 'All Color'}, 106 | {'value': 40, 'label': 'Red'}, 107 | {'value': 50, 'label': 'Green'}, 108 | {'value': 60, 'label': 'Blue'}, 109 | {'value': 70, 'label': 'Yellow'}, 110 | {'value': 80, 'label': 'Cyan'}, 111 | {'value': 90, 'label': 'Purple'}, 112 | {'value': 100, 'label': 'White'}, 113 | {'value': 110, 'label': 'Red + Green'}, 114 | {'value': 120, 'label': 'Red + Blue'}, 115 | {'value': 130, 'label': 'Red + White'}, 116 | {'value': 140, 'label': 'Green + Blue'}, 117 | {'value': 150, 'label': 'Green + White'}, 118 | {'value': 160, 'label': 'Blue + White'}, 119 | {'value': 170, 'label': 'Red + Green + White'}, 120 | {'value': 180, 'label': 'Red + Blue + White'}, 121 | {'value': 190, 'label': 'Green + Blue + White'}, 122 | {'value': 200, 'label': 'Red + Green + Blue'}, 123 | {'value': 210, 'label': 'Red + Green + Blue + White'}, 124 | ], 125 | }, 126 | }, 127 | }, 128 | 'eurolite-led-tha-120PC': { 129 | channels: ['red', 'green', 'blue', 'white', 'dimmer', 'strobe', 'effect'], 130 | }, 131 | 'briteq-bt-theatre-60FC': { 132 | channels: ['dimmer', 'strobe', 'effect', 'red', 'green', 'blue', 'white'], 133 | }, 134 | 'lalucenatz-led-4ch': { 135 | channels: ['master', 'red', 'green', 'blue'], 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/drivers/abstract-serial-driver.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {SerialPort, SerialPortOpenOptions} from 'serialport'; 3 | import {AutoDetectTypes} from '@serialport/bindings-cpp'; 4 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 5 | 6 | type OpenOptions = Omit, 'path'>; 7 | 8 | export interface AbstractSerialDriverArgs { 9 | serialPortOptions: OpenOptions; 10 | sendInterval: number; 11 | } 12 | 13 | export abstract class AbstractSerialDriver extends EventEmitter implements IUniverseDriver { 14 | private _serialPort!: SerialPort; 15 | 16 | private readonly _universe: Buffer; 17 | private readonly _sendInterval: number; 18 | private readonly _serialPortName: string; 19 | private readonly _serialPortOptions: OpenOptions; 20 | private _intervalHandle: any | undefined = undefined; 21 | 22 | protected constructor(serialPort: string, args: AbstractSerialDriverArgs) { 23 | super(); 24 | this._sendInterval = args.sendInterval; 25 | this._serialPortName = serialPort; 26 | this._serialPortOptions = args.serialPortOptions; 27 | 28 | this._universe = Buffer.alloc(513); 29 | } 30 | 31 | init(): Promise { 32 | return new Promise((resolve, reject) => { 33 | this._serialPort = new SerialPort({ 34 | ...this._serialPortOptions, 35 | path: this._serialPortName, 36 | }, (err) => { 37 | if (!err) { 38 | this.start(); 39 | resolve(); 40 | } else { 41 | reject(err); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | close(): Promise { 48 | this.stop(); 49 | return new Promise((resolve, reject) => this._serialPort.close((err: any) => err ? reject(err) : resolve())); 50 | } 51 | 52 | protected get serialPort(): SerialPort { 53 | return this._serialPort; 54 | } 55 | 56 | protected get universeBuffer(): Buffer { 57 | return this._universe; 58 | } 59 | 60 | protected start(): void { 61 | if (this._intervalHandle !== undefined) { 62 | throw new Error('Driver is already running.'); 63 | } 64 | this._intervalHandle = setInterval(this.sendUniverse.bind(this), this._sendInterval); 65 | } 66 | 67 | protected stop(): void { 68 | if (this._intervalHandle !== undefined) { 69 | clearInterval(this._intervalHandle); 70 | this._intervalHandle = undefined; 71 | } 72 | } 73 | 74 | protected abstract sendUniverse(): Promise; 75 | 76 | get(channel: number): number { 77 | return this._universe[channel]; 78 | } 79 | 80 | update(channels: UniverseData, extraData?: any): void { 81 | 82 | for (const c in channels) { 83 | this._universe[c] = channels[c]; 84 | } 85 | 86 | this.emit('update', channels, extraData); 87 | } 88 | 89 | updateAll(value: number): void { 90 | for (let i = 1; i <= 512; i++) { 91 | this._universe[i] = value; 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/drivers/artnet.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 3 | import dmxlib from 'dmxnet'; 4 | 5 | export interface ArtnetArgs { 6 | unchangedDataInterval?: number, 7 | universe?: number, 8 | port?: number, 9 | net?: number, 10 | subnet?: number, 11 | subuni?: number, 12 | dmxlibOptions?: dmxlib.DmxnetOptions, 13 | } 14 | 15 | export class ArtnetDriver extends EventEmitter implements IUniverseDriver { 16 | options: ArtnetArgs; 17 | host: string; 18 | dmxnet: dmxlib.dmxnet; 19 | universe?: dmxlib.sender; 20 | 21 | constructor(host = '127.0.0.1', options: ArtnetArgs = {}) { 22 | super(); 23 | 24 | this.options = options; 25 | this.host = host; 26 | // eslint-disable-next-line new-cap 27 | this.dmxnet = new dmxlib.dmxnet(options.dmxlibOptions); 28 | } 29 | 30 | async init(): Promise { 31 | this.universe = this.dmxnet.newSender({ 32 | ip: this.host, 33 | // eslint-disable-next-line camelcase 34 | base_refresh_interval: this.options.unchangedDataInterval, 35 | net: this.options.net, 36 | port: this.options.port, 37 | subnet: this.options.subnet, 38 | subuni: this.options.subuni, 39 | universe: this.options.universe, 40 | }); 41 | } 42 | 43 | sendUniverse(): void { 44 | this.universe?.transmit(); 45 | } 46 | 47 | close(): Promise { 48 | return new Promise((resolve) => { 49 | this.stop(); 50 | resolve(); 51 | }); 52 | } 53 | 54 | update(u: UniverseData, extraData?: any): void { 55 | for (const c in u) { 56 | this.universe?.prepChannel(Number(c), u[c]); 57 | } 58 | this.sendUniverse(); 59 | 60 | this.emit('update', u, extraData); 61 | } 62 | 63 | updateAll(v: number): void { 64 | this.universe?.fillChannels(0, 511, v); 65 | this.sendUniverse(); 66 | } 67 | 68 | get(c: number): number { 69 | return this.universe?.values?.[c]!; 70 | } 71 | 72 | private stop(): void { 73 | this.universe?.stop(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/drivers/bbdmx.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 3 | import dgram from 'dgram'; 4 | 5 | const UNIVERSE_LEN = 512; 6 | 7 | export interface BBDMXArgs { 8 | dmxSpeed?: number, 9 | port?: number, 10 | } 11 | 12 | export class BBDMXDriver extends EventEmitter implements IUniverseDriver { 13 | readyToWrite: boolean; 14 | interval: number; 15 | timeout?: any; 16 | options: {}; 17 | universe: Buffer; 18 | host: string; 19 | port: any; 20 | dev: dgram.Socket; 21 | 22 | constructor(deviceId = '127.0.0.1', options: BBDMXArgs = {}) { 23 | super(); 24 | this.readyToWrite = true; 25 | this.interval = options.dmxSpeed ? (1000 / options.dmxSpeed) : 24; 26 | this.options = options; 27 | this.universe = Buffer.alloc(UNIVERSE_LEN + 1); 28 | this.host = deviceId; 29 | this.port = options.port || 9930; 30 | this.dev = dgram.createSocket('udp4'); 31 | this.start(); 32 | } 33 | 34 | async init(): Promise { 35 | this.start(); 36 | } 37 | 38 | sendUniverse(): void { 39 | if (this.readyToWrite) { 40 | this.readyToWrite = false; 41 | 42 | let channel; 43 | let messageBuffer = Buffer.from(UNIVERSE_LEN.toString()); 44 | 45 | for (let i = 1; i <= UNIVERSE_LEN; i++) { 46 | channel = Buffer.from(' ' + this.universe[i]); 47 | messageBuffer = Buffer.concat([messageBuffer, channel]); 48 | } 49 | 50 | this.dev.send(messageBuffer, 0, messageBuffer.length, this.port, this.host, () => { 51 | this.readyToWrite = true; 52 | }); 53 | } 54 | } 55 | 56 | close(): void { 57 | this.stop(); 58 | this.dev.close(); 59 | } 60 | 61 | update(u: UniverseData, extraData?: any): void { 62 | for (const c in u) { 63 | this.universe[c] = u[c]; 64 | } 65 | 66 | this.emit('update', u, extraData); 67 | } 68 | 69 | updateAll(v: number): void { 70 | for (let i = 1; i <= UNIVERSE_LEN; i++) { 71 | this.universe[i] = v; 72 | } 73 | } 74 | 75 | get(c: number): number { 76 | return this.universe[c]; 77 | } 78 | 79 | private start(): void { 80 | this.timeout = setInterval(this.sendUniverse.bind(this), this.interval); 81 | } 82 | 83 | private stop(): void { 84 | if (this.timeout) clearInterval(this.timeout); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/drivers/dmx4all.ts: -------------------------------------------------------------------------------- 1 | import {AbstractSerialDriver} from './abstract-serial-driver'; 2 | 3 | const UNIVERSE_LEN = 512; 4 | 5 | export interface DMX4AllArgs { 6 | dmxSpeed?: number; 7 | } 8 | 9 | export class DMX4AllDriver extends AbstractSerialDriver { 10 | private readyToWrite: boolean; 11 | 12 | constructor(serialPort: string, options: DMX4AllArgs = {}) { 13 | super(serialPort, { 14 | serialPortOptions: { 15 | 'baudRate': 38400, 16 | 'dataBits': 8, 17 | 'stopBits': 1, 18 | 'parity': 'none', 19 | }, 20 | sendInterval: 1000 / (options.dmxSpeed || 33), 21 | }); 22 | this.readyToWrite = true; 23 | } 24 | 25 | async sendUniverse(): Promise { 26 | if (!this.serialPort.writable) { 27 | return; 28 | } 29 | 30 | if (this.readyToWrite) { 31 | this.readyToWrite = false; 32 | 33 | const msg = Buffer.alloc(UNIVERSE_LEN * 3); 34 | 35 | for (let i = 0; i < UNIVERSE_LEN; i++) { 36 | msg[i * 3 + 0] = (i < 256) ? 0xE2 : 0xE3; 37 | msg[i * 3 + 1] = i; 38 | msg[i * 3 + 2] = this.universeBuffer[i + 1]; 39 | } 40 | 41 | this.serialPort.write(msg); 42 | this.serialPort.drain(() => { 43 | this.readyToWrite = true; 44 | }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/drivers/dmxking-ultra-dmx-pro.ts: -------------------------------------------------------------------------------- 1 | import {AbstractSerialDriver} from './abstract-serial-driver'; 2 | 3 | const DMXKING_ULTRA_DMX_PRO_DMX_STARTCODE = 0x00; 4 | const DMXKING_ULTRA_DMX_PRO_START_OF_MSG = 0x7e; 5 | const DMXKING_ULTRA_DMX_PRO_END_OF_MSG = 0xe7; 6 | const DMXKING_ULTRA_DMX_PRO_SEND_DMX_RQ = 0x06; 7 | const DMXKING_ULTRA_DMX_PRO_SEND_DMX_A_RQ = 0x64; 8 | const DMXKING_ULTRA_DMX_PRO_SEND_DMX_B_RQ = 0x65; 9 | 10 | // var DMXKING_ULTRA_DMX_PRO_RECV_DMX_PKT = 0x05; 11 | 12 | export interface DMXKingUltraDMXProDriverArgs { 13 | dmxSpeed?: number; 14 | port?: 'A' | 'B'; 15 | } 16 | 17 | export class DMXKingUltraDMXProDriver extends AbstractSerialDriver { 18 | private readonly _options: DMXKingUltraDMXProDriverArgs; 19 | private readonly _sendDMXReq: number; 20 | private _readyToWrite: boolean; 21 | 22 | constructor(serialPort: string, options: DMXKingUltraDMXProDriverArgs = {}) { 23 | super(serialPort, { 24 | serialPortOptions: { 25 | 'baudRate': 250000, 26 | 'dataBits': 8, 27 | 'stopBits': 2, 28 | 'parity': 'none', 29 | }, 30 | sendInterval: 1000 / (options.dmxSpeed || 40), 31 | }); 32 | this._options = options; 33 | this._readyToWrite = true; 34 | 35 | this._sendDMXReq = DMXKING_ULTRA_DMX_PRO_SEND_DMX_RQ; 36 | if (this._options.port === 'A') { 37 | this._sendDMXReq = DMXKING_ULTRA_DMX_PRO_SEND_DMX_A_RQ; 38 | } else if (this._options.port === 'B') { 39 | this._sendDMXReq = DMXKING_ULTRA_DMX_PRO_SEND_DMX_B_RQ; 40 | } 41 | 42 | } 43 | 44 | async sendUniverse(): Promise { 45 | if (!this.serialPort.writable) { 46 | return; 47 | } 48 | 49 | if (this._readyToWrite) { 50 | this._readyToWrite = false; 51 | 52 | const hdr = Buffer.from([ 53 | DMXKING_ULTRA_DMX_PRO_START_OF_MSG, 54 | this._sendDMXReq, 55 | (this.universeBuffer.length) & 0xff, 56 | ((this.universeBuffer.length) >> 8) & 0xff, 57 | DMXKING_ULTRA_DMX_PRO_DMX_STARTCODE, 58 | ]); 59 | 60 | const msg = Buffer.concat([ 61 | hdr, 62 | this.universeBuffer.slice(1), 63 | Buffer.from([DMXKING_ULTRA_DMX_PRO_END_OF_MSG]), 64 | ]); 65 | 66 | this.serialPort.write(msg); 67 | this.serialPort.drain(() => { 68 | this._readyToWrite = true; 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/drivers/enttec-open-usb-dmx.ts: -------------------------------------------------------------------------------- 1 | import {wait} from '../util/time'; 2 | import {AbstractSerialDriver} from './abstract-serial-driver'; 3 | 4 | export interface EnttecOpenUsbDmxArgs { 5 | dmxSpeed?: number; 6 | } 7 | 8 | /** 9 | * Controls the Enttec Open DMX device: 10 | * https://www.enttec.com.au/product/lighting-communication-protocols/usb-lighting-interface/open-dmx-usb/ 11 | * 12 | * The controller uses a FTDI FT232RL chip for serial communication. See 13 | * [here](http://www.ftdichip.com/Support/Documents/ProgramGuides/D2XX_Programmer's_Guide(FT_000071).pdf) 14 | * for an API reference and to translate the Enttec code examples to Node.js/Serialport. 15 | */ 16 | export class EnttecOpenUSBDMXDriver extends AbstractSerialDriver { 17 | private _readyToWrite: boolean; 18 | 19 | constructor(serialPort: string, args?: EnttecOpenUsbDmxArgs) { 20 | super(serialPort, { 21 | serialPortOptions: { 22 | 'baudRate': 250000, 23 | 'dataBits': 8, 24 | 'stopBits': 2, 25 | 'parity': 'none', 26 | }, 27 | sendInterval: args?.dmxSpeed ? (1000 / args.dmxSpeed) : 46, 28 | }); 29 | 30 | this._readyToWrite = true; 31 | } 32 | 33 | async sendUniverse(): Promise { 34 | if (!this.serialPort.writable) { 35 | return; 36 | } 37 | 38 | // toggle break 39 | await this.serialPort.set({brk: true, rts: false}); 40 | await wait(1); 41 | await this.serialPort.set({brk: false, rts: false}); 42 | await wait(1); 43 | if (this._readyToWrite) { 44 | const dataToWrite = Buffer.concat([Buffer.from([0]), this.universeBuffer.slice(1)]); 45 | 46 | this._readyToWrite = false; 47 | this.serialPort.write(dataToWrite); 48 | this.serialPort.drain(() => { 49 | this._readyToWrite = true; 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/drivers/enttec-usb-dmx-pro.ts: -------------------------------------------------------------------------------- 1 | import {AbstractSerialDriver} from './abstract-serial-driver'; 2 | 3 | const ENTTEC_PRO_DMX_STARTCODE = 0x00; 4 | const ENTTEC_PRO_START_OF_MSG = 0x7e; 5 | const ENTTEC_PRO_END_OF_MSG = 0xe7; 6 | const ENTTEC_PRO_SEND_DMX_RQ = 0x06; 7 | 8 | // var ENTTEC_PRO_RECV_DMX_PKT = 0x05; 9 | 10 | export interface EnttecUSBDMXProArgs { 11 | dmxSpeed?: number; 12 | } 13 | 14 | export class EnttecUSBDMXProDriver extends AbstractSerialDriver { 15 | private _readyToWrite: boolean; 16 | 17 | constructor(serialPort: string, options: EnttecUSBDMXProArgs = {}) { 18 | super(serialPort, { 19 | serialPortOptions: { 20 | 'baudRate': 250000, 21 | 'dataBits': 8, 22 | 'stopBits': 2, 23 | 'parity': 'none', 24 | }, 25 | sendInterval: 1000 / (options.dmxSpeed || 40), 26 | }); 27 | 28 | this._readyToWrite = true; 29 | } 30 | 31 | async sendUniverse(): Promise { 32 | if (!this.serialPort.writable) { 33 | return; 34 | } 35 | 36 | if (this._readyToWrite) { 37 | const hdr = Buffer.from([ 38 | ENTTEC_PRO_START_OF_MSG, 39 | ENTTEC_PRO_SEND_DMX_RQ, 40 | (this.universeBuffer.length) & 0xff, 41 | ((this.universeBuffer.length) >> 8) & 0xff, 42 | ENTTEC_PRO_DMX_STARTCODE, 43 | ]); 44 | 45 | const msg = Buffer.concat([ 46 | hdr, 47 | this.universeBuffer.slice(1), 48 | Buffer.from([ENTTEC_PRO_END_OF_MSG]), 49 | ]); 50 | 51 | this._readyToWrite = false; 52 | this.serialPort.write(msg); 53 | this.serialPort.drain(() => { 54 | this._readyToWrite = true; 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/drivers/null.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {IUniverseDriver} from '../models/IUniverseDriver'; 3 | 4 | export interface NullDriverArgs { 5 | dmxSpeed?: number; 6 | } 7 | 8 | export class NullDriver extends EventEmitter implements IUniverseDriver { 9 | constructor(options: NullDriverArgs = {}) { 10 | super(); 11 | 12 | this._universe = Buffer.alloc(513, 0); 13 | this._interval = 1000 / (options?.dmxSpeed ?? 1); 14 | } 15 | 16 | async init(): Promise { 17 | this.start(); 18 | } 19 | 20 | close(): void { 21 | this.stop(); 22 | } 23 | 24 | update(u: {[key: number]: number}, extraData: any): void { 25 | for (const c in u) { 26 | this._universe[c] = u[c]; 27 | } 28 | this.logUniverse(); 29 | 30 | this.emit('update', u, extraData); 31 | } 32 | 33 | updateAll(v: number): void { 34 | for (let i = 1; i <= 512; i++) { 35 | this._universe[i] = v; 36 | } 37 | } 38 | 39 | get(c: number): number { 40 | return this._universe[c]; 41 | } 42 | 43 | logUniverse(): void { 44 | console.log(this._universe.slice(1)); 45 | } 46 | 47 | private start(): void { 48 | this._timeout = setInterval(() => { 49 | this.logUniverse(); 50 | }, this._interval); 51 | } 52 | 53 | private stop(): void { 54 | clearInterval(this._timeout); 55 | } 56 | 57 | private readonly _universe: Buffer; 58 | private readonly _interval: number; 59 | private _timeout: any; 60 | } 61 | -------------------------------------------------------------------------------- /src/drivers/sacn.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 3 | import * as sacn from 'sacn'; 4 | 5 | export type SACNOptions = { 6 | sourceName?: string; 7 | priority?: number; 8 | cid?: Buffer; 9 | reuseAddr?: boolean; 10 | interface?: string; 11 | minRefreshRate?: number; 12 | port?: number; 13 | ip?: string; 14 | }; 15 | 16 | export class SACNDriver extends EventEmitter implements IUniverseDriver { 17 | sACNServer: sacn.Sender; 18 | universe: any = {}; 19 | 20 | constructor(universe = 1, private options: SACNOptions = { reuseAddr: true }) { 21 | super(); 22 | this.sACNServer = new sacn.Sender({ 23 | universe: universe || 1, 24 | reuseAddr: options.reuseAddr, 25 | iface: options.interface, 26 | minRefreshRate: options.minRefreshRate, 27 | port: options.port, 28 | useUnicastDestination: options.ip, 29 | }); 30 | } 31 | 32 | async init(): Promise { 33 | } 34 | 35 | close(): void { 36 | this.sACNServer.close(); 37 | } 38 | 39 | update(u: UniverseData, extraData: any): void { 40 | for (const c in u) { 41 | this.universe[c] = SACNDriver.dmxToPercent(u[c]); 42 | } 43 | this.sendUniverse(); 44 | } 45 | 46 | sendUniverse(): void { 47 | this.sACNServer.send({ 48 | payload: this.universe, 49 | sourceName: this.options.sourceName, 50 | priority: this.options.priority, 51 | cid: this.options.cid, 52 | }); 53 | } 54 | 55 | updateAll(v: number): void { 56 | for (let i = 1; i <= 512; i++) { 57 | this.universe[i] = SACNDriver.dmxToPercent(v); 58 | } 59 | this.sendUniverse(); 60 | } 61 | 62 | get(c: number): number { 63 | return SACNDriver.percentToDmx(this.universe[c]); 64 | } 65 | 66 | static dmxToPercent(v: number): number { 67 | return v / 255 * 100; 68 | } 69 | 70 | static percentToDmx(v: number): number { 71 | return v / 100 * 255; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/drivers/socketio.ts: -------------------------------------------------------------------------------- 1 | import {IUniverseDriver, UniverseData} from '../models/IUniverseDriver'; 2 | import { Server } from 'socket.io'; 3 | import {EventEmitter} from 'events'; 4 | 5 | export interface SocketIOArgs { 6 | port?: number; 7 | debug?: boolean; 8 | } 9 | 10 | export class SocketIODriver extends EventEmitter implements IUniverseDriver { 11 | universe: Buffer; 12 | server: Server; 13 | 14 | constructor(options: SocketIOArgs) { 15 | super(); 16 | options = options || {}; 17 | 18 | const port = options.port || 18909; 19 | const debug = options.debug || false; 20 | 21 | this.server = new Server(port, {allowEIO3: true}); 22 | this.server.on('connection', (socket) => { 23 | if (debug) console.info(`Client connected [id=${socket.id}]`); 24 | socket.on('disconnect', () => { 25 | if (debug) console.info(`Client gone [id=${socket.id}]`); 26 | }); 27 | }); 28 | 29 | this.universe = Buffer.alloc(513, 0); 30 | } 31 | 32 | async init(): Promise { 33 | } 34 | 35 | close(): void { 36 | this.server.close(); 37 | } 38 | 39 | update(u: UniverseData, extraData: any): void { 40 | for (const c in u) { 41 | this.universe[c] = u[c]; 42 | } 43 | this.server.sockets.emit('update', [...this.universe]); 44 | this.emit('update', u, extraData); 45 | } 46 | 47 | updateAll(v: number): void { 48 | for (let i = 1; i <= 512; i++) { 49 | this.universe[i] = v; 50 | } 51 | 52 | this.server.sockets.emit('update', [...this.universe]); 53 | } 54 | 55 | get(c: number): number { 56 | return this.universe[c]; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/easing.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * based on jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * 4 | * TERMS OF USE - jQuery Easing 5 | * 6 | * Open source under the BSD License. 7 | * 8 | * Copyright © 2008 George McGinley Smith 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without modification, 12 | * are permitted provided that the following conditions are met: 13 | * 14 | * Redistributions of source code must retain the above copyright notice, this list of 15 | * conditions and the following disclaimer. 16 | * Redistributions in binary form must reproduce the above copyright notice, this list 17 | * of conditions and the following disclaimer in the documentation and/or other materials 18 | * provided with the distribution. 19 | * 20 | * Neither the name of the author nor the names of contributors may be used to endorse 21 | * or promote products derived from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 24 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 25 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 28 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 29 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 31 | * OF THE POSSIBILITY OF SUCH DAMAGE. 32 | * 33 | */ 34 | 35 | // t: current time, b: begInnIng value, c: change In value, d: duration 36 | 37 | export const ease = { 38 | linear(t: number, b: number, c: number, d: number): number { 39 | return c * t / d + b; 40 | }, 41 | inQuad(t: number, b: number, c: number, d: number): number { 42 | return c * (t /= d) * t + b; 43 | }, 44 | outQuad(t: number, b: number, c: number, d: number): number { 45 | return -c * (t /= d) * (t - 2) + b; 46 | }, 47 | inOutQuad(t: number, b: number, c: number, d: number): number { 48 | if ((t /= d / 2) < 1) return c / 2 * t * t + b; 49 | return -c / 2 * ((--t) * (t - 2) - 1) + b; 50 | }, 51 | inCubic(t: number, b: number, c: number, d: number): number { 52 | return c * (t /= d) * t * t + b; 53 | }, 54 | outCubic(t: number, b: number, c: number, d: number): number { 55 | return c * ((t = t / d - 1) * t * t + 1) + b; 56 | }, 57 | inOutCubic(t: number, b: number, c: number, d: number): number { 58 | if ((t /= d / 2) < 1) return c / 2 * t * t * t + b; 59 | return c / 2 * ((t -= 2) * t * t + 2) + b; 60 | }, 61 | inQuart(t: number, b: number, c: number, d: number): number { 62 | return c * (t /= d) * t * t * t + b; 63 | }, 64 | outQuart(t: number, b: number, c: number, d: number): number { 65 | return -c * ((t = t / d - 1) * t * t * t - 1) + b; 66 | }, 67 | inOutQuart(t: number, b: number, c: number, d: number): number { 68 | if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b; 69 | return -c / 2 * ((t -= 2) * t * t * t - 2) + b; 70 | }, 71 | inQuint(t: number, b: number, c: number, d: number): number { 72 | return c * (t /= d) * t * t * t * t + b; 73 | }, 74 | outQuint(t: number, b: number, c: number, d: number): number { 75 | return c * ((t = t / d - 1) * t * t * t * t + 1) + b; 76 | }, 77 | inOutQuint(t: number, b: number, c: number, d: number): number { 78 | if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b; 79 | return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; 80 | }, 81 | inSine(t: number, b: number, c: number, d: number): number { 82 | return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; 83 | }, 84 | outSine(t: number, b: number, c: number, d: number): number { 85 | return c * Math.sin(t / d * (Math.PI / 2)) + b; 86 | }, 87 | inOutSine(t: number, b: number, c: number, d: number): number { 88 | return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; 89 | }, 90 | inExpo(t: number, b: number, c: number, d: number): number { 91 | return (t === 0) ? b : c * (2 ** (10 * (t / d - 1))) + b; 92 | }, 93 | outExpo(t: number, b: number, c: number, d: number): number { 94 | return (t === d) ? b + c : c * (-(2 ** (-10 * t / d)) + 1) + b; 95 | }, 96 | inOutExpo(t: number, b: number, c: number, d: number): number { 97 | if (t === 0) return b; 98 | if (t === d) return b + c; 99 | if ((t /= d / 2) < 1) return c / 2 * (2 ** (10 * (t - 1))) + b; 100 | return c / 2 * (-(2 ** (-10 * --t)) + 2) + b; 101 | }, 102 | inCirc(t: number, b: number, c: number, d: number): number { 103 | return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; 104 | }, 105 | outCirc(t: number, b: number, c: number, d: number): number { 106 | return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; 107 | }, 108 | inOutCirc(t: number, b: number, c: number, d: number): number { 109 | if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; 110 | return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; 111 | }, 112 | inElastic(t: number, b: number, c: number, d: number): number { 113 | let s = 1.70158; let p = 0; let a = c; 114 | 115 | if (t === 0) return b; if ((t /= d) === 1) return b + c; if (!p) p = d * 0.3; 116 | if (a < Math.abs(c)) { 117 | a = c; s = p / 4; 118 | } else s = p / (2 * Math.PI) * Math.asin(c / a); 119 | 120 | return -(a * (2 ** (10 * (t -= 1))) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; 121 | }, 122 | outElastic(t: number, b: number, c: number, d: number): number { 123 | let s = 1.70158; let p = 0; let a = c; 124 | 125 | if (t === 0) return b; if ((t /= d) === 1) return b + c; if (!p) p = d * 0.3; 126 | if (a < Math.abs(c)) { 127 | a = c; s = p / 4; 128 | } else s = p / (2 * Math.PI) * Math.asin(c / a); 129 | 130 | return a * (2 ** (-10 * t)) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b; 131 | }, 132 | inOutElastic(t: number, b: number, c: number, d: number): number { 133 | let s = 1.70158; let p = 0; let a = c; 134 | 135 | if (t === 0) return b; if ((t /= d / 2) === 2) return b + c; if (!p) p = d * (0.3 * 1.5); 136 | if (a < Math.abs(c)) { 137 | a = c; s = p / 4; 138 | } else s = p / (2 * Math.PI) * Math.asin(c / a); 139 | 140 | if (t < 1) return -0.5 * (a * (2 ** (10 * (t -= 1))) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; 141 | return a * (2 ** (-10 * (t -= 1))) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b; 142 | }, 143 | inBack(t: number, b: number, c: number, d: number, s: number): number { 144 | if (s === undefined) s = 1.70158; 145 | return c * (t /= d) * t * ((s + 1) * t - s) + b; 146 | }, 147 | outBack(t: number, b: number, c: number, d: number, s: number): number { 148 | if (s === undefined) s = 1.70158; 149 | return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; 150 | }, 151 | inOutBack(t: number, b: number, c: number, d: number, s: number): number { 152 | if (s === undefined) s = 1.70158; 153 | if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; 154 | return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; 155 | }, 156 | inBounce(t: number, b: number, c: number, d: number): number { 157 | return c - exports.ease.outBounce(d - t, 0, c, d) + b; 158 | }, 159 | outBounce(t: number, b: number, c: number, d: number): number { 160 | if ((t /= d) < (1 / 2.75)) { 161 | return c * (7.5625 * t * t) + b; 162 | } else if (t < (2 / 2.75)) { 163 | return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; 164 | } else if (t < (2.5 / 2.75)) { 165 | return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; 166 | } 167 | return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; 168 | 169 | }, 170 | inOutBounce(t: number, b: number, c: number, d: number): number { 171 | if (t < d / 2) return exports.ease.inBounce(t * 2, 0, c, d) * 0.5 + b; 172 | return exports.ease.outBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; 173 | }, 174 | }; 175 | 176 | /* 177 | * 178 | * TERMS OF USE - EASING EQUATIONS 179 | * 180 | * Open source under the BSD License. 181 | * 182 | * Copyright © 2001 Robert Penner 183 | * All rights reserved. 184 | * 185 | * Redistribution and use in source and binary forms, with or without modification, 186 | * are permitted provided that the following conditions are met: 187 | * 188 | * Redistributions of source code must retain the above copyright notice, this list of 189 | * conditions and the following disclaimer. 190 | * Redistributions in binary form must reproduce the above copyright notice, this list 191 | * of conditions and the following disclaimer in the documentation and/or other materials 192 | * provided with the distribution. 193 | * 194 | * Neither the name of the author nor the names of contributors may be used to endorse 195 | * or promote products derived from this software without specific prior written permission. 196 | * 197 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 198 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 199 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 200 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 201 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 202 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 203 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 204 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 205 | * OF THE POSSIBILITY OF SUCH DAMAGE. 206 | * 207 | */ 208 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { DMX } from './DMX'; 2 | import { Animation } from './Animation'; 3 | import { ArtnetDriver } from './drivers/artnet'; 4 | import { BBDMXDriver } from './drivers/bbdmx'; 5 | import { DMX4AllDriver } from './drivers/dmx4all'; 6 | import { DMXKingUltraDMXProDriver } from './drivers/dmxking-ultra-dmx-pro'; 7 | import { EnttecUSBDMXProDriver } from './drivers/enttec-usb-dmx-pro'; 8 | import { NullDriver } from './drivers/null'; 9 | import { SocketIODriver } from './drivers/socketio'; 10 | import { EnttecOpenUSBDMXDriver } from './drivers/enttec-open-usb-dmx'; 11 | import { SACNDriver } from './drivers/sacn'; 12 | import { IUniverseDriver, UniverseData } from './models/IUniverseDriver'; 13 | 14 | export { 15 | DMX, 16 | Animation, 17 | }; 18 | export { 19 | IUniverseDriver, 20 | UniverseData, 21 | }; 22 | export { 23 | ArtnetDriver, 24 | BBDMXDriver, 25 | DMX4AllDriver, 26 | DMXKingUltraDMXProDriver, 27 | EnttecOpenUSBDMXDriver, 28 | EnttecUSBDMXProDriver, 29 | NullDriver, 30 | SocketIODriver, 31 | SACNDriver, 32 | }; 33 | -------------------------------------------------------------------------------- /src/models/Events.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export enum Events { 3 | update = 'update', 4 | updateAll = 'updateAll', 5 | } 6 | -------------------------------------------------------------------------------- /src/models/IUniverseDriver.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export type UniverseData = { 4 | [key: number]: number; 5 | } 6 | 7 | export interface IUniverseDriver extends EventEmitter { 8 | init(): Promise; 9 | update(channels: UniverseData, extraData?: any): void; 10 | 11 | get(channel: number): number; 12 | 13 | updateAll(value: number): void; 14 | 15 | close(): Promise | void; 16 | } 17 | -------------------------------------------------------------------------------- /src/util/time.ts: -------------------------------------------------------------------------------- 1 | export const wait = (millis: number): Promise => new Promise((resolve) => setTimeout(resolve, millis)); 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noImplicitAny": true, 6 | "removeComments": false, 7 | "preserveConstEnums": true, 8 | "inlineSourceMap": true, 9 | "declaration": true, 10 | "outDir": "dist", 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": false, 13 | "target": "ES6", 14 | "strict": true, 15 | "lib": [ 16 | "es2018", 17 | "esnext" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*", 22 | "spec/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------