├── .gitignore ├── LICENSE ├── README.md ├── dist ├── app.js ├── config.js ├── controller-inspect.js ├── controllers │ ├── dualshock4.js │ └── sidewinderpp.js └── visca │ ├── camera.js │ ├── command.js │ ├── constants.js │ ├── controller.js │ ├── visca-ip.js │ ├── visca-serial.js │ └── visca.js ├── nearus-visca-protocol.pdf ├── package-lock.json ├── package.json ├── sony-evi-h100s-user-manual.pdf ├── src ├── app.ts ├── config.js ├── controller-inspect.js ├── controllers │ ├── dualshock4.js │ └── sidewinderpp.js └── macros.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | node-gamepad 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 jeffmikels 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PTZ-SERVER 2 | 3 | PTZ-SERVER is a full PTZ server to control VISCA-IP and VISCA-Serial cameras using a web interface and HID devices like joysticks and gamepads. 4 | 5 | Eventually, it will enable users to program advanced camera moves as macros. 6 | 7 | ## Rationale... 8 | 9 | Of course, the main reason for this module is to fully implement the VISCA protocol without any reliance on `libvisca` the main C library for VISCA commands. 10 | 11 | I did it as a challenge to myself, but also so that the VISCA protocol could be more easily understood by Javascript/Typescript developers who are not comfortable reading protocol documentation or C source code. 12 | 13 | Finally, by implementing it in Typescript, I have been able to create a code-base that can be parsed easily by Visual Studio Code (and presumably other IDEs) providing intelligent code suggestions to make calling VISCA commands much easier on the developer. 14 | 15 | The biggest challenge for the Javascript/Typescript ecosystem in managing Visca connections is that Visca commands are intended to be executed in the following manner: 16 | 17 | - send a command over the serial or network connection 18 | - wait for an ACK (for most commands) 19 | - wait for a DONE (for many commands) 20 | 21 | Javascript doesn't like to work this way. Javascript/Typescript code wants everything to run asynchronously, so this library virtualizes VISCA cameras to maintain command buffers and callbacks. As VISCA commands come in, the Camera objects will update themselves and then emit an 'update' event. As a developer, you only need to send commands and listen for changes on the various objects. 22 | -------------------------------------------------------------------------------- /dist/app.js: -------------------------------------------------------------------------------- 1 | // MODULES 2 | const http = require('http'); 3 | const config = require('./config'); 4 | const ViscaControl = require("./visca/visca"); 5 | let SidewinderPP = require("./controllers/sidewinderpp"); 6 | let Dualshock4 = require("./controllers/dualshock4"); 7 | // VISCA INTERFACE 8 | let visca = new ViscaControl(portname = 'COM8', baud = '38400'); 9 | visca.start(); 10 | /* CONTROLLER HANDLER */ 11 | let sw = new SidewinderPP(); // reports axes as signed values 12 | sw.onUpdate((data) => console.log(data)); 13 | let ds4 = new Dualshock4(); // reports axes as unsigned 8 bit 14 | ds4.onUpdate((data) => { 15 | console.log(data); 16 | }); 17 | /* VISCA IP PASSTHROUGH */ 18 | /* HTTP HANDLER FOR AUTOMATION BY API */ 19 | const httpHandler = function (req, res) { 20 | res.writeHead(200); 21 | res.end('Hello, World!'); 22 | }; 23 | const server = http.createServer(httpHandler); 24 | server.listen(52380); 25 | -------------------------------------------------------------------------------- /dist/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | let macros; 3 | if (fs.existsSync('macros.json')) { 4 | try { 5 | macros = JSON.parse(fs.readFileSync('macros.json')); 6 | } 7 | catch (e) { 8 | macros = {}; 9 | } 10 | } 11 | let config = { 12 | // serial port for talking to visca cameras 13 | viscaSerial: { 14 | port: 'COM8', 15 | baud: 38400, 16 | }, 17 | // configuration for visca-ip cameras 18 | // {name, index, ip, port, [ptz | sony]} 19 | viscaIPCameras: [], 20 | // configuration for the visca ip translation server 21 | // the http server will reside at the basePort 22 | // udp servers will exist at basePort + cameraIndex 23 | viscaServer: { 24 | basePort: 52380, 25 | }, 26 | // default controller configurations 27 | controllers: { 28 | 'global': {}, 29 | 'sidewinderpp': {}, 30 | 'dualshock4': {} 31 | }, 32 | macros 33 | }; 34 | module.exports = config; 35 | -------------------------------------------------------------------------------- /dist/controller-inspect.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | console.log(HID.devices()); 3 | // quick and dirty 6 bit twos complement 4 | // because of how javascript handles bitwise NOT 5 | // of unsigned values... it does the NOT, 6 | // but then it interprets the result as a SIGNED value 7 | // which ends up negative because the original was unsigned 8 | function unsigned2signed(val, bits = 8) { 9 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 10 | let mask = Math.pow(2, bits); // 0b1_0000_0000 11 | if (val & signBit) 12 | return -(mask + ~val + 1); 13 | else 14 | return val; 15 | } 16 | // var hid = new HID.HID(1356, 2508); 17 | var hid = new HID.HID(1118, 8); 18 | hid.on("data", function (data) { 19 | let x = data[1] | ((data[2] & 0b00000011) << 8); 20 | x = unsigned2signed(x, 10); 21 | let a = data[2] & 0b11111100; // ignore the two bits used as the sign for x 22 | let b = data[3] & 0b00001111; // ignore the part used for z 23 | let y = (b << 6) | (a >> 2); 24 | y = unsigned2signed(y, 10); 25 | // let y = ((data[2] & 0b11111100) >> 2 ) | data[3] 26 | let unknown = data[3] & 0b00001111; 27 | let other = data[5]; 28 | console.log(y); 29 | }); 30 | /* SIDEWINDER PRECISION PRO 31 | details are found near line 300 32 | https://github.com/torvalds/linux/blob/master/drivers/input/joystick/sidewinder.c 33 | 34 | // this is what I reverse engineered 35 | first four bits are the top buttons 36 | trigger = 1 37 | left button = 2 38 | top right = 4 39 | bottom right = 8 40 | 41 | next four bits are the nine positions of the hat 42 | 0-8 up-nothing 43 | 44 | next 8 bits are the least significant bits of the 10 bit X axis (signed) 45 | next 6 bits are the least significant bits of the 10 bit Y axis (signed) 46 | next 2 bits are the most significant bits of the X axis 47 | next 4 bits are the least significant bits of the 6 bit Z axis (signed) 48 | next 4 bits are the most significant bits of the Y axis 49 | next 6 bits are used for buttons 50 | next 2 bits are used for the most significant bits of the Z axis 51 | 52 | it works like this: 53 | byte 3 - cdef---- 54 | byte 4 - ------ab 55 | final value - abcdef as a six bit signed value 56 | 57 | sample code: 58 | let sign = data[4] << 4; 59 | let val = data[3] >> 4; 60 | let final = sign | val; 61 | // six bit signed 2s complement 62 | final = unsigned2signed(final, 6); 63 | console.log(sign.toString(2), val.toString(2), final.toString(2), final); 64 | 65 | 66 | next 4 bits are buttons c, d, arrow 67 | next 4 bits are complicated 68 | bit 0 is z axis turned to the right but also when slightly to the left 69 | bit 1 is z axis turned to the left at all 70 | bit 2 is button A 71 | bit 3 is button B 72 | 73 | next 8 bits are the dial but only 7 bits are used 74 | when centered, the value is 00 75 | it's value is a 7 bit 2s complement signed integer 76 | it increments when moving counter-clockwise 77 | -64 – 63 78 | 79 | represented as a signed 7 bit integer 80 | incrementing when moving counterclockwise 81 | 82 | */ 83 | /* PS4 84 | LX = 1 85 | LY = 2 86 | 87 | RX = 3 88 | RY = 4 89 | 90 | DPAD = 5low (VALUE CLOCKWISE 0-8 up-nothing) 91 | BUTTONS = 5high BITMASK - T O X S (8 4 2 1) 92 | STICK BUTTON = 6high BITMASK - RIGHT LEFT OPTION SHARE (8 4 2 1) 93 | TRIGGERS = 6low BITMASK (R2 L2 R1 L1) (8 4 2 1) 94 | L2 analog = 8 (0-255) 95 | R2 analog = 9 (0-255) 96 | TOUCHPAD 97 | 98 | */ 99 | // let Gamecontroller = require('gamecontroller'); 100 | // let dev = Gamecontroller.getDevices(); 101 | // console.log(dev); 102 | -------------------------------------------------------------------------------- /dist/controllers/dualshock4.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | const dpadcodes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']; 3 | // quick and dirty 6 bit twos complement 4 | // because of how javascript handles bitwise NOT 5 | // of unsigned values... it does the NOT, 6 | // but then it interprets the result as a SIGNED value 7 | // which ends up negative because the original was unsigned 8 | function unsigned2signed(val, bits = 8) { 9 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 10 | let mask = Math.pow(2, bits); // 0b1_0000_0000 11 | if (val & signBit) 12 | return -(mask + ~val + 1); 13 | else 14 | return val; 15 | } 16 | function deepEquals(a, b) { 17 | if (typeof a != typeof b) 18 | return false; 19 | if (Array.isArray(a) && Array.isArray(b)) { 20 | for (let i = 0; i < a.length; i++) { 21 | if (!deepEquals(a[i], b[i])) 22 | return false; 23 | } 24 | } 25 | else if (typeof a == 'object' && a != null) { 26 | for (let key of Object.keys(a)) { 27 | if (!deepEquals(a[key], b[key])) 28 | return false; 29 | } 30 | } 31 | else { 32 | return a == b; 33 | } 34 | return true; 35 | } 36 | class Dualshock4 { 37 | constructor() { 38 | this.hid = new HID.HID(1356, 2508); 39 | this.status = {}; 40 | this.callback = null; 41 | this.hid.on("data", (data) => { 42 | this.handleData(data); 43 | }); 44 | } 45 | onUpdate(f) { 46 | this.callback = f; 47 | } 48 | handleData(data) { 49 | let status = {}; 50 | status.lx = data[1]; 51 | status.ly = data[2]; 52 | status.rx = data[3]; 53 | status.ry = data[4]; 54 | let dpadval = data[5] & 0b00001111; 55 | let dpadcode = dpadcodes[dpadval]; 56 | status.dpad = { value: dpadval, code: dpadcode }; 57 | status.buttonT = (data[5] & 0b10000000) != 0; 58 | status.buttonO = (data[5] & 0b01000000) != 0; 59 | status.buttonX = (data[5] & 0b00100000) != 0; 60 | status.buttonS = (data[5] & 0b00010000) != 0; 61 | status.buttonShare = (data[6] & 0b00010000) != 0; 62 | status.buttonOption = (data[6] & 0b00100000) != 0; 63 | status.buttonLS = (data[6] & 0b01000000) != 0; 64 | status.buttonRS = (data[6] & 0b10000000) != 0; 65 | status.triggerL1 = (data[6] & 0b0001) != 0; 66 | status.triggerR1 = (data[6] & 0b0010) != 0; 67 | status.triggerL2 = (data[6] & 0b0100) != 0; 68 | status.triggerR2 = (data[6] & 0b1000) != 0; 69 | status.analogL2 = data[8]; 70 | status.analogR2 = data[9]; 71 | // TODO: touchpad data 72 | // let dirty = false; 73 | // for (let key of Object.keys(status)) { 74 | // if (key == 'dpad') { 75 | // if (status.dpad.value != this.status.dpad.value) { 76 | // dirty = true; 77 | // break; 78 | // } 79 | // continue; 80 | // } 81 | // if (status[key] != this.status[key]) { 82 | // dirty = true; 83 | // console.log(key); 84 | // break; 85 | // } 86 | // } 87 | if (!deepEquals(status, this.status)) { 88 | this.status = status; 89 | if (this.callback != null) 90 | this.callback(status); 91 | } 92 | } 93 | } 94 | module.exports = Dualshock4; 95 | /* PS4 96 | LX = 1 97 | LY = 2 98 | 99 | RX = 3 100 | RY = 4 101 | 102 | DPAD = 5low (VALUE CLOCKWISE 0-8 up-nothing) 103 | BUTTONS = 5high BITMASK - T O X S (8 4 2 1) 104 | STICK BUTTON = 6high BITMASK - RIGHT LEFT OPTION SHARE (8 4 2 1) 105 | TRIGGERS = 6low BITMASK (R2 L2 R1 L1) (8 4 2 1) 106 | L2 analog = 8 (0-255) 107 | R2 analog = 9 (0-255) 108 | TOUCHPAD 109 | 110 | */ 111 | -------------------------------------------------------------------------------- /dist/controllers/sidewinderpp.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | const hatcodes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']; 3 | // quick and dirty 6 bit twos complement 4 | // because of how javascript handles bitwise NOT 5 | // of unsigned values... it does the NOT, 6 | // but then it interprets the result as a SIGNED value 7 | // which ends up negative because the original was unsigned 8 | function unsigned2signed(val, bits = 8) { 9 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 10 | let mask = Math.pow(2, bits); // 0b1_0000_0000 11 | if (val & signBit) 12 | return -(mask + ~val + 1); 13 | else 14 | return val; 15 | } 16 | function deepEquals(a, b) { 17 | if (typeof a != typeof b) 18 | return false; 19 | if (Array.isArray(a) && Array.isArray(b)) { 20 | for (let i = 0; i < a.length; i++) { 21 | if (!deepEquals(a[i], b[i])) 22 | return false; 23 | } 24 | } 25 | else if (typeof a == 'object' && a != null) { 26 | for (let key of Object.keys(a)) { 27 | if (!deepEquals(a[key], b[key])) 28 | return false; 29 | } 30 | } 31 | else { 32 | return a == b; 33 | } 34 | return true; 35 | } 36 | class SidewinderPP { 37 | constructor() { 38 | this.hid = new HID.HID(1118, 8); 39 | this.status = {}; 40 | this.callback = null; 41 | this.hid.on("data", (data) => { 42 | this.handleData(data); 43 | }); 44 | } 45 | onUpdate(f) { 46 | this.callback = f; 47 | } 48 | handleData(data) { 49 | let status = {}; 50 | status.t1 = (data[0] & 0b00010000) > 0; 51 | status.t2 = (data[0] & 0b00100000) > 0; 52 | status.t3 = (data[0] & 0b01000000) > 0; 53 | status.t4 = (data[0] & 0b10000000) > 0; 54 | let hatval = data[0] & 0b00001111; 55 | let hatcode = hatcodes[hatval]; 56 | status.hat = { value: hatval, code: hatcode }; 57 | let x = data[1] | ((data[2] & 0b00000011) << 8); 58 | status.x = unsigned2signed(x, 10); 59 | let a = data[2] & 0b11111100; // ignore the two bits used as the sign for x 60 | let b = data[3] & 0b00001111; // ignore the part used for z 61 | let y = (b << 6) | (a >> 2); 62 | status.y = unsigned2signed(y, 10); 63 | // handle z axis 64 | let sign = (data[4] & 0b11) << 4; 65 | let val = data[3] >> 4; 66 | let final = sign | val; 67 | status.z = unsigned2signed(final, 6); 68 | status.buttonA = (data[4] & 0b00000100) > 0; 69 | status.buttonB = (data[4] & 0b00001000) > 0; 70 | status.buttonC = (data[4] & 0b00010000) > 0; 71 | status.buttonD = (data[4] & 0b00100000) > 0; 72 | status.buttonArrow = (data[4] & 0b01000000) > 0; 73 | status.dial = unsigned2signed(data[5], 7); 74 | if (!deepEquals(status, this.status)) { 75 | this.status = status; 76 | if (this.callback != null) 77 | this.callback(status); 78 | } 79 | } 80 | } 81 | module.exports = SidewinderPP; 82 | /* SIDEWINDER PRECISION PRO 83 | first four bits are the top buttons 84 | trigger = 1 85 | left button = 2 86 | top right = 4 87 | bottom right = 8 88 | 89 | next four bits are the nine positions of the hat 90 | 0-8 up-nothing 91 | 92 | next 8 bits are the least significant bits of the 10 bit X axis (signed) 93 | next 6 bits are the least significant bits of the 10 bit Y axis (signed) 94 | next 2 bits are the most significant bits of the X axis 95 | next 4 bits are the least significant bits of the 6 bit Z axis (signed) 96 | next 4 bits are the most significant bits of the Y axis 97 | next 6 bits are used for buttons 98 | next 2 bits are used for the most significant bits of the Z axis 99 | 100 | it works like this: 101 | byte 3 - cdef---- 102 | byte 4 - ------ab 103 | final value - abcdef as a six bit signed value 104 | 105 | sample code: 106 | let sign = data[4] << 4; 107 | let val = data[3] >> 4; 108 | let final = sign | val; 109 | // six bit signed 2s complement 110 | final = unsigned2signed(final, 6); 111 | console.log(sign.toString(2), val.toString(2), final.toString(2), final); 112 | 113 | 114 | byte 4 also holds the data for buttons c, d, arrow, b, a 115 | in the most significant six bits 116 | 117 | bits 0 and 1 are used as the sign bit for the z axis 118 | bit 2 is button A 119 | bit 3 is button B 120 | bit 4 is arrow 121 | bit 5 is d 122 | bit 6 is c 123 | 124 | next 8 bits are the dial but only 7 bits are used 125 | when centered, the value is 00 126 | it's value is a 7 bit 2s complement signed integer 127 | it increments when moving counter-clockwise 128 | -64 – 63 129 | 130 | represented as a signed 7 bit integer 131 | incrementing when moving counterclockwise 132 | 133 | */ 134 | -------------------------------------------------------------------------------- /dist/visca/camera.js: -------------------------------------------------------------------------------- 1 | const { v4: uuid } = require('uuid'); 2 | const { EventEmitter } = require('events'); 3 | const { Command, PTStatus, CamLensData, CamImageData } = require('./command'); 4 | const { Constants: C } = require('./constants'); 5 | class CameraStatus { 6 | constructor(pan = 0, tilt = 0, zoom = 0, dzoom = false, effect = 0) { 7 | this.pan = pan; 8 | this.tilt = tilt; 9 | this.zoom = zoom; 10 | this.dzoom = dzoom; 11 | this.effect = effect; 12 | this.updateImageData({}); 13 | this.updateLensData({}); 14 | this.updatePTStatus({}); 15 | } 16 | // takes CamImageData 17 | updateImageData(imageData) { 18 | this.gain = imageData.gain; 19 | this.gainr = imageData.gainr; 20 | this.gainb = imageData.gainb; 21 | this.wbMode = imageData.wbMode; 22 | this.exposureMode = imageData.exposureMode; 23 | this.shutterPos = imageData.shutterPos; 24 | this.irisPos = imageData.irisPos; 25 | this.gainPos = imageData.gainPos; 26 | this.brightness = imageData.brightness; 27 | this.exposure = imageData.exposure; 28 | this.highResEnabled = imageData.highResEnabled; 29 | this.wideDEnabled = imageData.wideDEnabled; 30 | this.backlightCompEnabled = imageData.backlightCompEnabled; 31 | this.exposureCompEnabled = imageData.exposureCompEnabled; 32 | this.slowShutterAutoEnabled = imageData.slowShutterAutoEnabled; 33 | } 34 | updateLensData(lensData) { 35 | this.zooming = lensData.zooming; 36 | this.zoomPos = lensData.zoomPos; 37 | this.digitalZoomEnabled = lensData.digitalZoomEnabled; 38 | this.focusing = lensData.focusing; 39 | this.focusPos = lensData.focusPos; 40 | this.focusNearLimit = lensData.focusNearLimit; 41 | this.autoFocusMode = lensData.autoFocusMode; 42 | this.autoFocusSensitivity = lensData.autoFocusSensitivity; 43 | this.autoFocusEnabled = lensData.autoFocusEnabled; 44 | this.lowContrast = lensData.lowContrast; 45 | this.loadingPreset = lensData.loadingPreset; 46 | } 47 | updatePTStatus(ptStatus) { 48 | this.initStatus = ptStatus.initStatus; 49 | this.initializing = ptStatus.initializing; 50 | this.ready = ptStatus.ready; 51 | this.fail = ptStatus.fail; 52 | this.moveStatus = ptStatus.moveStatus; 53 | this.moveDone = ptStatus.moveDone; 54 | this.moveFail = ptStatus.moveFail; 55 | this.atMaxL = ptStatus.atMaxL; 56 | this.atMaxR = ptStatus.atMaxR; 57 | this.atMaxU = ptStatus.atMaxU; 58 | this.atMaxD = ptStatus.atMaxD; 59 | this.moving = ptStatus.moving; 60 | } 61 | } 62 | class Camera extends EventEmitter { 63 | // transport should support the socket interface => .send(ViscaCommand) 64 | constructor(index, transport) { 65 | var _a; 66 | this.index = index; 67 | this.transport = transport; 68 | this.cameraBuffers = {}; 69 | this.sentCommands = []; // FIFO stack for commands 70 | this.commandQueue = []; 71 | this.inquiryQueue = []; 72 | this.status = new CameraStatus(); 73 | this.commandReady = true; // true when camera can receive commands 74 | this.inquiryReady = true; 75 | this.updatetimer = 0; 76 | this.uuid = (_a = transport.uuid) !== null && _a !== void 0 ? _a : index; // UDPTransports provide a unique uuid 77 | } 78 | _clear() { this.cameraBuffers = {}; this.sentCommands = []; } 79 | _update() { 80 | this._clearOldCommands(); 81 | this.commandReady = !(1 in this.cameraBuffers || 2 in this.cameraBuffers); 82 | this.inquiryReady = !(0 in this.cameraBuffers); 83 | this._processQueue(); 84 | if (this.inquiryQueue.length > 0 || this.commandQueue.length > 0) { 85 | clearTimeout(this.updatetimer); 86 | this.updatetimer = setTimeout(this._update, 20); 87 | } 88 | this.emit('update'); 89 | } 90 | // if a command in the stack is older than 2 seconds drop it 91 | _clearOldCommands() { 92 | let now = Date.now(); 93 | while (this.sentCommands.length > 0) { 94 | if (now - this.sentCommands[0].addedAt < 1000) 95 | break; 96 | this.sentCommands.splice(0, 1); 97 | } 98 | for (let key of Object.keys(this.cameraBuffers)) { 99 | if (now - this.cameraBuffers[key].addedAt > 1000) 100 | this.sentCommands.splice(0, 1); 101 | } 102 | } 103 | _processQueue() { 104 | if (this.commandReady && this.commandQueue.length > 0) { 105 | this.sendCommand(this.commandQueue.splice(0, 1)); 106 | } 107 | if (this.inquiryReady && this.inquiryQueue.length > 0) { 108 | this.sendCommand(this.inquiryQueue.splice(0, 1)); 109 | } 110 | } 111 | // treat commands that don't send ack as if 112 | // they were stored in camera socket 0 113 | // because the parsed response will have socket 0. 114 | // other commands will be put on the stack until 115 | // the ack tells us which socket received it 116 | sendCommand(command) { 117 | // update the header data 118 | command.source = 0; 119 | command.recipient = this.index; 120 | command.broadcast = false; 121 | // add metadata so we can expire old commands 122 | command.addedAt = Date.now(); 123 | let queued = false; 124 | // INTERFACE_DATA, ADDRESS_SET commands always get sent and aren't tracked 125 | // keep track of other commands in order, so we can match replies to commands 126 | if (command.msgType == MSGTYPE_INQUIRY) { 127 | // only allow one non-ack command at a time 128 | if (this.inquiryReady) { 129 | this.cameraBuffers[0] = command; // no ACK, only complete / error 130 | } 131 | else { 132 | this.inquiryQueue.push(command); 133 | queued = true; 134 | } 135 | } 136 | else if (command.msgType == MSGTYPE_COMMAND) { 137 | if (this.commandReady) { 138 | this.sentCommands.push(command); // not in a buffer until we get ACK 139 | } 140 | else { 141 | this.commandQueue.push(command); 142 | queued = true; 143 | } 144 | } 145 | if (!queued) 146 | this.transport.send(command); 147 | this._update(); 148 | } 149 | ack(viscaCommand) { 150 | // get the first viscaCommand that expects an ACK 151 | let cmd = this.sentCommands.splice(0, 1); // pops the head 152 | cmd.ack(); // run the command ACK callback if it exists 153 | this.cameraBuffers[viscaCommand.socket] = cmd; 154 | this._update(); 155 | } 156 | complete(viscaCommand) { 157 | this.cameraBuffers[viscaCommand.socket].complete(viscaCommand.data); 158 | delete (this.cameraBuffers[viscaCommand.socket]); 159 | this._update(); 160 | } 161 | error(viscaCommand) { 162 | let message; 163 | let errorType = viscaCommand.data[0]; 164 | switch (errorType) { 165 | case C.ERROR_SYNTAX: 166 | message = `syntax error, invalid command`; 167 | break; 168 | case C.ERROR_BUFFER_FULL: 169 | message = `command buffers full`; 170 | break; 171 | case C.ERROR_CANCELLED: 172 | // command was cancelled 173 | message = 'cancelled'; 174 | break; 175 | case C.ERROR_INVALID_BUFFER: 176 | message = `socket cannot be cancelled`; 177 | break; 178 | case C.ERROR_COMMAND_FAILED: 179 | message = `command failed`; 180 | break; 181 | } 182 | console.log(`camera ${this.index}-${viscaCommand.socket}: ${message}`); 183 | this.cameraBuffers[viscaCommand.socket].error(errorType); 184 | delete (this.cameraBuffers[viscaCommand.socket]); 185 | this._update(); 186 | } 187 | inquireAll() { 188 | this.getCameraPower(); 189 | this.getCameraPanStatus(); 190 | this.getCameraLens(); 191 | this.getCameraImage(); 192 | this.getCameraPanPos(); 193 | } 194 | // camera specific inquiry commands 195 | // ---------------- Inquiries --------------------------- 196 | getCameraPower() { let v = Command.inqCameraPower(this.index, (data) => { this.status.powerStatus = data; }); this.sendCommand(v); } 197 | getCameraICRMode() { let v = Command.inqCameraICRMode(this.index, (data) => { this.status.icrMode = data; }); this.sendCommand(v); } 198 | getCameraICRAutoMode() { let v = Command.inqCameraICRAutoMode(this.index, (data) => { this.status.icrAutoMode = data; }); this.sendCommand(v); } 199 | getCameraICRThreshold() { let v = Command.inqCameraICRThreshold(this.index, (data) => { this.status.icrThreshold = data; }); this.sendCommand(v); } 200 | getCameraGainLimit() { let v = Command.inqCameraGainLimit(this.index, (data) => { this.status.gainLimit = data; }); this.sendCommand(v); } 201 | getCameraGain() { let v = Command.inqCameraGain(this.index, (data) => { this.status.gain = data; }); this.sendCommand(v); } 202 | getCameraGainR() { let v = Command.inqCameraGainR(this.index, (data) => { this.status.gainr = data; }); this.sendCommand(v); } 203 | getCameraGainB() { let v = Command.inqCameraGainB(this.index, (data) => { this.status.gainb = data; }); this.sendCommand(v); } 204 | getCameraDZoomMode() { let v = Command.inqCameraDZoomMode(this.index, (data) => { this.status.dzoom = data; }); this.sendCommand(v); } 205 | getCameraZoomPos() { let v = Command.inqCameraZoomPos(this.index, (data) => { this.status.zoomPos = data; }); this.sendCommand(v); } 206 | getCameraFocusAutoStatus() { let v = Command.inqCameraFocusAutoStatus(this.index, (data) => { this.status.focusAutoStatus = data; }); this.sendCommand(v); } 207 | getCameraFocusAutoMode() { let v = Command.inqCameraFocusAutoMode(this.index, (data) => { this.status.focusAutoMode = data; }); this.sendCommand(v); } 208 | getCameraFocusIRCorrection() { let v = Command.inqCameraFocusIRCorrection(this.index, (data) => { this.status.focusIRCorrection = data; }); this.sendCommand(v); } 209 | getCameraFocusPos() { let v = Command.inqCameraFocusPos(this.index, (data) => { this.status.focusPos = data; }); this.sendCommand(v); } 210 | getCameraFocusNearLimit() { let v = Command.inqCameraFocusNearLimit(this.index, (data) => { this.status.focusNearLimit = data; }); this.sendCommand(v); } 211 | getCameraFocusAutoIntervalTime() { let v = Command.inqCameraFocusAutoIntervalTime(this.index, (data) => { this.status.focusAutoIntervalTime = data; }); this.sendCommand(v); } 212 | getCameraFocusSensitivity() { let v = Command.inqCameraFocusSensitivity(this.index, (data) => { this.status.autoFocusSensitivity = data; }); this.sendCommand(v); } 213 | getCameraWBMode() { let v = Command.inqCameraWBMode(this.index, (data) => { this.status.wbMode = data; }); this.sendCommand(v); } 214 | getCameraExposureMode() { let v = Command.inqCameraExposureMode(this.index, (data) => { this.status.exposureMode = data; }); this.sendCommand(v); } 215 | getCameraShutterSlowMode() { let v = Command.inqCameraShutterSlowMode(this.index, (data) => { this.status.shutterSlowMode = data; }); this.sendCommand(v); } 216 | getCameraShutter() { let v = Command.inqCameraShutterPos(this.index, (data) => { this.status.shutterPos = data; }); this.sendCommand(v); } 217 | getCameraIris() { let v = Command.inqCameraIris(this.index, (data) => { this.status.irisPos = data; }); this.sendCommand(v); } 218 | getCameraBrightness() { let v = Command.inqCameraBrightness(this.index, (data) => { this.status.brightness = data; }); this.sendCommand(v); } 219 | getCameraExposureCompensationStatus() { let v = Command.inqCameraExposureCompensationStatus(this.index, (data) => { this.status.exposureCompEnabled = data; }); this.sendCommand(v); } 220 | getCameraExposureCompensation() { let v = Command.inqCameraExposureCompensation(this.index, (data) => { this.status.exposureCompLevel = data; }); this.sendCommand(v); } 221 | getCameraBacklightStatus() { let v = Command.inqCameraBacklightStatus(this.index, (data) => { this.status.backlightCompEnabled = data; }); this.sendCommand(v); } 222 | getCameraWideDStatus() { let v = Command.inqCameraWideDStatus(this.index, (data) => { this.status.wideDEnabled = data; }); this.sendCommand(v); } 223 | getCameraWideD() { let v = Command.inqCameraWideD(this.index, (data) => { this.status.wideDLevel = data; }); this.sendCommand(v); } 224 | getCameraAperture() { let v = Command.inqCameraAperture(this.index, (data) => { this.status.aperture = data; }); this.sendCommand(v); } 225 | getCameraHighResStatus() { let v = Command.inqCameraHighResStatus(this.index, (data) => { this.status.highResEnabled = data; }); this.sendCommand(v); } 226 | getCameraNoiseReductionStatus() { let v = Command.inqCameraNoiseReductionStatus(this.index, (data) => { this.status.noiseReductionEnabled = data; }); this.sendCommand(v); } 227 | getCameraHighSensitivityStatus() { let v = Command.inqCameraHighSensitivityStatus(this.index, (data) => { this.status.hightSensitivityEnabled = data; }); this.sendCommand(v); } 228 | getCameraFreezeStatus() { let v = Command.inqCameraFreezeStatus(this.index, (data) => { this.status.freeze = data; }); this.sendCommand(v); } 229 | getCameraEffect() { let v = Command.inqCameraEffect(this.index, (data) => { this.status.effect = data; }); this.sendCommand(v); } 230 | getCameraEffectDigital() { let v = Command.inqCameraEffectDigital(this.index, (data) => { this.status.digitalEffect = data; }); this.sendCommand(v); } 231 | getCameraEffectDigitalLevel() { let v = Command.inqCameraEffectDigitalLevel(this.index, (data) => { this.status.digitalEffectLevel = data; }); this.sendCommand(v); } 232 | getCameraID() { let v = Command.inqCameraID(this.index, (data) => { this.status.cameraID = data; }); this.sendCommand(v); } 233 | getCameraChromaSuppressStatus() { let v = Command.inqCameraChromaSuppressStatus(this.index, (data) => { this.status.chromaSuppresEnabled = data; }); this.sendCommand(v); } 234 | getCameraColorGain() { let v = Command.inqCameraColorGain(this.index, (data) => { this.status.colorGain = data; }); this.sendCommand(v); } 235 | getCameraColorHue() { let v = Command.inqCameraColorHue(this.index, (data) => { this.status.colorHue = data; }); this.sendCommand(v); } 236 | // these use op commands 237 | getVideoSystemNow() { let v = Command.inqVideoSystemNow(this.index, (data) => { this.status.videoSystemNow = data; }); this.sendCommand(v); } 238 | getVideoSystemNext() { let v = Command.inqVideoSystemNext(this.index, (data) => { this.status.videoSystemPending = data; }); this.sendCommand(v); } 239 | getCameraPanPos() { let v = Command.inqCameraPanPos(this.index, (data) => { this.status.panPos = data; }); this.sendCommand(v); } 240 | getCameraPanSpeed() { let v = Command.inqCameraPanSpeed(this.index, (data) => { this.status.panSpeed = data; }); this.sendCommand(v); } 241 | getCameraPanStatus() { let v = Command.inqCameraPanStatus(this.index, (data) => { this.status.updatePTStatus(data); }); this.sendCommand(v); } 242 | // block inquiry commands 243 | getCameraLens() { let v = Command.inqCameraLens(this.index, (data) => { this.status.updateLensData(data); }); this.sendCommand(v); } 244 | getCameraImage() { let v = Command.inqCameraImage(this.index, (data) => { this.status.updateImageData(data); }); this.sendCommand(v); } 245 | } 246 | module.exports = { Camera }; 247 | -------------------------------------------------------------------------------- /dist/visca/command.js: -------------------------------------------------------------------------------- 1 | // import and create an alias 2 | const { Constants: C } = require('./constants'); 3 | // according to the documentation: 4 | // https://ptzoptics.com/wp-content/uploads/2014/09/PTZOptics-VISCA-over-IP-Commands-Rev1_0-5-18.pdf 5 | // and from https://www.epiphan.com/userguides/LUMiO12x/Content/UserGuides/PTZ/3-operation/Commands.htm 6 | // and from the Sony EVI H100S User Manual 7 | // 8 | // |------packet (3-16 bytes)---------| 9 | // header message terminator 10 | // (1 byte) (1-14 bytes) (1 byte) 11 | // | X | X . . . . . . . . . . X | X | 12 | // HEADER: 13 | // addressed header 14 | // header bits: terminator: 15 | // 1 s2 s1 s0 0 r2 r1 r0 0xff 16 | // with r,s = recipient, sender msb first (big endian for bytes and bits) 17 | // 18 | // broadcast header is always 0x88! 19 | // CONTROL MESSAGE FORMAT 20 | // QQ RR ... 21 | // QQ = 0x01 (Command) or 0x09 (Inquiry) 22 | // RR = 0x00 (interface), 0x04 (camera), 0x06 (pan/tilt), 0x7(d|e) other 23 | // ... data 24 | // REPLY MESSAGE FORMAT 25 | // Camera responses come in three types 26 | // COMMAND ACK: header 0x4y 0xff -- command accepted, y = socket (index of command in buffer) 27 | // COMMAND COMPLETE: header 0x5y 0xff -- command executed, y = socket (index of buffered command) 28 | // INQUIRY COMPLETE: header 0x50 data 0xff -- inquiry response data 29 | class Command { 30 | constructor(opts = {}) { 31 | opts = this._mergeDefaults(opts); 32 | // header items 33 | this.source = opts.source & 0b111; // only 0-7 allowed 34 | this.recipient = opts.recipient; // -1 for broadcast 35 | this.broadcast = opts.broadcast == true; 36 | // message type is the QQ in the spec 37 | this.msgType = opts.msgType & 0b11111111; // one byte allowed 38 | this.socket = opts.socket & 0b111; 39 | // data might be empty 40 | this.dataType = opts.dataType; 41 | this.data = opts.data; 42 | this.onComplete = opts.onComplete; 43 | this.onError = opts.onError; 44 | this.onAck = opts.onAck; 45 | this.dataParser = opts.dataParser; 46 | this.status = 0; 47 | } 48 | static fromPacket(packet) { 49 | let v = new Command(); 50 | v._parsePacket(packet); 51 | return v; 52 | } 53 | static raw(recipient, raw) { 54 | let v = new Command({ recipient }); 55 | v._parsePacket([v.header(), ...raw, 0xff]); 56 | return v; 57 | } 58 | // use recipient -1 for broadcast 59 | static shortcut(recipient = -1, msgType = 0x00, dataType = 0x00, data = [], callbacks = {}) { 60 | let { onComplete, onError, onAck } = callbacks; 61 | let broadcast = (recipient == -1); 62 | let v = new Command({ 63 | recipient, broadcast, msgType, dataType, data, onComplete, onError, onAck 64 | }); 65 | return v; 66 | } 67 | // defaults to an invalid command 68 | _mergeDefaults(opts = {}) { 69 | var _a; 70 | if (opts.broadcast) 71 | opts.recipient = -1; 72 | if (opts.recipient > -1) 73 | opts.broadcast = false; 74 | let defaults = { 75 | source: 0, 76 | recipient: -1, 77 | broadcast: true, 78 | msgType: C.MSGTYPE_COMMAND, 79 | socket: 0, 80 | dataType: 0, 81 | data: [], 82 | onComplete: null, 83 | onError: null, 84 | onAck: null, 85 | dataParser: null, 86 | }; 87 | for (let key in Object.keys(defaults)) { 88 | defaults[key] = (_a = opts[key]) !== null && _a !== void 0 ? _a : defaults[key]; 89 | } 90 | return defaults; 91 | } 92 | _parsePacket(packet) { 93 | let header = packet[0]; 94 | this.source = (header & C.HEADERMASK_SOURCE) >> 4; 95 | this.recipient = header & C.HEADERMASK_RECIPIENT; // replies have recipient 96 | this.broadcast = ((header & C.HEADERMASK_BROADCAST) >> 3) == 1; 97 | switch (packet[1]) { 98 | case C.MSGTYPE_COMMAND: 99 | case C.MSGTYPE_INQUIRY: 100 | case C.MSGTYPE_ADDRESS_SET: 101 | case C.MSGTYPE_NETCHANGE: 102 | this.msgType = packet[1]; 103 | this.socket = 0; 104 | break; 105 | default: 106 | this.socket = packet[1] & 0b00001111; 107 | this.msgType = packet[1] & 0b11110000; 108 | } 109 | this.data = packet.slice(2, packet.length - 1); // might be empty, ignore terminator 110 | // if data is more than one byte, the first byte determines the dataType 111 | this.dataType = (data.length < 2) ? 0 : data.splice(0, 1); 112 | } 113 | // instance methods 114 | header() { 115 | let header = 0x88; 116 | // recipient overrides broadcast 117 | if (this.recipient > -1) 118 | this.broadcast = false; 119 | if (!this.broadcast) { 120 | header = 0b10000000 | (this.source << 4) | (this.recipient & 0x111); 121 | } 122 | return header; 123 | } 124 | toPacket() { 125 | let header = this.header(); 126 | let qq = this.msgType | this.socket; 127 | let rr = this.dataType; 128 | if (rr > 0) 129 | return Buffer.from([header, qq, rr, ...this.data, 0xff]); 130 | else 131 | return Buffer.from([header, qq, ...this.data, 0xff]); 132 | } 133 | send(transport) { 134 | transport.write(this); 135 | } 136 | ack() { 137 | this.status = C.MSGTYPE_ACK; 138 | if (this.onAck != null) 139 | this.onAck(); 140 | } 141 | error() { 142 | this.status = C.MSGTYPE_ERROR; 143 | if (this.onError != null) 144 | this.onError(); 145 | } 146 | // some command completions include data 147 | complete(data = null) { 148 | this.status = C.MSGTYPE_COMPLETE; 149 | if (this.dataParser != null && data != null) { 150 | data = this.dataParser(data); 151 | } 152 | if (this.onComplete != null) { 153 | if (data == null || data.length == 0) 154 | this.onComplete(); 155 | else 156 | this.onComplete(data); 157 | } 158 | } 159 | // commands for each message type 160 | static addressSet() { 161 | return new Command({ msgType: C.MSGTYPE_ADDRESS_SET, data: [1] }); 162 | } 163 | static cmd(recipient = -1, dataType, data = []) { 164 | return new Command({ msgType: C.MSGTYPE_COMMAND, dataType, recipient, data }); 165 | } 166 | static inquire(recipient = -1, dataType, data, onComplete, dataParser) { 167 | return new Command({ msgType: C.MSGTYPE_INQUIRY, dataType, recipient, data, dataParser, onComplete }); 168 | } 169 | static cancel(recipient = -1, socket = 0) { 170 | return new Command({ msgType: C.MSGTYPE_CANCEL | socket, recipient }); 171 | } 172 | // commands for each datatype 173 | static cmdInterfaceClearAll(recipient = -1) { 174 | return Command.cmd(recipient, C.DATATYPE_INTERFACE, [0, 1]); 175 | } 176 | static cmdCamera(recipient = -1, data = []) { 177 | return Command.cmd(recipient, C.DATATYPE_CAMERA, data); 178 | } 179 | static cmdOp(recipient = -1, data = []) { 180 | return Command.cmd(recipient, C.DATATYPE_OPERATION, data); 181 | } 182 | static inqCamera(recipient = -1, query, onComplete, dataParser) { 183 | return Command.inquire(recipient, C.DATATYPE_CAMERA, query, onComplete, dataParser); 184 | } 185 | static inqOp(recipient = -1, query, onComplete, dataParser) { 186 | return Command.inquire(recipient, C.DATATYPE_OPERATION, query, onComplete, dataParser); 187 | } 188 | // ----------------------- Setters ------------------------------------- 189 | // POWER =========================== 190 | static cmdCameraPower(recipient, enable = false) { 191 | let powerval = enable ? C.DATA_ONVAL : C.DATA_OFFVAL; 192 | let subcmd = [C.CAM_POWER, powerval]; 193 | return Command.cmdCamera(recipient, subcmd); 194 | } 195 | static cmdCameraPowerAutoOff(device, time = 0) { 196 | // time = minutes without command until standby 197 | // 0: disable 198 | // 0xffff: 65535 minutes 199 | let subcmd = [0x40, ...i2v(time)]; 200 | return Command.cmdCamera(device, subcmd); 201 | } 202 | // PRESETS ========================= 203 | // Store custom presets if the camera supports them 204 | // PTZOptics can store presets 0-127 205 | // Sony has only 0-5 206 | static cmdCameraPresetReset(device, preset = 0) { 207 | let subcmd = [C.CAM_MEMORY, 0x00, preset]; 208 | return Command.cmdCamera(device, subcmd); 209 | } 210 | static cmdCameraPresetSet(device, preset = 0) { 211 | let subcmd = [C.CAM_MEMORY, 0x01, preset]; 212 | return Command.cmdCamera(device, subcmd); 213 | } 214 | static cmdCameraPresetRecall(device, preset = 0) { 215 | let subcmd = [C.CAM_MEMORY, 0x02, preset]; 216 | return Command.cmdCamera(device, subcmd); 217 | } 218 | // PAN/TILT =========================== 219 | // 8x 01 06 01 VV WW XX YY FF 220 | // VV = x(pan) speed 1-18 221 | // WW = y(tilt) speed 1-17 222 | // XX = x mode 01 (dec), 02 (inc), 03 (stop) 223 | // YY = y mode 01 (dec), 02 (inc), 03 (stop) 224 | // x increases rightward 225 | // y increases downward!! 226 | static cmdCameraPanTilt(device, xspeed, yspeed, xmode, ymode) { 227 | let subcmd = [C.OP_PAN_DRIVE, xspeed, yspeed, xmode, ymode]; 228 | return Command.cmdOp(device, subcmd); 229 | } 230 | // x and y are signed 16 bit integers, 0x0000 is center 231 | // range is -2^15 - 2^15 (32768) 232 | // relative defaults to false 233 | static cmdCameraPanTiltDirect(device, xspeed, yspeed, x, y, relative = false) { 234 | let xpos = si2v(x); 235 | let ypos = si2v(y); 236 | let absrel = relative ? C.OP_PAN_RELATIVE : C.OP_PAN_ABSOLUTE; 237 | let subcmd = [absrel, xspeed, yspeed, ...xpos, ...ypos]; 238 | return Command.cmdOp(device, subcmd); 239 | } 240 | static cmdCameraPanTiltHome(device) { return Command.cmdOp(device, [C.OP_PAN_HOME]); } 241 | static cmdCameraPanTiltReset(device) { return Command.cmdOp(device, [C.OP_PAN_RESET]); } 242 | // corner should be C.DATA_PANTILT_UR or C.DATA_PANTILT_BL 243 | static cmdCameraPanTiltLimitSet(device, corner, x, y) { 244 | x = si2v(x); 245 | y = si2v(y); 246 | let subcmd = [C.OP_PAN_LIMIT, 0x00, corner, ...x, ...y]; 247 | return Command.cmdOp(device, subcmd); 248 | } 249 | static cmdCameraPanTiltLimitClear(device, corner) { 250 | let subcmd = [C.OP_PAN_LIMIT, 0x01, corner, 0x07, 0x0F, 0x0F, 0x0F, 0x07, 0x0F, 0x0F, 0x0F]; 251 | return Command.cmdOp(device, subcmd); 252 | } 253 | // ZOOM =============================== 254 | /// offinout = 0x00, 0x02, 0x03 255 | /// speed = 0(low)..7(high) (-1 means default) 256 | static cmdCameraZoom(device, offinout = 0x00, speed = -1) { 257 | let data = offinout; 258 | if (speed > -1 && offinout != 0x00) 259 | data = (data << 8) + (speed & 0b111); 260 | let subcmd = [C.CAM_ZOOM, data]; 261 | return Command.cmdCamera(device, subcmd); 262 | } 263 | static cmdCameraZoomStop(device) { 264 | return Command.cmdCameraZoom(device, 0x00); 265 | } 266 | /// zoom in with speed = 0..7 (-1 means default) 267 | static cmdCameraZoomIn(device, speed = -1) { 268 | return Command.cmdCameraZoom(device, C.DATA_ZOOMIN, speed); 269 | } 270 | /// zoom out with speed = 0..7 (-1 means default) 271 | static cmdCameraZoomOut(device, speed = -1) { 272 | return Command.cmdCameraZoom(device, C.DATA_ZOOMOUT, speed); 273 | } 274 | /// max zoom value is 0x4000 (16384) unless digital is enabled 275 | /// 0xpqrs -> 0x0p 0x0q 0x0r 0x0s 276 | static cmdCameraZoomDirect(device, zoomval) { 277 | let subcmd = [C.CAM_ZOOM_DIRECT, ...i2v(zoomval)]; 278 | return Command.cmdCamera(device, subcmd); 279 | } 280 | // Digital Zoom enable/disable 281 | static cmdCameraDigitalZoom(device, enable = false) { 282 | let data = enable ? C.DATA_ONVAL : C.DATA_OFFVAL; 283 | let subcmd = [C.CAM_DZOOM, data]; 284 | return Command.cmdCamera(device, subcmd); 285 | } 286 | // Focus controls 287 | /// stopfarnear = 0x00, 0x02, 0x03 288 | /// speed = 0(low)..7(high) -1 means default 289 | static cmdCameraFocus(device, stopfarnear = 0x00, speed = -1) { 290 | let data = stopfarnear; 291 | if (speed > -1 && stopfarnear != 0x00) 292 | data = (data << 8) + (speed & 0b111); 293 | let subcmd = [C.CAM_ZOOM, data]; 294 | return Command.cmdCamera(device, subcmd); 295 | } 296 | static cmdCameraFocusStop(device) { 297 | return Command.cmdCameraFocus(device, 0x00); 298 | } 299 | /// zoom in with speed = 0..7 (-1 means default) 300 | static cmdCameraFocusFar(device, speed = -1) { 301 | return Command.cmdCameraFocus(device, C.DATA_FOCUSFAR, speed); 302 | } 303 | /// zoom out with speed = 0..7 (-1 means default) 304 | static cmdCameraFocusNear(device, speed = -1) { 305 | return Command.cmdCameraFocus(device, C.DATA_FOCUSNEAR, speed); 306 | } 307 | /// max focus value is 0xF000 308 | /// 0xpqrs -> 0x0p 0x0q 0x0r 0x0s 309 | static cmdCameraFocusDirect(device, focusval) { 310 | let subcmd = [C.CAM_FOCUS_DIRECT, ...i2v(focusval)]; 311 | return Command.cmdCamera(device, subcmd); 312 | } 313 | static cmdCameraFocusAuto(device, enable = true) { 314 | let data = enable ? C.DATA_ONVAL : C.DATA_OFFVAL; 315 | let subcmd = [C.CAM_FOCUS_AUTO, data]; 316 | return Command.cmdCamera(device, subcmd); 317 | } 318 | static cmdCameraFocusAutoManual(device) { 319 | let subcmd = [C.CAM_FOCUS_AUTO, C.DATA_TOGGLEVAL]; 320 | return Command.cmdCamera(device, subcmd); 321 | } 322 | static cmdCameraFocusAutoTrigger(device) { 323 | let subcmd = [C.CAM_FOCUS_TRIGGER, 0x01]; 324 | return Command.cmdCamera(device, subcmd); 325 | } 326 | static cmdCameraFocusInfinity(device) { 327 | let subcmd = [C.CAM_FOCUS_TRIGGER, 0x02]; 328 | return Command.cmdCamera(device, subcmd); 329 | } 330 | static cmdCameraFocusSetNearLimit(device, limit = 0xf000) { 331 | // limit must have low byte 0x00 332 | limit = limit & 0xff00; 333 | let subcmd = [C.CAM_FOCUS_NEAR_LIMIT_POS, ...i2v(limit)]; 334 | return Command.cmdCamera(device, subcmd); 335 | } 336 | static cmdCameraFocusAutoSensitivity(device, high = true) { 337 | let data = high ? C.DATA_ONVAL : C.DATA_OFFVAL; 338 | let subcmd = [C.CAM_FOCUS_SENSE_HIGH, data]; 339 | return Command.cmdCamera(device, subcmd); 340 | } 341 | /// mode = 0 (normal), 1 (interval), 2 (trigger) 342 | static cmdCameraFocusAutoMode(device, mode = 0) { 343 | let subcmd = [C.CAM_FOCUS_AF_MODE, mode]; 344 | return Command.cmdCamera(device, subcmd); 345 | } 346 | static cmdCameraFocusAutoIntervalTime(device, movementTime = 0, intervalTime = 0) { 347 | let pqrs = (movementTime << 8) + intervalTime; 348 | let subcmd = [C.CAM_FOCUS_AF_INTERVAL, ...i2v(pqrs)]; 349 | return Command.cmdCamera(device, subcmd); 350 | } 351 | static cmdCameraFocusIRCorrection(device, enable = false) { 352 | let data = enable ? 0x00 : 0x01; 353 | let subcmd = [C.CAM_FOCUS_IR_CORRECTION, data]; 354 | return Command.cmdCamera(device, subcmd); 355 | } 356 | // combo zoom & focus 357 | static cmdCameraZoomFocus(device, zoomval = 0, focusval = 0) { 358 | let z = i2v(zoomval); 359 | let f = i2v(focusval); 360 | let subcmd = [C.CAM_ZOOM_DIRECT, ...z, ...f]; 361 | return Command.cmdCamera(device, subcmd); 362 | } 363 | // OTHER IMAGE CONTROLS 364 | /// white balance 365 | /// mode = 0(auto),1(indoor),2(outdoor),3(trigger),5(manual) 366 | static cmdCameraWBMode(device, mode = 0) { 367 | let subcmd = [C.CAM_WB_MODE, mode]; 368 | return Command.cmdCamera(device, subcmd); 369 | } 370 | static cmdCameraWBTrigger(device) { 371 | let subcmd = [C.CAM_WB_TRIGGER, 0x05]; 372 | return Command.cmdCamera(device, subcmd); 373 | } 374 | // VARIOUS EXPOSURE CONTROLS 375 | /// mode should be 'r' for RGain, 'b' for BGain. defaults to Gain 376 | /// resetupdown = 0, 2, 3 377 | /// value must be less than 0xff; 378 | static cmdCameraGain(device, mode = 'r', resetupdown = 0, directvalue = -1) { 379 | let subcmd; 380 | let gaintype; 381 | switch (mode) { 382 | case 'r': 383 | gaintype = C.CAM_RGAIN; 384 | break; 385 | case 'b': 386 | gaintype = C.CAM_BGAIN; 387 | break; 388 | default: 389 | gaintype = C.CAM_GAIN; 390 | break; 391 | } 392 | if (directvalue > 0) { 393 | gaintype += 0x40; 394 | subcmd = [gaintype, ...i2v(directvalue)]; 395 | } 396 | else { 397 | subcmd = [gaintype, resetupdown]; 398 | } 399 | return Command.cmdCamera(device, subcmd); 400 | } 401 | static cmdCameraGainUp(device) { let mode = ''; return Command.cmdCameraGain(device, mode, C.DATA_ONVAL); } 402 | static cmdCameraGainDown(device) { let mode = ''; return Command.cmdCameraGain(device, mode, C.DATA_OFFVAL); } 403 | static cmdCameraGainReset(device) { let mode = ''; return Command.cmdCameraGain(device, mode, 0x00); } 404 | static cmdCameraGainDirect(device, value) { let mode = 'r'; return Command.cmdCameraGain(device, mode, 0x00, value); } 405 | static cmdCameraGainRUp(device) { let mode = 'r'; return Command.cmdCameraGain(device, mode, C.DATA_ONVAL); } 406 | static cmdCameraGainRDown(device) { let mode = 'r'; return Command.cmdCameraGain(device, mode, C.DATA_OFFVAL); } 407 | static cmdCameraGainRReset(device) { let mode = 'r'; return Command.cmdCameraGain(device, mode, 0x00); } 408 | static cmdCameraGainRDirect(device, value) { let mode = 'r'; return Command.cmdCameraGain(device, mode, 0x00, value); } 409 | static cmdCameraGainBUp(device) { let mode = 'b'; return Command.cmdCameraGain(device, mode, C.DATA_ONVAL); } 410 | static cmdCameraGainBDown(device) { let mode = 'b'; return Command.cmdCameraGain(device, mode, C.DATA_OFFVAL); } 411 | static cmdCameraGainBReset(device) { let mode = 'b'; return Command.cmdCameraGain(device, mode, 0x00); } 412 | static cmdCameraGainBDirect(device, value) { let mode = 'b'; return Command.cmdCameraGain(device, mode, 0x00, value); } 413 | /// gain value is from 4-F 414 | static cmdCameraGainLimit(device, value) { 415 | let subcmd = [C.CAM_GAIN_LIMIT, value]; 416 | return Command.cmdCamera(device, subcmd); 417 | } 418 | // EXPOSURE ======================= 419 | /// mode = 0, 3, A, B, D 420 | /// auto, manual, shutter priority, iris priority, bright 421 | static cmdCameraExposureMode(device, mode = 0x00) { 422 | let subcmd = [C.CAM_EXPOSURE_MODE, mode]; 423 | return Command.cmdCamera(device, subcmd); 424 | } 425 | static cmdCameraExposureCompensationEnable(device, enable = true) { 426 | let subcmd = [C.CAM_EXP_COMP_ENABLE, enable ? 0x02 : 0x03]; 427 | return Command.cmdCamera(device, subcmd); 428 | } 429 | static cmdCameraExposureCompensationAdjust(device, resetupdown = 0x00) { 430 | let subcmd = [C.CAM_EXP_COMP, resetupdown]; 431 | return Command.cmdCamera(device, subcmd); 432 | } 433 | static cmdCameraExposureCompensationUp(device) { 434 | return Command.cmdCameraExposureCompensationAdjust(device, 0x02); 435 | } 436 | static cmdCameraExposureCompensationDown(device) { 437 | return Command.cmdCameraExposureCompensationAdjust(device, 0x03); 438 | } 439 | static cmdCameraExposureCompensationReset(device) { 440 | return Command.cmdCameraExposureCompensationAdjust(device, 0x00); 441 | } 442 | static cmdCameraExposureCompensationDirect(device, directval = 0) { 443 | let subcmd = [C.CAM_EXP_COMP_DIRECT, ...i2v(directval)]; 444 | return Command.cmdCamera(device, subcmd); 445 | } 446 | // BACKLIGHT ======================================= 447 | static cmdCameraBacklightCompensation(device, enable = true) { 448 | let subcmd = [C.CAM_BACKLIGHT, enable ? 0x02 : 0x03]; 449 | return Command.cmdCamera(device, subcmd); 450 | } 451 | // SHUTTER ======================================== 452 | /// resetupdown = 0, 2, 3 453 | static cmdCameraShutter(device, resetupdown = 0x00, directvalue = -1) { 454 | let subcmd = [C.CAM_SHUTTER, resetupdown]; 455 | if (directvalue > -1) { 456 | subcmd = [C.CAM_SHUTTER_DIRECT, ...i2v(directvalue)]; 457 | } 458 | return Command.cmdCamera(device, subcmd); 459 | } 460 | static cmdCameraShutterUp(device) { let r = 0x02; return Command.cmdCameraShutter(device, r); } 461 | static cmdCameraShutterDown(device) { let r = 0x03; return Command.cmdCameraShutter(device, r); } 462 | static cmdCameraShutterReset(device) { let r = 0x00; return Command.cmdCameraShutter(device, r); } 463 | static cmdCameraShutterDirect(device, value = 0) { let r = 0x00; return Command.cmdCameraShutter(device, r, value); } 464 | static cmdCameraShutterSlow(device, auto = true) { 465 | let subcmd = [C.CAM_SHUTTER_SLOW_AUTO, auto ? 0x02 : 0x03]; 466 | return Command.cmdCamera(device, subcmd); 467 | } 468 | /// IRIS ====================================== 469 | /// resetupdown = 0, 2, 3 470 | static cmdCameraIris(device, resetupdown = 0x00, directvalue = -1) { 471 | let subcmd = [C.CAM_IRIS, resetupdown]; 472 | if (directvalue > -1) { 473 | subcmd = [C.CAM_IRIS_DIRECT, ...i2v(directvalue)]; 474 | } 475 | return Command.cmdCamera(device, subcmd); 476 | } 477 | static cmdCameraIrisUp(device) { let r = 0x02; return Command.cmdCameraIris(device, r); } 478 | static cmdCameraIrisDown(device) { let r = 0x03; return Command.cmdCameraIris(device, r); } 479 | static cmdCameraIrisReset(device) { let r = 0x00; return Command.cmdCameraIris(device, r); } 480 | static cmdCameraIrisDirect(device, value = 0) { let r = 0x00; return Command.cmdCameraIris(device, r, value); } 481 | // APERTURE ===================================== 482 | /// resetupdown = 0, 2, 3 483 | static cmdCameraAperture(device, resetupdown = 0x00, directvalue = -1) { 484 | let subcmd = [C.CAM_APERTURE, resetupdown]; 485 | if (directvalue > -1) { 486 | subcmd = [C.CAM_APERTURE_DIRECT, ...i2v(directvalue)]; 487 | } 488 | return Command.cmdCamera(device, subcmd); 489 | } 490 | static cmdCameraApertureUp(device) { let r = 0x02; return Command.cmdCameraAperture(device, r); } 491 | static cmdCameraApertureDown(device) { let r = 0x03; return Command.cmdCameraAperture(device, r); } 492 | static cmdCameraApertureReset(device) { let r = 0x00; return Command.cmdCameraAperture(device, r); } 493 | static cmdCameraApertureDirect(device, value = 0) { let r = 0x00; return Command.cmdCameraAperture(device, r, value); } 494 | // QUALITY ================================== 495 | static cmdCameraHighResMode(device, enable = true) { 496 | let subcmd = [C.CAM_HIRES_ENABLE, enable ? 0x02 : 0x03]; 497 | return Command.cmdCamera(device, subcmd); 498 | } 499 | static cmdCameraHighSensitivityMode(device, enable = true) { 500 | let subcmd = [C.CAM_HIGH_SENSITIVITY, enable ? 0x02 : 0x03]; 501 | return Command.cmdCamera(device, subcmd); 502 | } 503 | /// val = 0..5 504 | static cmdCameraNoiseReduction(device, val) { 505 | let subcmd = [C.CAM_NOISE_REDUCTION, val]; 506 | return Command.cmdCamera(device, subcmd); 507 | } 508 | /// val = 0..4 509 | static cmdCameraGamma(device, val) { 510 | let subcmd = [C.CAM_GAMMA, val]; 511 | return Command.cmdCamera(device, subcmd); 512 | } 513 | // EFFECTS ======================================== 514 | /// effect types are enumerated in the constants file 515 | static cmdCameraEffect(device, effectType) { 516 | return Command.cmdCamera(device, [C.CAM_EFFECT, effectType]); 517 | } 518 | static cmdCameraEffectDigital(device, effectType) { 519 | return Command.cmdCamera(device, [C.CAM_EFFECT_DIGITAL, effectType]); 520 | } 521 | static cmdCameraEffectDigitalIntensity(device, level) { 522 | return Command.cmdCamera(device, [C.CAM_EFFECT_LEVEL, level]); 523 | } 524 | // basic effects 525 | static cmdCameraEffectOff(device) { 526 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_OFF); 527 | } 528 | static cmdCameraEffectPastel(device) { 529 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_PASTEL); 530 | } 531 | static cmdCameraEffectNegative(device) { 532 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_NEGATIVE); 533 | } 534 | static cmdCameraEffectSepia(device) { 535 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_SEPIA); 536 | } 537 | static cmdCameraEffectBW(device) { 538 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_BW); 539 | } 540 | static cmdCameraEffectSolar(device) { 541 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_SOLAR); 542 | } 543 | static cmdCameraEffectMosaic(device) { 544 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_MOSAIC); 545 | } 546 | static cmdCameraEffectSlim(device) { 547 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_SLIM); 548 | } 549 | static cmdCameraEffectStretch(device) { 550 | return Command.cmdCameraEffect(device, C.DATA_EFFECT_STRETCH); 551 | } 552 | // digital effects 553 | static cmdCameraEffectDigitalOff(device) { 554 | return Command.cmdCameraEffectDigital(device, C.DATA_EFFECT_OFF); 555 | } 556 | static cmdCameraEffectDigitalStill(device) { 557 | return Command.cmdCameraEffectDigital(device, C.DATA_EFFECT_STILL); 558 | } 559 | static cmdCameraEffectDigitalFlash(device) { 560 | return Command.cmdCameraEffectDigital(device, C.DATA_EFFECT_FLASH); 561 | } 562 | static cmdCameraEffectDigitalLumi(device) { 563 | return Command.cmdCameraEffectDigital(device, C.DATA_EFFECT_LUMI); 564 | } 565 | static cmdCameraEffectDigitalTrail(device) { 566 | return Command.cmdCameraEffectDigital(device, C.DATA_EFFECT_TRAIL); 567 | } 568 | // FREEZE ==================================== 569 | static cmdCameraFreeze(device, enable = true) { 570 | let mode = enable ? C.DATA_ONVAL : C.DATA_OFFVAL; 571 | let subcmd = [C.CAM_FREEZE, mode]; 572 | return Command.cmdCamera(device, subcmd); 573 | } 574 | // ICR ======================================= 575 | static cmdCameraICR(device, enable = true) { 576 | let subcmd = [C.CAM_ICR, enable ? 0x02 : 0x03]; 577 | return Command.cmdCamera(device, subcmd); 578 | } 579 | static cmdCameraICRAuto(device, enable = true) { 580 | let subcmd = [C.CAM_AUTO_ICR, enable ? 0x02 : 0x03]; 581 | return Command.cmdCamera(device, subcmd); 582 | } 583 | static cmdCameraICRAutoThreshold(device, val = 0) { 584 | let subcmd = [C.CAM_AUTO_ICR_THRESHOLD, ...i2v(val)]; 585 | return Command.cmdCamera(device, subcmd); 586 | } 587 | // ID write 588 | static cmdCameraIDWrite(device, data) { 589 | let subcmd = [C.CAM_ID_WRITE, ...i2v(data)]; 590 | return Command.cmdCamera(device, subcmd); 591 | } 592 | // Chroma Suppress 593 | // value = 0(off), 1-3 594 | static cmdCameraChromaSuppress(device, value) { 595 | let subcmd = [C.CAM_CHROMA_SUPPRESS, value]; 596 | return Command.cmdCamera(device, subcmd); 597 | } 598 | // value = 0h - Eh 599 | static cmdCameraColorGain(device, value) { 600 | let subcmd = [C.CAM_COLOR_GAIN, value]; 601 | return Command.cmdCamera(device, subcmd); 602 | } 603 | // value = 0h - Eh 604 | static cmdCameraColorHue(device, value) { 605 | let subcmd = [C.CAM_COLOR_HUE, value]; 606 | return Command.cmdCamera(device, subcmd); 607 | } 608 | } 609 | // TODO: 610 | // CAM_WIDE_D 611 | // VIDEO_SYSTEM_SET 612 | // IR Receive 613 | // IR Receive Return 614 | // Information Display 615 | // ---------------- Inquiries --------------------------- 616 | Command.inqCameraPower = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_POWER, onComplete, IsOnParser); 617 | Command.inqCameraICRMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_ICR, onComplete, IsOnParser); 618 | Command.inqCameraICRAutoMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_AUTO_ICR, onComplete, IsOnParser); 619 | Command.inqCameraICRThreshold = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_AUTO_ICR_THRESHOLD, onComplete, v2iParser); 620 | Command.inqCameraGainLimit = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_GAIN_LIMIT, onComplete); 621 | Command.inqCameraGain = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_GAIN_DIRECT, onComplete, v2iParser); 622 | Command.inqCameraGainR = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_RGAIN_DIRECT, onComplete, v2iParser); 623 | Command.inqCameraGainB = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_BGAIN_DIRECT, onComplete, v2iParser); 624 | Command.inqCameraDZoomMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_DZOOM, onComplete, IsOnParser); 625 | Command.inqCameraZoomPos = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_ZOOM_DIRECT, onComplete, v2iParser); 626 | Command.inqCameraFocusAutoStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_AUTO, onComplete, IsOnParser); 627 | Command.inqCameraFocusAutoMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_AF_MODE, onComplete); 628 | Command.inqCameraFocusIRCorrection = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_IR_CORRECTION, onComplete); 629 | Command.inqCameraFocusPos = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_DIRECT, onComplete, v2iParser); 630 | Command.inqCameraFocusNearLimit = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_NEAR_LIMIT_POS, onComplete, v2iParser); 631 | Command.inqCameraFocusAutoIntervalTime = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_AF_INTERVAL, onComplete, AFIntervalParser); 632 | Command.inqCameraFocusSensitivity = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FOCUS_SENSE_HIGH, onComplete); 633 | Command.inqCameraWBMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_WB_MODE, onComplete); 634 | Command.inqCameraExposureMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EXPOSURE_MODE, onComplete); 635 | Command.inqCameraShutterSlowMode = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_SHUTTER_SLOW_AUTO, onComplete, IsOnParser); 636 | Command.inqCameraShutterPos = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_SHUTTER_DIRECT, onComplete, v2iParser); 637 | Command.inqCameraIris = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_IRIS_DIRECT, onComplete, v2iParser); 638 | Command.inqCameraBrightness = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_BRIGHT_DIRECT, onComplete, v2iParser); 639 | Command.inqCameraExposureCompensationStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EXP_COMP_ENABLE, onComplete, IsOnParser); 640 | Command.inqCameraExposureCompensation = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EXP_COMP_DIRECT, onComplete, v2iParser); 641 | Command.inqCameraBacklightStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_BACKLIGHT, onComplete, IsOnParser); 642 | Command.inqCameraWideDStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_WIDE_D, onComplete); 643 | Command.inqCameraWideD = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_WIDE_D_SET, onComplete); 644 | Command.inqCameraAperture = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_APERTURE_DIRECT, onComplete, v2iParser); 645 | Command.inqCameraHighResStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_HIRES_ENABLE, onComplete, IsOnParser); 646 | Command.inqCameraNoiseReductionStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_NOISE_REDUCTION, onComplete); 647 | Command.inqCameraHighSensitivityStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_HIGH_SENSITIVITY, onComplete, IsOnParser); 648 | Command.inqCameraFreezeStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_FREEZE, onComplete, IsOnParser); 649 | Command.inqCameraEffect = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EFFECT, onComplete); 650 | Command.inqCameraEffectDigital = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EFFECT_DIGITAL, onComplete); 651 | Command.inqCameraEffectDigitalLevel = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_EFFECT_LEVEL, onComplete); 652 | Command.inqCameraID = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_ID_WRITE, onComplete, v2iParser); 653 | Command.inqCameraChromaSuppressStatus = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_CHROMA_SUPPRESS, onComplete); 654 | Command.inqCameraColorGain = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_COLOR_GAIN, onComplete, v2iParser); 655 | Command.inqCameraColorHue = (recipient = -1, onComplete) => Command.inqCamera(recipient, C.CAM_COLOR_HUE, onComplete, v2iParser); 656 | // these use op commands 657 | Command.inqVideoSystemNow = (recipient = -1, onComplete) => Command.inqOp(recipient, C.OP_VIDEO_FORMAT_I_NOW, onComplete, VideoSystemParser); 658 | Command.inqVideoSystemNext = (recipient = -1, onComplete) => Command.inqOp(recipient, C.OP_VIDEO_FORMAT_I_NEXT, onComplete, VideoSystemParser); 659 | Command.inqCameraPanSpeed = (recipient = -1, onComplete) => Command.inqOp(recipient, C.OP_PAN_MAX_SPEED, onComplete, PTMaxSpeedParser); 660 | Command.inqCameraPanPos = (recipient = -1, onComplete) => Command.inqOp(recipient, C.OP_PAN_POS, onComplete, PTPosParser); 661 | Command.inqCameraPanStatus = (recipient = -1, onComplete) => Command.inqOp(recipient, C.OP_PAN_STATUS, onComplete, PTStatusParser); 662 | // block inquiry commands 663 | Command.inqCameraLens = (recipient = -1, onComplete) => { let c = Command.raw(recipient, C.CAM_LENS_INQUIRY); c.dataParser = CamLensDataParser; c.onComplete = onComplete; return c; }; 664 | Command.inqCameraImage = (recipient = -1, onComplete) => { let c = Command.raw(recipient, C.CAM_IMAGE_INQUIRY); c.dataParser = CamImageDataParser; c.onComplete = onComplete; return c; }; 665 | // Parsers 666 | class NoParser { 667 | } 668 | NoParser.parse = (data) => data; 669 | class v2iParser { 670 | } 671 | v2iParser.parse = (data) => v2i(data); 672 | class v2siParser { 673 | } 674 | v2siParser.parse = (data) => v2si(data); 675 | class IsOnParser { 676 | } 677 | IsOnParser.parse = (data) => data == C.DATA_ONVAL; 678 | class AFIntervalParser { 679 | } 680 | AFIntervalParser.parse = (data) => Object.freeze({ 681 | movementTime: v2i(data.slice(0, 2)), 682 | intervalTime: v2i(data.slice(2, 4)), 683 | }); 684 | class PTMaxSpeedParser { 685 | } 686 | PTMaxSpeedParser.parse = (data) => Object.freeze({ panSpeed: data[0], tiltSpeed: data[1] }); 687 | class PTPosParser { 688 | } 689 | PTPosParser.parse = (data) => Object.freeze({ panPos: v2si(data.slice(0, 4)), tiltPos: v2si(data.slice(4, 8)) }); 690 | class PTStatusParser { 691 | } 692 | PTStatusParser.parse = (data) => new PTStatus(data); 693 | class PTStatus { 694 | constructor(data) { 695 | let [p, q, r, s] = nibbles(data); 696 | this.moveStatus = (q & C.PAN_MOVE_FAIL) >> 2; 697 | this.initStatus = (p & C.PAN_INIT_FAIL); 698 | this.atMaxL = (s & C.PAN_MAXL) > 0; 699 | this.atMaxR = (s & C.PAN_MAXR) > 0; 700 | this.atMaxU = (s & C.PAN_MAXU) > 0; 701 | this.atMaxD = (s & C.PAN_MAXD) > 0; 702 | this.moving = this.moveStatus == 1; 703 | this.moveDone = this.moveStatus == 2; 704 | this.moveFail = this.moveStatus == 3; 705 | this.initializing = this.initStatus == 1; 706 | this.ready = this.initStatus == 2; 707 | this.fail = this.initStatus == 3; 708 | } 709 | } 710 | class CamLensDataParser { 711 | } 712 | CamLensDataParser.parse = (data) => new CamLensData(data); 713 | class CamLensData { 714 | constructor(data) { 715 | this.zoomPos = v2i(data.slice(0, 4)); 716 | this.focusNearLimit = v2i(data.slice(4, 6)); 717 | this.focusPos = v2i(data.slice(6, 10)); 718 | // no data is in byte 10 719 | let ww = data[11]; 720 | // 0-normal, 1-interval, 2-trigger 721 | this.autoFocusMode = (ww & 0b11000) >> 3; 722 | // 0-slow, 1-normal 723 | this.autoFocusSensitivity = (ww & 0b100) >> 2; 724 | this.digitalZoomEnabled = testBit(ww, 0b10); 725 | this.autoFocusEnabled = testBit(ww, 0b1); 726 | let vv = data[12]; 727 | this.lowContrast = testBit(vv, 0b1000); 728 | this.loadingPreset = testBit(vv, 0b100); 729 | this.focusing = testBit(vv, 0b10); 730 | this.zooming = testBit(vv, 0b1); 731 | } 732 | } 733 | class CamImageDataParser { 734 | } 735 | CamImageDataParser.parse = (data) => new CamImageData(data); 736 | class CamImageData { 737 | constructor(data) { 738 | this.gainr = v2i(data.slice(0, 2)); 739 | this.gainb = v2i(data.slice(2, 4)); 740 | this.wbMode = data[4]; 741 | this.gain = data[5]; 742 | this.exposureMode = data[6]; 743 | this.shutterPos = data[8]; 744 | this.irisPos = data[9]; 745 | this.gainPos = data[10]; 746 | this.brightness = data[11]; 747 | this.exposure = data[12]; 748 | let aa = data[7]; 749 | this.highResEnabled = testBit(aa, 0b100000); 750 | this.wideDEnabled = testBit(aa, 0b10000); 751 | this.backlightCompEnabled = testBit(aa, 0b100); 752 | this.exposureCompEnabled = testBit(aa, 0b10); 753 | this.slowShutterAutoEnabled = testBit(aa, 0b1); 754 | } 755 | } 756 | // not implemented yet because this Video System codes are camera 757 | // specific. We would need to implement a parser for every different 758 | // camera individually. 759 | class VideoSystemParser { 760 | } 761 | VideoSystemParser.parse = (data) => data; 762 | class VideoSystemMode { 763 | constructor(data) { 764 | } 765 | } 766 | // HELPER FUNCTIONS 767 | testBit = (val, mask) => (val & mask) == mask; 768 | function nibbles(data) { 769 | let result = []; 770 | for (let d of data) { 771 | let pq = d; 772 | let p = pq >> 4; 773 | let q = pq & 0b1111; 774 | result.push(p); 775 | result.push(q); 776 | } 777 | return result; 778 | } 779 | function si2v(value) { 780 | // first, handle the possibility of signed integer values 781 | if (value > 32767) 782 | value = 32767; 783 | if (value < -32768) 784 | value = -32768; 785 | if (value < 0) 786 | value = 0xffff + value + 1; // this is the magic 787 | return i2v(value); 788 | } 789 | // data must be a buffer or array 790 | function v2si(data) { 791 | if (data.length == 2) 792 | data = [0, 0, ...data]; 793 | let value = v2i(data); 794 | if (value > 32767) 795 | value = value - 0xffff - 1; 796 | return value; 797 | } 798 | function i2v(value) { 799 | // return word as dword in visca format 800 | // packets are not allowed to be 0xff 801 | // so for numbers the first nibble is 0b0000 802 | // and 0xfd gets encoded into 0x0f 0x0d 803 | let ms = (value & 0b1111111100000000) >> 8; 804 | let ls = value & 0b0000000011111111; 805 | let p = (ms & 0b11110000) >> 4; 806 | let r = (ls & 0b11110000) >> 4; 807 | let q = ms & 0b1111; 808 | let s = ls & 0b1111; 809 | return Buffer.from([p, q, r, s]); 810 | } 811 | // value must be a buffer or array 812 | function v2i(data) { 813 | if (data.length == 2) 814 | data = [0, 0, ...data]; 815 | let [p, q, r, s] = data; 816 | let ls = (r << 4) | (s & 0b1111); 817 | let ms = (p << 4) | (q & 0b1111); 818 | return (ms << 8) | ls; 819 | } 820 | function takeClosest(myList, myNumber) { 821 | /// Assumes myList is sorted. Returns closest value to myNumber. 822 | /// If two numbers are equally close, return the smallest number. 823 | let pos = 0; 824 | for (var i = 0; i < myList.length; i++) { 825 | if (myNumber < myList[i]) 826 | break; 827 | else 828 | pos = i; 829 | } 830 | if (pos == 0) 831 | return myList[0]; 832 | if (pos == myList.length) 833 | return myList[-1]; 834 | before = myList[pos - 1]; 835 | after = myList[pos]; 836 | if (after - myNumber < myNumber - before) 837 | return after; 838 | else 839 | return before; 840 | } 841 | module.exports = { Command, PTStatus, CamLensData, CamImageData }; 842 | -------------------------------------------------------------------------------- /dist/visca/constants.js: -------------------------------------------------------------------------------- 1 | class Constants { 2 | } 3 | // masks for header components 4 | Constants.HEADERMASK_SOURCE = 0b01110000; 5 | Constants.HEADERMASK_RECIPIENT = 0b00000111; 6 | Constants.HEADERMASK_BROADCAST = 0b00001000; 7 | // controller message categories (QQ from the spec) 8 | Constants.MSGTYPE_COMMAND = 0x01; 9 | Constants.MSGTYPE_IF_CLEAR = 0x01; 10 | Constants.MSGTYPE_INQUIRY = 0x09; 11 | Constants.MSGTYPE_CANCEL = 0x20; // low nibble identifies the command buffer to cancel 12 | Constants.MSGTYPE_ADDRESS_SET = 0x30; // goes through all devices and then back to controller 13 | // camera message types (QQ from the spec) 14 | Constants.MSGTYPE_NETCHANGE = 0x38; 15 | Constants.MSGTYPE_ACK = 0x40; // low nibble identifies the command buffer holding command 16 | Constants.MSGTYPE_COMPLETE = 0x50; // low nibble identifies the command buffer that completed 17 | Constants.MSGTYPE_ERROR = 0x60; // low nibble identifies the command buffer for the error 18 | // data types (RR from the spec) 19 | Constants.DATATYPE_INTERFACE = 0x00; 20 | Constants.DATATYPE_CAMERA = 0x04; 21 | Constants.DATATYPE_OPERATION = 0x06; // sometimes referred to as pan/tilt, but also does system 22 | // camera settings codes // data (pqrs is in i2v format) 23 | Constants.CAM_POWER = 0x00; // (can inquire) 0x02, 0x03 24 | Constants.CAM_ICR = 0x01; // (can inquire) ON/OFF / infrared mode 25 | Constants.CAM_AUTO_ICR = 0x51; // (can inquire) ON/OFF / Auto dark-field mode 26 | Constants.CAM_AUTO_ICR_THRESHOLD = 0x21; // (can inquire) 00 00 0p 0q / threshold level 27 | Constants.CAM_GAIN = 0x0C; // 00, 02, 03 / reset, up, down 28 | Constants.CAM_GAIN_LIMIT = 0x2C; // (can inquire) 0p / range from 4-F 29 | Constants.CAM_GAIN_DIRECT = 0x4C; // (can inquire) 00 00 0p 0q / gain position 30 | Constants.CAM_RGAIN = 0x03; // reset 00, up 02, down 03 31 | Constants.CAM_RGAIN_DIRECT = 0X43; // (can inquire) direct 00 00 0p 0q 32 | Constants.CAM_BGAIN = 0x04; // reset 00, up 02, down 03 33 | Constants.CAM_BGAIN_DIRECT = 0X44; // (can inquire) direct 00 00 0p 0q 34 | Constants.CAM_ZOOM = 0x07; // 0x00 (stop), T/W 0x02, 0x03, 0x2p, 0x3p (variable) 35 | Constants.CAM_DZOOM = 0x06; // (can inquire) 0x02, 0x03 36 | Constants.CAM_ZOOM_DIRECT = 0x47; // (can inquire) pqrs: zoom value, optional tuvw: focus value 37 | Constants.CAM_FOCUS = 0x08; // data settings just like zoom 38 | Constants.CAM_FOCUS_IR_CORRECTION = 0x11; // (can inquire) 0x00, 0x01 39 | Constants.CAM_FOCUS_TRIGGER = 0x18; // when followed by 0x01 40 | Constants.CAM_FOCUS_INFINITY = 0x18; // when followed by 0x02 41 | Constants.CAM_FOCUS_NEAR_LIMIT_POS = 0x28; // (can inquire) pqrs (i2v) 42 | Constants.CAM_FOCUS_AUTO = 0x38; // (can inquire) ON/OFF/TOGGLE / toggle means activate on trigger 43 | Constants.CAM_FOCUS_DIRECT = 0x48; // (can inquire) pqrs (i2v) 44 | Constants.CAM_FOCUS_AF_MODE = 0x57; // (can inquire) 0x00, 0x01, 0x02 (on movement, interval, on zoom) 45 | Constants.CAM_FOCUS_AF_INTERVAL = 0x27; // (can inquire) pq: Movement time, rs: Interval time 46 | Constants.CAM_FOCUS_SENSE_HIGH = 0x58; // (can inquire) 0x02, 0x03 47 | Constants.CAM_WB_MODE = 0x35; // (can inquire) 0-5 auto, indoor, outdoor, one-push, manual 48 | Constants.CAM_WB_TRIGGER = 0x10; // when followed by 0x05 49 | Constants.CAM_EXPOSURE_MODE = 0x39; // (can inquire) 00, 03, 0A, 0B, 0D / auto, manual, shutter, iris, bright 50 | Constants.CAM_SHUTTER_SLOW_AUTO = 0x5A; // (can inquire) 0x02, 0x03 / auto, manual 51 | Constants.CAM_SHUTTER = 0x0A; // 00, 02, 03 / reset, up, down 52 | Constants.CAM_SHUTTER_DIRECT = 0x4A; // (can inquire) 00 00 0p 0q 53 | Constants.CAM_IRIS = 0x0B; // 00, 02, 03 / reset, up, down 54 | Constants.CAM_IRIS_DIRECT = 0x4B; // (can inquire) 00 00 0p 0q 55 | Constants.CAM_BRIGHT = 0x0D; // 00, 02, 03 / reset, up, down 56 | Constants.CAM_BRIGHT_DIRECT = 0x4D; // (can inquire) 00 00 0p 0q 57 | Constants.CAM_EXP_COMP = 0x0E; // 00, 02, 03 / reset, up, down 58 | Constants.CAM_EXP_COMP_ENABLE = 0x3E; // (can inquire) ON/OFF 59 | Constants.CAM_EXP_COMP_DIRECT = 0x4E; // (can inquire) 00 00 0p 0q 60 | Constants.CAM_BACKLIGHT = 0x33; // (can inquire) ON/OFF 61 | Constants.CAM_WIDE_D = 0x3D; // (can inquire) 0-4 / auto, on(ratio), on, off, on(hist) 62 | Constants.CAM_WIDE_D_REFRESH = 0x10; // when followed by 0x0D 63 | Constants.CAM_WIDE_D_SET = 0x2D; // (can inquire) 0p 0q 0r 0s 0t 0u 00 00 64 | // p: Screen display (0: Combined image, 2: Long-time, 3: Short-time) 65 | // q: Detection sensitivity (0: L 1: M 2: H) 66 | // r: Blocked-up shadow correction level (0: L 1: M 2: H 3: S) 67 | // s: Blown-out highlight correction level (0: L 1: M 2: H) 68 | // tu: Exposure ratio of short exposure (x1 to x64) 69 | Constants.CAM_APERTURE = 0x02; // 00, 02, 03 / reset, up, down 70 | Constants.CAM_APERTURE_DIRECT = 0x42; // (can inquire) 00 00 0p 0q / aperture gain 71 | Constants.CAM_HIRES_ENABLE = 0x52; // (can inquire) ON/OFF 72 | Constants.CAM_NOISE_REDUCTION = 0x53; // (can inquire) 0p / 0-5 73 | Constants.CAM_GAMMA = 0x5B; // (can inquire) 0p / 0: standard, 1-4 74 | Constants.CAM_HIGH_SENSITIVITY = 0x5E; // (can inquire) ON/OFF 75 | Constants.CAM_FREEZE = 0x62; // (can inquire) see data constants 76 | Constants.CAM_EFFECT = 0x63; // (can inquire) see data constants 77 | Constants.CAM_EFFECT_DIGITAL = 0x64; // (can inquire) see data constants 78 | Constants.CAM_EFFECT_LEVEL = 0x65; // intensity of digital effect 79 | Constants.CAM_MEMORY = 0x3F; // 0a 0p / a: 0-reset, 1-set, 2-recall, p: memory bank 0-5 80 | Constants.CAM_ID_WRITE = 0x22; // (can inquire) pqrs: give the camera an id from 0000-FFFF 81 | Constants.CAM_CHROMA_SUPPRESS = 0x5F; // (can inquire) 0-3 / Chroma Suppression level off, 1, 2, 3 82 | Constants.CAM_COLOR_GAIN = 0x49; // (can inquire) 00 00 00 0p / 0-E 83 | Constants.CAM_COLOR_HUE = 0x4F; // (can inquire) 00 00 00 0p / 0-E 84 | // operational settings 85 | Constants.OP_MENU_SCREEN = 0x06; // ON/OFF 86 | Constants.OP_VIDEO_FORMAT = 0x35; // 00 0p 87 | Constants.OP_VIDEO_FORMAT_I_NOW = 0x23; // 0p / inquire only, returns current value 88 | Constants.OP_VIDEO_FORMAT_I_NEXT = 0x33; // 0p / inquire only, returns value for next power on 89 | // These codes are camera specific. Sony camera codes are as follows here 90 | // p: 91 | // 0 = 1080i59.94, 1 = 1080p29.97, 2 = 720p59.94, 3 = 720p29.97, 4 = NTSC (not all cameras) 92 | // 8 = 1080i50, 9 = 720p50, A = 720p25, B = 1080i50, C = PAL (some cameras) 93 | // (I wonder if the manual intended to say B = 1080p50 ?) 94 | // video system changes require a power cycle 95 | Constants.OP_PAN_DRIVE = 0x01; // VV WW 0p 0q 96 | Constants.OP_PAN_ABSOLUTE = 0x02; // VV WW 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z 97 | Constants.OP_PAN_RELATIVE = 0X03; // VV WW 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z 98 | Constants.OP_PAN_MAX_SPEED = 0x11; // (inquire only) VV WW 99 | Constants.OP_PAN_POS = 0x12; // (inquire only) 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z 100 | // VV: pan speed 101 | // WW: tilt speed 102 | // p: pan move 1-left, 2-right, 3-none 103 | // q: tilt move 1-up, 2-down, 3-none 104 | // YYYY: pan 4 bit signed value from E1E5 - 1E1B 105 | // ZZZZ: tilt 4 bit signed from FC75 to 0FF0 (flip off) or F010 to 038B (flip on) 106 | Constants.OP_PAN_HOME = 0x04; // no data 107 | Constants.OP_PAN_RESET = 0x05; // no data 108 | Constants.OP_PAN_LIMIT = 0x07; 109 | // W: 1 addresses the up-right limit, 0 addresses the down-left limit 110 | // to clear: 01 0W 07 0F 0F 0F 07 0F 0F 0F 111 | // to set: 00 0W 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z 112 | Constants.OP_PAN_STATUS = 0x10; // (inquire only) pq rs, see below 113 | Constants.OP_IR_RECEIVE = 0x08; // (can inquire) ON/OFF/TOGGLE 114 | // special system commands (still need header and terminator) 115 | Constants.OP_IR_RETURN_ON = [0x01, 0x7D, 0x01, 0x03, 0x00, 0x00]; // returns IR commands over VISCA? 116 | Constants.OP_IR_RETURN_OFF = [0x01, 0x7D, 0x01, 0x13, 0x00, 0x00]; 117 | Constants.OP_INFO_DISPLAY_ON = [0x01, 0x7E, 0x01, 0x18, 0x02]; 118 | Constants.OP_INFO_DISPLAY_OFF = [0x01, 0x7E, 0x01, 0x18, 0x03]; 119 | // special inquiry commands 120 | Constants.OP_IR_CONDITION = [0x09, 0x06, 0x34]; // 0-stable, 1-unstable, 2-inactive 121 | Constants.OP_FAN_CONDITION = [0x09, 0x7e, 0x01, 0x38]; // 0-on, 1-off 122 | Constants.OP_INFORMATION_DISPLAY_STATUS = [0x09, 0x7e, 0x01, 0x18]; // ON/OFF 123 | Constants.OP_VERSION_INQUIRY = [0x09, 0x00, 0x02]; // returns 00 01 mn pq rs tu vw 124 | //mnq: model code 125 | //rstu: rom version 126 | //vw: socket number 127 | // block inquiries 128 | Constants.CAM_LENS_INQUIRY = [0x09, 0x7E, 0x7E, 0x00]; 129 | // 0w 0w 0w 0w 0v 0v 0y 0y 0y 0y 00 WW VV 130 | // w: zoom position 131 | // v: focus near limit 132 | // y: focus position 133 | // WW: 134 | // bit 0 indicates autofocus status, 135 | // bit 1 indicates digital zoom status 136 | // bit 2 indicates AF sensitivity / 0-slow 1-normal 137 | // bits 3-4 indicate AF mode / 0-normal, 1-interval, 2-zoom trigger 138 | // VV: 139 | // bit 0 indicates zooming status / 0-stopped, 1-executing 140 | // bit 1 indicates focusing status 141 | // bit 2 indicates camera memory recall status / 0-stopped, 1-executing 142 | // bit 3 indicates low contrast detection 143 | Constants.CAM_IMAGE_INQUIRY = [0x09, 0x7E, 0x7E, 0x01]; 144 | // 0w 0w 0v 0v 0a 0b 0c AA BB CC DD EE FF 145 | // w: R gain 146 | // v: B gain 147 | // a: WB mode 148 | // b: aperture gain 149 | // c: exposure mode 150 | // AA: 151 | // bit 0 slow shutter / 1-auto, 0-manual 152 | // bit 1 exposure comp 153 | // bit 2 backlight 154 | // bit 3 unused 155 | // bit 4 wide D / 0-off, 1-other 156 | // bit 5 High Res 157 | // BB: shutter position 158 | // CC: iris position 159 | // DD: gain position 160 | // EE: brightness 161 | // FF: exposure 162 | // data constants 163 | Constants.DATA_ONVAL = 0x02; 164 | Constants.DATA_OFFVAL = 0x03; 165 | Constants.DATA_ZOOMIN = 0x02; 166 | Constants.DATA_ZOOMOUT = 0x03; 167 | Constants.DATA_FOCUSFAR = 0x02; 168 | Constants.DATA_FOCUSNEAR = 0x03; 169 | Constants.DATA_TOGGLEVAL = 0x10; 170 | // basic effects 171 | Constants.DATA_EFFECT_OFF = 0x00; 172 | Constants.DATA_EFFECT_PASTEL = 0x01; 173 | Constants.DATA_EFFECT_NEGATIVE = 0x02; 174 | Constants.DATA_EFFECT_SEPIA = 0x03; 175 | Constants.DATA_EFFECT_BW = 0x04; 176 | Constants.DATA_EFFECT_SOLAR = 0x05; 177 | Constants.DATA_EFFECT_MOSAIC = 0x06; 178 | Constants.DATA_EFFECT_SLIM = 0x07; 179 | Constants.DATA_EFFECT_STRETCH = 0x08; 180 | // digital effects 181 | Constants.DATA_EFFECT_STILL = 0x01; 182 | Constants.DATA_EFFECT_FLASH = 0x02; 183 | Constants.DATA_EFFECT_LUMI = 0x03; 184 | Constants.DATA_EFFECT_TRAIL = 0x04; 185 | Constants.DATA_PANLEFT = 0x01; 186 | Constants.DATA_TILTUP = 0x01; 187 | Constants.DATA_PANRIGHT = 0x02; 188 | Constants.DATA_TILTDOWN = 0x02; 189 | Constants.DATA_PANSTOP = 0x00; 190 | Constants.DATA_TILTSTOP = 0x00; 191 | Constants.DATA_PANTILT_UR = 0x01; 192 | Constants.DATA_PANTILT_DL = 0x00; 193 | // pan status data masks 194 | Constants.PAN_MAXL = 0b0001; // apply to s 195 | Constants.PAN_MAXR = 0b0010; // apply to s 196 | Constants.PAN_MAXU = 0b0100; // apply to s 197 | Constants.PAN_MAXD = 0b1000; // apply to s 198 | Constants.PAN_PAN_UNK = 0b0001; // apply to r 199 | Constants.PAN_TILT_UNK = 0b0001; // apply to q 200 | Constants.PAN_MOVING = 0b0100; // apply to q 201 | Constants.PAN_MOVE_DONE = 0b1000; // apply to q 202 | Constants.PAN_MOVE_FAIL = 0b1100; // apply to q 203 | Constants.PAN_NR = 0b0000; // apply to p 204 | Constants.PAN_INIT = 0b0001; // apply to p 205 | Constants.PAN_READY = 0b0010; // apply to p 206 | Constants.PAN_INIT_FAIL = 0b0011; // apply to p 207 | // error codes 208 | Constants.ERROR_SYNTAX = 0x02; 209 | Constants.ERROR_BUFFER_FULL = 0x03; 210 | Constants.ERROR_CANCELLED = 0x04; 211 | Constants.ERROR_INVALID_BUFFER = 0x05; 212 | Constants.ERROR_COMMAND_FAILED = 0x41; 213 | // Zoom and Focus Settings 214 | Constants.FOCUS_NEAR_LIMIT_SONY_SETTINGS = [ 215 | 0x1000, 216 | 0x2000, 217 | 0x3000, 218 | 0x4000, 219 | 0x5000, 220 | 0x6000, 221 | 0x7000, 222 | 0x8000, 223 | 0x9000, 224 | 0xA000, 225 | 0xB000, 226 | 0xC000, 227 | 0xD000, 228 | 0xE000, 229 | 0xF000, 230 | ]; 231 | Constants.OPTICAL_ZOOM_PRESETS = [ 232 | 0x0000, 233 | 0x16A1, 234 | 0x2063, 235 | 0x2628, 236 | 0x2A1D, 237 | 0x2D13, 238 | 0x2F6D, 239 | 0x3161, 240 | 0x330D, 241 | 0x3486, 242 | 0x35D7, 243 | 0x3709, 244 | 0x3820, 245 | 0x3920, 246 | 0x3A0A, 247 | 0x3ADD, 248 | 0x3B9C, 249 | 0x3C46, 250 | 0x3CDC, 251 | 0x3D60, 252 | 0x3DD4, 253 | 0x3E39, 254 | 0x3E90, 255 | 0x3EDC, 256 | 0x3F1E, 257 | 0x3F57, 258 | 0x3F8A, 259 | 0x3FB6, 260 | 0x3FDC, 261 | 0x4000, 262 | ]; 263 | Constants.DIGITAL_ZOOM_PRESETS = [ 264 | 0x4000, 265 | 0x6000, 266 | 0x6A80, 267 | 0x7000, 268 | 0x7300, 269 | 0x7540, 270 | 0x76C0, 271 | 0x7800, 272 | 0x78C0, 273 | 0x7980, 274 | 0x7A00, 275 | 0x7AC0, 276 | ]; 277 | module.exports = { Constants }; 278 | -------------------------------------------------------------------------------- /dist/visca/controller.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events'); 2 | const { Constants: C } = require('./constants'); 3 | const { Command } = require("./command"); 4 | const { SerialTransport } = require('./visca-serial'); 5 | const { UDPTransport } = require("./visca-ip"); 6 | // the controller keeps track of the cameras connected by serial 7 | // it also communicates with cameras over IP 8 | // and it exposes a UDP server for each serially connected camera 9 | class ViscaController extends EventEmitter { 10 | constructor() { 11 | this.ipClients = []; 12 | this.serialBroadcastCommands = []; // FIFO stack of serial commands sent 13 | this.init(); 14 | } 15 | init() { 16 | this.cameras = {}; // will be indexed with uuid strings for ip cameras 17 | this.cameraCount = 0; 18 | } 19 | // uuid will be specified when the data comes from an IP camera 20 | addIPCamera(host, port) { 21 | let transport = new UDPTransport(host, port); 22 | transport.on('data', this.onUDPData); 23 | let camera = new Camera(1, transport); // IP cameras all have index 1 24 | cameras[transport.uuid] = camera; 25 | camera.sendCommand(Command.cmdInterfaceClearAll(1)); 26 | camera.inquireAll(); 27 | } 28 | // manage the serial transport 29 | restartSerial() { this.close(); this.init(); this.start(); } 30 | closeSerial() { this.serialConnection.close(); } 31 | startSerial(portname = "/dev/ttyUSB0", timeout = 1, baudRate = 9600, debug = false) { 32 | this.serialConnection = SerialTransport(portname, timeout, baudRate, debug); 33 | this.serialConnection.start(); 34 | // create callbacks 35 | this.serialConnection.on('open', this.onSerialOpen); 36 | this.serialConnection.on('close', this.onSerialClose); 37 | this.serialConnection.on('error', this.onSerialError); 38 | this.serialConnection.on('data', this.onSerialData); 39 | // send enumeration command (on reply, we will send the IF clear command) 40 | this.enumerateSerial(); 41 | } 42 | onSerialOpen() { } 43 | onSerialClose() { } 44 | onSerialError(e) { console.log(e); } 45 | onSerialData(viscaCommand) { 46 | let v = viscaCommand; 47 | // make sure we have this camera as an object if it came from a camera 48 | // but leave the camera null if it was a broadcast command 49 | let camera = null; 50 | if (v.source != 0) { 51 | if (!(v.source in this.cameras)) { 52 | camera = new Camera(v.source, this.serialConnection); 53 | camera.uuid = v.source; 54 | this.cameras[v.source] = camera; 55 | } 56 | else { 57 | camera = this.cameras[v.source]; 58 | } 59 | } 60 | if (camera != null) { 61 | return this.onCameraData(camera, v); 62 | } 63 | // the following commands are 'passthrough' commands that 64 | // go through the whole serial chain as broadcast commands 65 | switch (v.msgType) { 66 | case C.MSGTYPE_IF_CLEAR: 67 | // reset data for all serial port cameras 68 | for (let cam of Object.values(this.cameras)) { 69 | if (cam.uuid == cam.index) 70 | cam.clear(); 71 | } 72 | this.inquireAllSerial(); 73 | break; 74 | // address set message, reset all serial port cameras 75 | case C.MSGTYPE_ADDRESS_SET: 76 | let highestIndex = v.data[0] - 1; 77 | for (let i = 1; i <= highestIndex; i++) 78 | this.cameras[i] = new Camera(i, this.serialConnection); 79 | for (let i = highestIndex + 1; i < 8; i++) 80 | delete (this.cameras[i]); 81 | this.ifClearAllSerial(); 82 | break; 83 | default: 84 | break; 85 | } 86 | this.emit('update'); 87 | } 88 | onUDPData({ uuid, viscaCommand }) { 89 | let camera = cameras[uuid]; 90 | return this.onCameraData(camera, viscaCommand); 91 | } 92 | onCameraData(camera, v) { 93 | switch (v.msgType) { 94 | case C.MSGTYPE_IF_CLEAR: 95 | camera.clear(); 96 | break; 97 | // network change messages are unprompted 98 | case C.MSGTYPE_NETCHANGE: 99 | // a camera issues this when it detects a change on the serial line, 100 | // and if we get it, we should re-assign all serial port cameras. 101 | this.enumerateSerial(); 102 | break; 103 | // ack message, one of our commands was accepted and put in a buffer 104 | case C.MSGTYPE_ACK: 105 | camera.ack(v); 106 | return; 107 | // completion message 108 | case C.MSGTYPE_COMPLETE: 109 | camera.complete(v); 110 | break; 111 | // error messages 112 | case C.MSGTYPE_ERROR: 113 | camera.error(v); 114 | break; 115 | default: 116 | break; 117 | } 118 | this.emit('update'); 119 | } 120 | sendSerial(viscaCommand) { 121 | this.serialConnection.send(viscaCommand); 122 | } 123 | // forces a command to be a broadcast command (only applies to serial) 124 | broadcastSerial(viscaCommand) { 125 | viscaCommand.broadcast = true; 126 | this.serialConnection.send(viscaCommand); 127 | } 128 | // forces a command to go to a specific camera 129 | sendToCamera(camera, viscaCommand) { 130 | camera.sendCommand(viscaCommand); 131 | } 132 | // system-level commands... only relevant to serial connections 133 | enumerateSerial() { 134 | this.sendSerial(Command.addressSet()); 135 | } 136 | ifClearAllSerial() { 137 | this.sendSerial(Command.cmdInterfaceClearAll()); 138 | } 139 | // for each camera queue all the inquiry commands 140 | // to get a full set of camera status data 141 | inquireAllSerial() { 142 | for (let camera of cameras) { 143 | if (camera.transport == this.serialConnection) { 144 | camera.inquireAll(); 145 | } 146 | } 147 | } 148 | inquireAllIP() { 149 | for (let camera of cameras) { 150 | if (camera.transport.uuid) { 151 | camera.inquireAll(); 152 | } 153 | } 154 | } 155 | inquireAll() { this.inquireAllSerial(); this.inquireAllIP(); } 156 | // for debugging 157 | dump(packet, title = null) { 158 | if (!packet || packet.length == 0 || !this.DEBUG) 159 | return; 160 | header = packet[0]; 161 | term = packet[packet.length - 2]; // last item 162 | qq = packet[1]; 163 | sender = (header & 0b01110000) >> 4; 164 | broadcast = (header & 0b1000) >> 3; 165 | recipient = header & 0b0111; 166 | if (broadcast) 167 | recipient_s = "*"; 168 | else 169 | recipient_s = str(recipient); 170 | console.log("-----"); 171 | if (title) 172 | console.log(`packet (${title}) [${sender} => ${recipient_s}] len=${packet.length}: ${packet}`); 173 | else 174 | console.log(`packet [%d => %s] len=%d: %s` % (sender, recipient_s, packet.length, packet)); 175 | console.log(` QQ.........: ${qq}`); 176 | if (qq == 0x01) 177 | console.log(" (Command)"); 178 | if (qq == 0x09) 179 | console.log(" (Inquiry)"); 180 | if (packet.length > 3) { 181 | rr = packet[2]; 182 | console.log(` RR.........: ${rr}`); 183 | if (rr == 0x00) 184 | console.log(" (Interface)"); 185 | if (rr == 0x04) 186 | console.log(" (Camera [1])"); 187 | if (rr == 0x06) 188 | console.log(" (Pan/Tilter)"); 189 | } 190 | if (packet.length > 4) { 191 | data = packet.slice(3); 192 | console.log(` Data.......: ${data}`); 193 | } 194 | else 195 | console.log(" Data.......: null"); 196 | if (term !== 0xff) { 197 | console.log("ERROR: Packet not terminated correctly"); 198 | return; 199 | } 200 | if (packet.length == 3 && (qq & 0b11110000) >> 4 == 4) { 201 | socketno = qq & 0b1111; 202 | console.log(` packet: ACK for socket ${socketno}`); 203 | } 204 | if (packet.length == 3 && (qq & 0b11110000) >> 4 == 5) { 205 | socketno = qq & 0b1111; 206 | console.log(` packet: COMPLETION for socket ${socketno}`); 207 | } 208 | if (packet.length > 3 && (qq & 0b11110000) >> 4 == 5) { 209 | socketno = qq & 0b1111; 210 | ret = packet.slice(2); 211 | console.log(` packet: COMPLETION for socket ${socketno}, data=${ret}`); 212 | } 213 | if (packet.length == 4 && (qq & 0b11110000) >> 4 == 6) { 214 | console.log(" packet: ERROR!"); 215 | socketno = qq & 0b00001111; 216 | errcode = packet[2]; 217 | //these two are special, socket is zero && has no meaning: 218 | if (errcode == 0x02 && socketno == 0) 219 | console.log(" : Syntax Error"); 220 | if (errcode == 0x03 && socketno == 0) 221 | console.log(" : Command Buffer Full"); 222 | if (errcode == 0x04) 223 | console.log(` : Socket ${socketno}: Command canceled`); 224 | if (errcode == 0x05) 225 | console.log(` : Socket ${socketno}: Invalid socket selected`); 226 | if (errcode == 0x41) 227 | console.log(` : Socket ${socketno}: Command not executable`); 228 | } 229 | if (packet.length == 3 && qq == 0x38) 230 | console.log("Network Change - we should immediately issue a renumbering!"); 231 | } 232 | } 233 | module.exports = { ViscaController }; 234 | -------------------------------------------------------------------------------- /dist/visca/visca-ip.js: -------------------------------------------------------------------------------- 1 | const udp = require('dgram'); 2 | const { EventEmitter } = require('events'); 3 | const { v4: uuid } = require('uuid'); 4 | /* 5 | Creates a UDP server to receive VISCA over IP commands. 6 | 7 | This can be used to route IP commands to cameras connected by Serial 8 | and to send camera replies back to the proper UDP clients. 9 | 10 | The Visca Controller should create one Server for each physical camera we want 11 | to expose to network control. 12 | */ 13 | class Server extends EventEmitter { 14 | constructor(port = 50000) { 15 | // creating a udp server 16 | this.socket = udp.createSocket('udp4'); 17 | let socket = this.socket; 18 | // emits when any error occurs 19 | socket.on('error', function (error) { 20 | console.log('Error: ' + error); 21 | socket.close(); 22 | }); 23 | // emits on new datagram msg 24 | socket.on('message', function (msg, info) { 25 | console.log('Data received from client : ' + msg.toString()); 26 | console.log('Received %d bytes from %s:%d\n', msg.length, info.address, info.port); 27 | // emit message up the chain 28 | emit('message', msg); 29 | }); 30 | //emits when socket is ready and listening for datagram msgs 31 | socket.on('listening', function () { 32 | let address = socket.address(); 33 | let port = address.port; 34 | let family = address.family; 35 | let ipaddr = address.address; 36 | console.log('Server is listening at port' + port); 37 | console.log('Server ip :' + ipaddr); 38 | console.log('Server is IP4/IP6 : ' + family); 39 | }); 40 | //emits after the socket is closed using socket.close(); 41 | socket.on('close', function () { 42 | console.log('Socket is closed !'); 43 | }); 44 | socket.bind(port); 45 | } 46 | write(packet) { 47 | this.socket.send(packet); 48 | } 49 | } 50 | // simply implements a visca transport over a udp socket 51 | class UDPTransport extends EventEmitter { 52 | constructor(host = '', port = 50000) { 53 | this.debug = false; 54 | this.host = host; 55 | this.port = port; 56 | this.uuid = uuid(); 57 | // creating a client socket 58 | this.socket = udp.createSocket('udp4'); 59 | // handle replies 60 | socket.on('message', function (msg, info) { 61 | console.log('Received %d bytes from %s:%d\n', msg.length, info.address, info.port); 62 | this.onData(msg); 63 | }); 64 | } 65 | onData(packet) { 66 | console.log('Received: ', packet); 67 | if (this.debug) 68 | console.log('Received: ' + packet); 69 | let v = ViscaCommand.fromPacket(packet); 70 | this.emit('data', { uuid: this.uuid, viscaCommand: v }); 71 | } 72 | send(viscaCommand) { 73 | let packet = viscaCommand.toPacket(); 74 | if (this.debug) 75 | console.log('Sent: ' + packet); 76 | // sending packet 77 | this.socket.send(packet, this.port, this.host, function (error) { 78 | if (error) { 79 | client.close(); 80 | } 81 | else { 82 | console.log('Data sent !!!'); 83 | } 84 | }); 85 | } 86 | } 87 | module.exports = { Server, UDPTransport }; 88 | -------------------------------------------------------------------------------- /dist/visca/visca-serial.js: -------------------------------------------------------------------------------- 1 | const SerialPort = require("serialport"); 2 | const Delimiter = require('@serialport/parser-delimiter'); 3 | const { EventEmitter } = require('events'); 4 | // simply implements a visca transport over the serial interface 5 | class SerialTransport extends EventEmitter { 6 | constructor(portname = "/dev/ttyUSB0", timeout = 1, baudRate = 9600, debug = false) { 7 | this.started = false; 8 | this.debug = false; 9 | if (this.started) 10 | return; 11 | this.portname = portname; 12 | this.timeout = timeout; 13 | this.baudRate = baudRate; 14 | this.debug = debug; 15 | this.start(); 16 | } 17 | start() { 18 | if (this.started) 19 | return; 20 | // open the serial port 21 | try { 22 | this.serialport = new SerialPort(portname, { baudRate }); 23 | this.parser = this.serialport.pipe(new Delimiter({ delimiter: [0xff] })); 24 | this.serialport.on('open', this.onOpen); // provides error object 25 | this.serialport.on('close', this.onClose); // if disconnected, err.disconnected == true 26 | this.serialport.on('error', this.onError); // provides error object 27 | this.parser.on('data', this.onData); // provides a Buffer object 28 | } 29 | catch (e) { 30 | console.log(`Exception opening serial port '${this.portname}' for (display) ${e}\n`); 31 | } 32 | } 33 | restart() { this.close(); this.init(); this.start(); } 34 | close() { this.serialport.close(); this.started = false; } 35 | onOpen() { this.started = true; this.emit('open'); } 36 | onClose(e) { console.log(e); this.started = false; this.emit('close'); } 37 | onError(e) { console.log(e); this.started = false; this.emit('error', e); } 38 | onData(packet) { 39 | // the socket parser gives us only full visca packets 40 | // (terminated with 0xff) 41 | console.log('Received: ', packet); 42 | if (this.debug) 43 | console.log('Received: ' + packet); 44 | // convert to command packet object 45 | let v = ViscaCommand.fromPacket(packet); 46 | this.emit('data', v); 47 | } 48 | send(viscaCommand) { 49 | if (!this.serialport.isOpen) 50 | return; 51 | let packet = viscaCommand.toPacket(); 52 | this.serialport.write(packet); 53 | if (this.debug) 54 | console.log('Sent: ' + packet); 55 | } 56 | } 57 | module.exports = { SerialTransport }; 58 | -------------------------------------------------------------------------------- /dist/visca/visca.js: -------------------------------------------------------------------------------- 1 | // INSPIRED BY https://github.com/benelgiac/PyVisca3/blob/master/pyviscalib/visca.py 2 | // 3 | // For this JavaScript version, we eliminate all synchronous reads to the socket 4 | // in favor of using callbacks. 5 | const { ViscaController } = require('./controller'); 6 | module.exports = { ViscaController }; 7 | -------------------------------------------------------------------------------- /nearus-visca-protocol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmikels/ptz-server/bd8f155a0c98166e50e3f9ca374b0cb70089cc8c/nearus-visca-protocol.pdf -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ptz-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@serialport/binding-abstract": { 8 | "version": "9.0.2", 9 | "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.0.2.tgz", 10 | "integrity": "sha512-kyMX6usn+VLpidt0YsDq5JwztIan9TPCX6skr0XcalOxI8I7w+/2qVZJzjgo2fSqDnPRcU2jMWTytwzEXFODvQ==", 11 | "requires": { 12 | "debug": "^4.1.1" 13 | } 14 | }, 15 | "@serialport/binding-mock": { 16 | "version": "9.0.2", 17 | "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-9.0.2.tgz", 18 | "integrity": "sha512-HfrvJ/LXULHk8w63CGxwDNiDidFgDX8BnadY+cgVS6yHMHikbhLCLjCmUKsKBWaGKRqOznl0w+iUl7TMi1lkXQ==", 19 | "requires": { 20 | "@serialport/binding-abstract": "^9.0.2", 21 | "debug": "^4.1.1" 22 | } 23 | }, 24 | "@serialport/bindings": { 25 | "version": "9.0.3", 26 | "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-9.0.3.tgz", 27 | "integrity": "sha512-hnqVqEc4IqGCIjztGkd30V0KcTatQ1T/SS03MZ9KLn6e3y2PSXFqf0TqxB0qF7K9lGHWldMTiPOQaZnuV/oZLQ==", 28 | "requires": { 29 | "@serialport/binding-abstract": "^9.0.2", 30 | "@serialport/parser-readline": "^9.0.1", 31 | "bindings": "^1.5.0", 32 | "debug": "^4.3.1", 33 | "nan": "^2.14.2", 34 | "prebuild-install": "^6.0.0" 35 | } 36 | }, 37 | "@serialport/parser-byte-length": { 38 | "version": "9.0.1", 39 | "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.0.1.tgz", 40 | "integrity": "sha512-1Ikv4lgCNw8OMf35yCpgzjHwkpgBEkhBuXFXIdWZk+ixaHFLlAtp03QxGPZBmzHMK58WDmEQoBHC1V5BkkAKSQ==" 41 | }, 42 | "@serialport/parser-cctalk": { 43 | "version": "9.0.1", 44 | "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-9.0.1.tgz", 45 | "integrity": "sha512-GtMda2DeJ+23bNqOc79JYV06dax2n3FLLFM3zA7nfReCOi98QbuDj4TUbFESMOnp4DB0oMO0GYHCR9gHOedTkg==" 46 | }, 47 | "@serialport/parser-delimiter": { 48 | "version": "9.0.1", 49 | "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-9.0.1.tgz", 50 | "integrity": "sha512-+oaSl5zEu47OlrRiF5p5tn2qgGqYuhVcE+NI+Pv4E1xsNB/A0fFxxMv/8XUw466CRLEJ5IESIB9qbFvKE6ltaQ==" 51 | }, 52 | "@serialport/parser-readline": { 53 | "version": "9.0.1", 54 | "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.0.1.tgz", 55 | "integrity": "sha512-38058gxvyfgdeLpg3aUyD98NuWkVB9yyTLpcSdeQ3GYiupivwH6Tdy/SKPmxlHIw3Ml2qil5MR2mtW2fLPB5CQ==", 56 | "requires": { 57 | "@serialport/parser-delimiter": "^9.0.1" 58 | } 59 | }, 60 | "@serialport/parser-ready": { 61 | "version": "9.0.1", 62 | "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-9.0.1.tgz", 63 | "integrity": "sha512-lgzGkVJaaV1rJVx26WwI2UKyPxc0vu1rsOeldzA3VVbF+ABrblUQA06+cRPpT6k96GY+X4+1fB1rWuPpt8HbgQ==" 64 | }, 65 | "@serialport/parser-regex": { 66 | "version": "9.0.1", 67 | "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-9.0.1.tgz", 68 | "integrity": "sha512-BHTV+Lkl+J8hSecFtDRENaR4fgA6tw44J+dmA1vEKEyum0iDN4bihbu8yvztYyo4PhBGUKDfm/PnD5EkJm0dPA==" 69 | }, 70 | "@serialport/stream": { 71 | "version": "9.0.2", 72 | "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-9.0.2.tgz", 73 | "integrity": "sha512-0RkVe+gvwZu/PPfbb7ExQ+euGoCTGKD/B8TQ5fuhe+eKk1sh73RwjKmu9gp6veSNqx9Zljnh1dF6mhdEKWZpSA==", 74 | "requires": { 75 | "debug": "^4.1.1" 76 | } 77 | }, 78 | "@types/events": { 79 | "version": "3.0.0", 80 | "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", 81 | "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", 82 | "dev": true 83 | }, 84 | "@types/node": { 85 | "version": "14.14.20", 86 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", 87 | "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", 88 | "dev": true 89 | }, 90 | "@types/uuid": { 91 | "version": "8.3.0", 92 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", 93 | "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", 94 | "dev": true 95 | }, 96 | "@utopian/visca": { 97 | "version": "0.0.13", 98 | "resolved": "https://registry.npmjs.org/@utopian/visca/-/visca-0.0.13.tgz", 99 | "integrity": "sha512-avHiutgvHu/49rAavlyQK1SyWupMWFqGkzwGwGTKJctj/6qJFqLmqySFN5XXmwou+VTGpAzEIsmQ2+ICkTxmqQ==", 100 | "requires": { 101 | "node-hid": "^2.1.1", 102 | "serialport": "^9.0.3", 103 | "uuid": "^8.3.2" 104 | } 105 | }, 106 | "ansi-regex": { 107 | "version": "2.1.1", 108 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 109 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 110 | }, 111 | "aproba": { 112 | "version": "1.2.0", 113 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 114 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 115 | }, 116 | "are-we-there-yet": { 117 | "version": "1.1.5", 118 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 119 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 120 | "requires": { 121 | "delegates": "^1.0.0", 122 | "readable-stream": "^2.0.6" 123 | } 124 | }, 125 | "base64-js": { 126 | "version": "1.5.1", 127 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 128 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 129 | }, 130 | "bindings": { 131 | "version": "1.5.0", 132 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 133 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 134 | "requires": { 135 | "file-uri-to-path": "1.0.0" 136 | } 137 | }, 138 | "bl": { 139 | "version": "4.0.3", 140 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", 141 | "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", 142 | "requires": { 143 | "buffer": "^5.5.0", 144 | "inherits": "^2.0.4", 145 | "readable-stream": "^3.4.0" 146 | }, 147 | "dependencies": { 148 | "readable-stream": { 149 | "version": "3.6.0", 150 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 151 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 152 | "requires": { 153 | "inherits": "^2.0.3", 154 | "string_decoder": "^1.1.1", 155 | "util-deprecate": "^1.0.1" 156 | } 157 | } 158 | } 159 | }, 160 | "buffer": { 161 | "version": "5.7.1", 162 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 163 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 164 | "requires": { 165 | "base64-js": "^1.3.1", 166 | "ieee754": "^1.1.13" 167 | } 168 | }, 169 | "chownr": { 170 | "version": "1.1.4", 171 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 172 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 173 | }, 174 | "code-point-at": { 175 | "version": "1.1.0", 176 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 177 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 178 | }, 179 | "console-control-strings": { 180 | "version": "1.1.0", 181 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 182 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 183 | }, 184 | "core-util-is": { 185 | "version": "1.0.2", 186 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 187 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 188 | }, 189 | "debug": { 190 | "version": "4.3.1", 191 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 192 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 193 | "requires": { 194 | "ms": "2.1.2" 195 | } 196 | }, 197 | "decompress-response": { 198 | "version": "4.2.1", 199 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 200 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 201 | "requires": { 202 | "mimic-response": "^2.0.0" 203 | } 204 | }, 205 | "deep-extend": { 206 | "version": "0.6.0", 207 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 208 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 209 | }, 210 | "delegates": { 211 | "version": "1.0.0", 212 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 213 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 214 | }, 215 | "detect-libc": { 216 | "version": "1.0.3", 217 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 218 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 219 | }, 220 | "end-of-stream": { 221 | "version": "1.4.4", 222 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 223 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 224 | "requires": { 225 | "once": "^1.4.0" 226 | } 227 | }, 228 | "expand-template": { 229 | "version": "2.0.3", 230 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 231 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" 232 | }, 233 | "file-uri-to-path": { 234 | "version": "1.0.0", 235 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 236 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 237 | }, 238 | "fs-constants": { 239 | "version": "1.0.0", 240 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 241 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 242 | }, 243 | "gauge": { 244 | "version": "2.7.4", 245 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 246 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 247 | "requires": { 248 | "aproba": "^1.0.3", 249 | "console-control-strings": "^1.0.0", 250 | "has-unicode": "^2.0.0", 251 | "object-assign": "^4.1.0", 252 | "signal-exit": "^3.0.0", 253 | "string-width": "^1.0.1", 254 | "strip-ansi": "^3.0.1", 255 | "wide-align": "^1.1.0" 256 | } 257 | }, 258 | "github-from-package": { 259 | "version": "0.0.0", 260 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 261 | "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" 262 | }, 263 | "has-unicode": { 264 | "version": "2.0.1", 265 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 266 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 267 | }, 268 | "ieee754": { 269 | "version": "1.2.1", 270 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 271 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 272 | }, 273 | "inherits": { 274 | "version": "2.0.4", 275 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 276 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 277 | }, 278 | "ini": { 279 | "version": "1.3.8", 280 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 281 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 282 | }, 283 | "is-fullwidth-code-point": { 284 | "version": "1.0.0", 285 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 286 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 287 | "requires": { 288 | "number-is-nan": "^1.0.0" 289 | } 290 | }, 291 | "isarray": { 292 | "version": "1.0.0", 293 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 294 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 295 | }, 296 | "mimic-response": { 297 | "version": "2.1.0", 298 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 299 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 300 | }, 301 | "minimist": { 302 | "version": "1.2.5", 303 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 304 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 305 | }, 306 | "mkdirp-classic": { 307 | "version": "0.5.3", 308 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 309 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 310 | }, 311 | "ms": { 312 | "version": "2.1.2", 313 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 314 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 315 | }, 316 | "nan": { 317 | "version": "2.14.2", 318 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", 319 | "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" 320 | }, 321 | "napi-build-utils": { 322 | "version": "1.0.2", 323 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 324 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 325 | }, 326 | "node-abi": { 327 | "version": "2.19.3", 328 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", 329 | "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", 330 | "requires": { 331 | "semver": "^5.4.1" 332 | } 333 | }, 334 | "node-addon-api": { 335 | "version": "3.0.2", 336 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz", 337 | "integrity": "sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==" 338 | }, 339 | "node-hid": { 340 | "version": "2.1.1", 341 | "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-2.1.1.tgz", 342 | "integrity": "sha512-Skzhqow7hyLZU93eIPthM9yjot9lszg9xrKxESleEs05V2NcbUptZc5HFqzjOkSmL0sFlZFr3kmvaYebx06wrw==", 343 | "requires": { 344 | "bindings": "^1.5.0", 345 | "node-addon-api": "^3.0.2", 346 | "prebuild-install": "^6.0.0" 347 | } 348 | }, 349 | "noop-logger": { 350 | "version": "0.1.1", 351 | "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", 352 | "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" 353 | }, 354 | "npmlog": { 355 | "version": "4.1.2", 356 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 357 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 358 | "requires": { 359 | "are-we-there-yet": "~1.1.2", 360 | "console-control-strings": "~1.1.0", 361 | "gauge": "~2.7.3", 362 | "set-blocking": "~2.0.0" 363 | } 364 | }, 365 | "number-is-nan": { 366 | "version": "1.0.1", 367 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 368 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 369 | }, 370 | "object-assign": { 371 | "version": "4.1.1", 372 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 373 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 374 | }, 375 | "once": { 376 | "version": "1.4.0", 377 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 378 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 379 | "requires": { 380 | "wrappy": "1" 381 | } 382 | }, 383 | "prebuild-install": { 384 | "version": "6.0.0", 385 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.0.tgz", 386 | "integrity": "sha512-h2ZJ1PXHKWZpp1caLw0oX9sagVpL2YTk+ZwInQbQ3QqNd4J03O6MpFNmMTJlkfgPENWqe5kP0WjQLqz5OjLfsw==", 387 | "requires": { 388 | "detect-libc": "^1.0.3", 389 | "expand-template": "^2.0.3", 390 | "github-from-package": "0.0.0", 391 | "minimist": "^1.2.3", 392 | "mkdirp-classic": "^0.5.3", 393 | "napi-build-utils": "^1.0.1", 394 | "node-abi": "^2.7.0", 395 | "noop-logger": "^0.1.1", 396 | "npmlog": "^4.0.1", 397 | "pump": "^3.0.0", 398 | "rc": "^1.2.7", 399 | "simple-get": "^3.0.3", 400 | "tar-fs": "^2.0.0", 401 | "tunnel-agent": "^0.6.0", 402 | "which-pm-runs": "^1.0.0" 403 | } 404 | }, 405 | "process-nextick-args": { 406 | "version": "2.0.1", 407 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 408 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 409 | }, 410 | "pump": { 411 | "version": "3.0.0", 412 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 413 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 414 | "requires": { 415 | "end-of-stream": "^1.1.0", 416 | "once": "^1.3.1" 417 | } 418 | }, 419 | "rc": { 420 | "version": "1.2.8", 421 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 422 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 423 | "requires": { 424 | "deep-extend": "^0.6.0", 425 | "ini": "~1.3.0", 426 | "minimist": "^1.2.0", 427 | "strip-json-comments": "~2.0.1" 428 | } 429 | }, 430 | "readable-stream": { 431 | "version": "2.3.7", 432 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 433 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 434 | "requires": { 435 | "core-util-is": "~1.0.0", 436 | "inherits": "~2.0.3", 437 | "isarray": "~1.0.0", 438 | "process-nextick-args": "~2.0.0", 439 | "safe-buffer": "~5.1.1", 440 | "string_decoder": "~1.1.1", 441 | "util-deprecate": "~1.0.1" 442 | } 443 | }, 444 | "safe-buffer": { 445 | "version": "5.1.2", 446 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 447 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 448 | }, 449 | "semver": { 450 | "version": "5.7.1", 451 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 452 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 453 | }, 454 | "serialport": { 455 | "version": "9.0.3", 456 | "resolved": "https://registry.npmjs.org/serialport/-/serialport-9.0.3.tgz", 457 | "integrity": "sha512-1JjH9jtWZ5up2SQTeNPA4I3vhHCDYh1AjN3SybZYf5m9KF9tFVIOGLGIMncqnWKYx3ks/wqfCpmpYUHkFYC3wg==", 458 | "requires": { 459 | "@serialport/binding-mock": "^9.0.2", 460 | "@serialport/bindings": "^9.0.3", 461 | "@serialport/parser-byte-length": "^9.0.1", 462 | "@serialport/parser-cctalk": "^9.0.1", 463 | "@serialport/parser-delimiter": "^9.0.1", 464 | "@serialport/parser-readline": "^9.0.1", 465 | "@serialport/parser-ready": "^9.0.1", 466 | "@serialport/parser-regex": "^9.0.1", 467 | "@serialport/stream": "^9.0.2", 468 | "debug": "^4.1.1" 469 | } 470 | }, 471 | "set-blocking": { 472 | "version": "2.0.0", 473 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 474 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 475 | }, 476 | "signal-exit": { 477 | "version": "3.0.3", 478 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 479 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 480 | }, 481 | "simple-concat": { 482 | "version": "1.0.1", 483 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 484 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 485 | }, 486 | "simple-get": { 487 | "version": "3.1.0", 488 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 489 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 490 | "requires": { 491 | "decompress-response": "^4.2.0", 492 | "once": "^1.3.1", 493 | "simple-concat": "^1.0.0" 494 | } 495 | }, 496 | "string-width": { 497 | "version": "1.0.2", 498 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 499 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 500 | "requires": { 501 | "code-point-at": "^1.0.0", 502 | "is-fullwidth-code-point": "^1.0.0", 503 | "strip-ansi": "^3.0.0" 504 | } 505 | }, 506 | "string_decoder": { 507 | "version": "1.1.1", 508 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 509 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 510 | "requires": { 511 | "safe-buffer": "~5.1.0" 512 | } 513 | }, 514 | "strip-ansi": { 515 | "version": "3.0.1", 516 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 517 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 518 | "requires": { 519 | "ansi-regex": "^2.0.0" 520 | } 521 | }, 522 | "strip-json-comments": { 523 | "version": "2.0.1", 524 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 525 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 526 | }, 527 | "tar-fs": { 528 | "version": "2.1.1", 529 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 530 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 531 | "requires": { 532 | "chownr": "^1.1.1", 533 | "mkdirp-classic": "^0.5.2", 534 | "pump": "^3.0.0", 535 | "tar-stream": "^2.1.4" 536 | } 537 | }, 538 | "tar-stream": { 539 | "version": "2.1.4", 540 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", 541 | "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", 542 | "requires": { 543 | "bl": "^4.0.3", 544 | "end-of-stream": "^1.4.1", 545 | "fs-constants": "^1.0.0", 546 | "inherits": "^2.0.3", 547 | "readable-stream": "^3.1.1" 548 | }, 549 | "dependencies": { 550 | "readable-stream": { 551 | "version": "3.6.0", 552 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 553 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 554 | "requires": { 555 | "inherits": "^2.0.3", 556 | "string_decoder": "^1.1.1", 557 | "util-deprecate": "^1.0.1" 558 | } 559 | } 560 | } 561 | }, 562 | "tunnel-agent": { 563 | "version": "0.6.0", 564 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 565 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 566 | "requires": { 567 | "safe-buffer": "^5.0.1" 568 | } 569 | }, 570 | "util-deprecate": { 571 | "version": "1.0.2", 572 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 573 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 574 | }, 575 | "uuid": { 576 | "version": "8.3.2", 577 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 578 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 579 | }, 580 | "which-pm-runs": { 581 | "version": "1.0.0", 582 | "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", 583 | "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" 584 | }, 585 | "wide-align": { 586 | "version": "1.1.3", 587 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 588 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 589 | "requires": { 590 | "string-width": "^1.0.2 || 2" 591 | } 592 | }, 593 | "wrappy": { 594 | "version": "1.0.2", 595 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 596 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ptz-server", 3 | "version": "1.0.0", 4 | "description": "A full PTZ server to control VISCA-IP and VISCA-Serial cameras using a web interface and HID devices like joysticks and gamepads.", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@utopian/visca": "0.0.13", 14 | "node-hid": "^2.1.1", 15 | "serialport": "^9.0.3", 16 | "uuid": "^8.3.2" 17 | }, 18 | "devDependencies": { 19 | "@types/events": "^3.0.0", 20 | "@types/node": "^14.14.20", 21 | "@types/uuid": "^8.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sony-evi-h100s-user-manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmikels/ptz-server/bd8f155a0c98166e50e3f9ca374b0cb70089cc8c/sony-evi-h100s-user-manual.pdf -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | // MODULES 2 | import http, { IncomingMessage, ServerResponse } from 'http' 3 | import { ViscaController, ViscaControllerConfig } from "@utopian/visca" 4 | 5 | import SidewinderPP from "./controllers/sidewinderpp" 6 | import Dualshock4 from "./controllers/dualshock4" 7 | import {config} from "./config"; 8 | let {viscaSerial, viscaIPCameras, viscaServer} = config; 9 | 10 | let controllerConfig = config as ViscaControllerConfig; 11 | 12 | /* CONTROLLER HANDLER */ 13 | let sw = new SidewinderPP(); // reports axes as signed values 14 | sw.onUpdate((data: Buffer) => console.log(data)); 15 | 16 | let ds4 = new Dualshock4(); // reports axes as unsigned 8 bit 17 | ds4.onUpdate((data: Buffer) => console.log(data)); 18 | 19 | // VISCA INTERFACE 20 | let vc = new ViscaController({viscaSerial, viscaIPCameras, viscaServer}); 21 | vc.startSerial('COM8', 38400); 22 | 23 | 24 | /* VISCA IP PASSTHROUGH */ 25 | 26 | 27 | /* HTTP HANDLER FOR AUTOMATION BY API */ 28 | const httpHandler = function (req: IncomingMessage, res: ServerResponse) { 29 | res.writeHead(200); 30 | res.end('Hello, World!'); 31 | } 32 | const server = http.createServer(httpHandler); 33 | server.listen(52380); 34 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | let macros; 4 | if ( fs.existsSync( 'macros.json' ) ) { 5 | try { 6 | macros = JSON.parse( fs.readFileSync( 'macros.json' ) ); 7 | } catch ( e ) { 8 | macros = {} 9 | } 10 | } 11 | 12 | export let config = { 13 | // serial port for talking to visca cameras 14 | viscaSerial: { 15 | port: 'COM8', 16 | baud: 38400, 17 | }, 18 | 19 | // configuration for visca-ip cameras 20 | // {name, index, ip, port, [ptz | sony]} 21 | viscaIPCameras: [], 22 | 23 | // configuration for the visca ip translation server 24 | // the http server will reside at the basePort 25 | // udp servers will exist at basePort + cameraIndex 26 | viscaServer: { 27 | basePort: 52380, 28 | }, 29 | 30 | // default controller configurations 31 | controllers: { 32 | 'global': {}, 33 | 'sidewinderpp': {}, 34 | 'dualshock4': {} 35 | }, 36 | 37 | macros 38 | } 39 | -------------------------------------------------------------------------------- /src/controller-inspect.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | console.log(HID.devices()); 3 | 4 | // quick and dirty 6 bit twos complement 5 | // because of how javascript handles bitwise NOT 6 | // of unsigned values... it does the NOT, 7 | // but then it interprets the result as a SIGNED value 8 | // which ends up negative because the original was unsigned 9 | function unsigned2signed(val, bits = 8) { 10 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 11 | let mask = Math.pow(2, bits); // 0b1_0000_0000 12 | if (val & signBit) return -(mask + ~val + 1); 13 | else return val; 14 | } 15 | 16 | // var hid = new HID.HID(1356, 2508); 17 | var hid = new HID.HID(1118, 8); 18 | hid.on("data", function (data) { 19 | let x = data[1] | ((data[2] & 0b00000011) << 8) 20 | x = unsigned2signed(x, 10); 21 | 22 | let a = data[2] & 0b11111100; // ignore the two bits used as the sign for x 23 | let b = data[3] & 0b00001111; // ignore the part used for z 24 | let y = (b << 6) | (a >> 2) 25 | y = unsigned2signed(y, 10); 26 | // let y = ((data[2] & 0b11111100) >> 2 ) | data[3] 27 | let unknown = data[3] & 0b00001111 28 | let other = data[5] 29 | console.log(y); 30 | }); 31 | 32 | /* SIDEWINDER PRECISION PRO 33 | details are found near line 300 34 | https://github.com/torvalds/linux/blob/master/drivers/input/joystick/sidewinder.c 35 | 36 | // this is what I reverse engineered 37 | first four bits are the top buttons 38 | trigger = 1 39 | left button = 2 40 | top right = 4 41 | bottom right = 8 42 | 43 | next four bits are the nine positions of the hat 44 | 0-8 up-nothing 45 | 46 | next 8 bits are the least significant bits of the 10 bit X axis (signed) 47 | next 6 bits are the least significant bits of the 10 bit Y axis (signed) 48 | next 2 bits are the most significant bits of the X axis 49 | next 4 bits are the least significant bits of the 6 bit Z axis (signed) 50 | next 4 bits are the most significant bits of the Y axis 51 | next 6 bits are used for buttons 52 | next 2 bits are used for the most significant bits of the Z axis 53 | 54 | it works like this: 55 | byte 3 - cdef---- 56 | byte 4 - ------ab 57 | final value - abcdef as a six bit signed value 58 | 59 | sample code: 60 | let sign = data[4] << 4; 61 | let val = data[3] >> 4; 62 | let final = sign | val; 63 | // six bit signed 2s complement 64 | final = unsigned2signed(final, 6); 65 | console.log(sign.toString(2), val.toString(2), final.toString(2), final); 66 | 67 | 68 | next 4 bits are buttons c, d, arrow 69 | next 4 bits are complicated 70 | bit 0 is z axis turned to the right but also when slightly to the left 71 | bit 1 is z axis turned to the left at all 72 | bit 2 is button A 73 | bit 3 is button B 74 | 75 | next 8 bits are the dial but only 7 bits are used 76 | when centered, the value is 00 77 | it's value is a 7 bit 2s complement signed integer 78 | it increments when moving counter-clockwise 79 | -64 – 63 80 | 81 | represented as a signed 7 bit integer 82 | incrementing when moving counterclockwise 83 | 84 | */ 85 | 86 | 87 | /* PS4 88 | LX = 1 89 | LY = 2 90 | 91 | RX = 3 92 | RY = 4 93 | 94 | DPAD = 5low (VALUE CLOCKWISE 0-8 up-nothing) 95 | BUTTONS = 5high BITMASK - T O X S (8 4 2 1) 96 | STICK BUTTON = 6high BITMASK - RIGHT LEFT OPTION SHARE (8 4 2 1) 97 | TRIGGERS = 6low BITMASK (R2 L2 R1 L1) (8 4 2 1) 98 | L2 analog = 8 (0-255) 99 | R2 analog = 9 (0-255) 100 | TOUCHPAD 101 | 102 | */ 103 | // let Gamecontroller = require('gamecontroller'); 104 | 105 | // let dev = Gamecontroller.getDevices(); 106 | 107 | // console.log(dev); -------------------------------------------------------------------------------- /src/controllers/dualshock4.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | 3 | const dpadcodes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']; 4 | 5 | // quick and dirty 6 bit twos complement 6 | // because of how javascript handles bitwise NOT 7 | // of unsigned values... it does the NOT, 8 | // but then it interprets the result as a SIGNED value 9 | // which ends up negative because the original was unsigned 10 | function unsigned2signed(val, bits = 8) { 11 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 12 | let mask = Math.pow(2, bits); // 0b1_0000_0000 13 | if (val & signBit) return -(mask + ~val + 1); 14 | else return val; 15 | } 16 | 17 | function deepEquals(a, b) { 18 | if (typeof a != typeof b) return false; 19 | 20 | if (Array.isArray(a) && Array.isArray(b)) { 21 | for (let i = 0; i < a.length; i++) { 22 | if (!deepEquals(a[i], b[i])) return false; 23 | } 24 | } else if (typeof a == 'object' && a != null) { 25 | for (let key of Object.keys(a)) { 26 | if (!deepEquals(a[key], b[key])) return false; 27 | } 28 | } else { 29 | return a == b; 30 | } 31 | return true; 32 | } 33 | 34 | 35 | 36 | class Dualshock4 { 37 | constructor() { 38 | this.hid = new HID.HID(1356, 2508); 39 | this.status = {}; 40 | this.callback = null; 41 | this.hid.on("data", (data) => { 42 | this.handleData(data); 43 | }); 44 | } 45 | 46 | onUpdate(f) { 47 | this.callback = f; 48 | } 49 | 50 | handleData(data) { 51 | let status = {} 52 | status.lx = data[1]; 53 | status.ly = data[2]; 54 | status.rx = data[3]; 55 | status.ry = data[4]; 56 | 57 | let dpadval = data[5] & 0b00001111; 58 | let dpadcode = dpadcodes[dpadval]; 59 | status.dpad = { value: dpadval, code: dpadcode } 60 | 61 | status.buttonT = (data[5] & 0b10000000) != 0; 62 | status.buttonO = (data[5] & 0b01000000) != 0; 63 | status.buttonX = (data[5] & 0b00100000) != 0; 64 | status.buttonS = (data[5] & 0b00010000) != 0; 65 | 66 | status.buttonShare = (data[6] & 0b00010000) != 0; 67 | status.buttonOption = (data[6] & 0b00100000) != 0; 68 | 69 | status.buttonLS = (data[6] & 0b01000000) != 0; 70 | status.buttonRS = (data[6] & 0b10000000) != 0; 71 | 72 | status.triggerL1 = (data[6] & 0b0001) != 0; 73 | status.triggerR1 = (data[6] & 0b0010) != 0; 74 | status.triggerL2 = (data[6] & 0b0100) != 0; 75 | status.triggerR2 = (data[6] & 0b1000) != 0; 76 | 77 | status.analogL2 = data[8]; 78 | status.analogR2 = data[9]; 79 | 80 | // TODO: touchpad data 81 | 82 | // let dirty = false; 83 | // for (let key of Object.keys(status)) { 84 | // if (key == 'dpad') { 85 | // if (status.dpad.value != this.status.dpad.value) { 86 | // dirty = true; 87 | // break; 88 | // } 89 | // continue; 90 | // } 91 | // if (status[key] != this.status[key]) { 92 | // dirty = true; 93 | // console.log(key); 94 | // break; 95 | // } 96 | // } 97 | 98 | if (!deepEquals(status, this.status)) { 99 | this.status = status; 100 | if (this.callback != null) this.callback(status); 101 | } 102 | } 103 | } 104 | 105 | module.exports = Dualshock4; 106 | 107 | /* PS4 108 | LX = 1 109 | LY = 2 110 | 111 | RX = 3 112 | RY = 4 113 | 114 | DPAD = 5low (VALUE CLOCKWISE 0-8 up-nothing) 115 | BUTTONS = 5high BITMASK - T O X S (8 4 2 1) 116 | STICK BUTTON = 6high BITMASK - RIGHT LEFT OPTION SHARE (8 4 2 1) 117 | TRIGGERS = 6low BITMASK (R2 L2 R1 L1) (8 4 2 1) 118 | L2 analog = 8 (0-255) 119 | R2 analog = 9 (0-255) 120 | TOUCHPAD 121 | 122 | */ 123 | -------------------------------------------------------------------------------- /src/controllers/sidewinderpp.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | 3 | const hatcodes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']; 4 | 5 | // quick and dirty 6 bit twos complement 6 | // because of how javascript handles bitwise NOT 7 | // of unsigned values... it does the NOT, 8 | // but then it interprets the result as a SIGNED value 9 | // which ends up negative because the original was unsigned 10 | function unsigned2signed(val, bits = 8) { 11 | let signBit = Math.pow(2, bits - 1); // 0b1000_0000 12 | let mask = Math.pow(2, bits); // 0b1_0000_0000 13 | if (val & signBit) return -(mask + ~val + 1); 14 | else return val; 15 | } 16 | 17 | function deepEquals(a, b) { 18 | if (typeof a != typeof b) return false; 19 | 20 | if (Array.isArray(a) && Array.isArray(b)) { 21 | for (let i = 0; i < a.length; i++) { 22 | if (!deepEquals(a[i], b[i])) return false; 23 | } 24 | } else if (typeof a == 'object' && a != null) { 25 | for (let key of Object.keys(a)) { 26 | if (!deepEquals(a[key], b[key])) return false; 27 | } 28 | } else { 29 | return a == b; 30 | } 31 | return true; 32 | } 33 | 34 | class SidewinderPP { 35 | constructor() { 36 | this.hid = new HID.HID(1118, 8); 37 | this.status = {}; 38 | this.callback = null; 39 | this.hid.on("data", (data) => { 40 | this.handleData(data); 41 | }); 42 | } 43 | 44 | onUpdate(f) { 45 | this.callback = f; 46 | } 47 | 48 | handleData(data) { 49 | let status = {} 50 | status.t1 = (data[0] & 0b00010000) > 0; 51 | status.t2 = (data[0] & 0b00100000) > 0; 52 | status.t3 = (data[0] & 0b01000000) > 0; 53 | status.t4 = (data[0] & 0b10000000) > 0; 54 | let hatval = data[0] & 0b00001111; 55 | let hatcode = hatcodes[hatval]; 56 | status.hat = { value: hatval, code: hatcode }; 57 | 58 | let x = data[1] | ((data[2] & 0b00000011) << 8) 59 | status.x = unsigned2signed(x, 10); 60 | 61 | let a = data[2] & 0b11111100; // ignore the two bits used as the sign for x 62 | let b = data[3] & 0b00001111; // ignore the part used for z 63 | let y = (b << 6) | (a >> 2) 64 | status.y = unsigned2signed(y, 10); 65 | 66 | // handle z axis 67 | let sign = (data[4] & 0b11) << 4; 68 | let val = data[3] >> 4; 69 | let final = sign | val; 70 | status.z = unsigned2signed(final, 6); 71 | 72 | status.buttonA = (data[4] & 0b00000100) > 0; 73 | status.buttonB = (data[4] & 0b00001000) > 0; 74 | status.buttonC = (data[4] & 0b00010000) > 0; 75 | status.buttonD = (data[4] & 0b00100000) > 0; 76 | status.buttonArrow = (data[4] & 0b01000000) > 0; 77 | 78 | status.dial = unsigned2signed(data[5], 7); 79 | 80 | if (!deepEquals(status, this.status)) { 81 | this.status = status; 82 | if (this.callback != null) this.callback(status); 83 | } 84 | } 85 | } 86 | 87 | module.exports = SidewinderPP; 88 | 89 | /* SIDEWINDER PRECISION PRO 90 | first four bits are the top buttons 91 | trigger = 1 92 | left button = 2 93 | top right = 4 94 | bottom right = 8 95 | 96 | next four bits are the nine positions of the hat 97 | 0-8 up-nothing 98 | 99 | next 8 bits are the least significant bits of the 10 bit X axis (signed) 100 | next 6 bits are the least significant bits of the 10 bit Y axis (signed) 101 | next 2 bits are the most significant bits of the X axis 102 | next 4 bits are the least significant bits of the 6 bit Z axis (signed) 103 | next 4 bits are the most significant bits of the Y axis 104 | next 6 bits are used for buttons 105 | next 2 bits are used for the most significant bits of the Z axis 106 | 107 | it works like this: 108 | byte 3 - cdef---- 109 | byte 4 - ------ab 110 | final value - abcdef as a six bit signed value 111 | 112 | sample code: 113 | let sign = data[4] << 4; 114 | let val = data[3] >> 4; 115 | let final = sign | val; 116 | // six bit signed 2s complement 117 | final = unsigned2signed(final, 6); 118 | console.log(sign.toString(2), val.toString(2), final.toString(2), final); 119 | 120 | 121 | byte 4 also holds the data for buttons c, d, arrow, b, a 122 | in the most significant six bits 123 | 124 | bits 0 and 1 are used as the sign bit for the z axis 125 | bit 2 is button A 126 | bit 3 is button B 127 | bit 4 is arrow 128 | bit 5 is d 129 | bit 6 is c 130 | 131 | next 8 bits are the dial but only 7 bits are used 132 | when centered, the value is 00 133 | it's value is a 7 bit 2s complement signed integer 134 | it increments when moving counter-clockwise 135 | -64 – 63 136 | 137 | represented as a signed 7 bit integer 138 | incrementing when moving counterclockwise 139 | 140 | */ -------------------------------------------------------------------------------- /src/macros.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "esModuleInterop": true, 5 | "allowJs": true, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | // "lib": [ 9 | // "es2018" 10 | // ], 11 | "module": "commonjs", 12 | "target": "es6", 13 | "baseUrl": ".", 14 | "paths": { 15 | "*": [ 16 | "node_modules/*", 17 | "src/types/*" 18 | ] 19 | }, 20 | "typeRoots": [ 21 | "node_modules/@types", 22 | "src/types" 23 | ], 24 | "outDir": "./dist", 25 | }, 26 | "include": [ 27 | "./src/**/*" 28 | ], 29 | "exclude": [ 30 | "./node-gamepad", 31 | "./build" 32 | ] 33 | } --------------------------------------------------------------------------------