feat: initial version
This commit is contained in:
parent
ec84170e32
commit
92c8542a24
372
index.js
Normal file
372
index.js
Normal file
|
@ -0,0 +1,372 @@
|
|||
"use strict";
|
||||
|
||||
const { Adapter, Device, Property, Action } = require('gateway-addon');
|
||||
const manifest = require('./manifest.json');
|
||||
const dgram = require('dgram');
|
||||
const net = require('net');
|
||||
|
||||
// https://source.android.com/devices/input/keyboard-devices#hid-keyboard-and-keypad-page-0x07
|
||||
const HID_MAP = {
|
||||
PLAY: {
|
||||
code: '176',
|
||||
type: 'consumer',
|
||||
modifier: '000', // this is actually the upper byte and not the modifiers
|
||||
},
|
||||
PAUSE: {
|
||||
code: '177',
|
||||
type: 'consumer',
|
||||
modifier: '000',
|
||||
},
|
||||
HOME: {
|
||||
code: '035',
|
||||
type: 'consumer',
|
||||
modifier: '002'
|
||||
},
|
||||
ENTER: {
|
||||
code: '040',
|
||||
type: 'keyboard',
|
||||
modifier: '000'
|
||||
},
|
||||
RIGHT: {
|
||||
code: '069',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
LEFT: {
|
||||
code: '068',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
DOWN: {
|
||||
code: '067',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
UP: {
|
||||
code: '066',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
BACK: {
|
||||
code: '036',
|
||||
type: 'consumer',
|
||||
modifier: '002'
|
||||
},
|
||||
MUTE: {
|
||||
code: '226',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
VOLUME_UP: {
|
||||
code: '233',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
VOLUME_DOWN: {
|
||||
code: '234',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
NEXT: {
|
||||
code: '181',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
PREVIOUS: {
|
||||
code: '182',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
STOP: {
|
||||
code: '183',
|
||||
type: 'consumer',
|
||||
modifier: '00*'
|
||||
},
|
||||
SLEEP: {
|
||||
code: '050',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
REWIND: {
|
||||
code: '180',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
FASTFORWARD: {
|
||||
code: '179',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
RED: {
|
||||
code: '105',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
GREEN: {
|
||||
code: '106',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
BLUE: {
|
||||
code: '107',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
YELLOW: {
|
||||
code: '108',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
CLOSED_CAPTIONS: {
|
||||
code: '097',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
CHANNEL_UP: {
|
||||
code: '156',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
},
|
||||
CHANNEL_DOWN: {
|
||||
code: '157',
|
||||
type: 'consumer',
|
||||
modifier: '000'
|
||||
}
|
||||
//TODO guide?
|
||||
};
|
||||
|
||||
const CONTROL_CODE = {
|
||||
consumer: {
|
||||
short: '2',
|
||||
long: '6'
|
||||
},
|
||||
keyboard: {
|
||||
short: '1',
|
||||
long: '5'
|
||||
}
|
||||
};
|
||||
|
||||
class IRUSBProperty extends Property {
|
||||
async setValue(value) {
|
||||
if(this.name === 'power') {
|
||||
if(value) {
|
||||
return this.device.sendCommand('WAKE');
|
||||
}
|
||||
return this.device.sendKey('SLEEP');
|
||||
}
|
||||
else if(this.name === 'playing') {
|
||||
if(value) {
|
||||
return this.device.sendKey('PLAY');
|
||||
}
|
||||
return this.device.sendKey('PAUSE');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IRUSBDevice extends Device {
|
||||
constructor(adapter, uuid, ip, port) {
|
||||
super(adapter, uuid);
|
||||
this.name = uuid;
|
||||
this.socket = new net.Socket();
|
||||
this.socket.setEncoding('ascii');
|
||||
this.addProperty(new IRUSBProperty(this, 'power', {
|
||||
title: 'Power',
|
||||
'@type': 'OnOffProperty',
|
||||
type: 'boolean'
|
||||
}));
|
||||
this.addAction('launch', {
|
||||
title: 'Launch App',
|
||||
input: {
|
||||
type: 'string'
|
||||
}
|
||||
});
|
||||
this.addAction('remoteShort', {
|
||||
title: 'Short Press',
|
||||
input: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'PLAY',
|
||||
'PAUSE',
|
||||
'ENTER',
|
||||
'HOME',
|
||||
'BACK',
|
||||
'UP',
|
||||
'DOWN',
|
||||
'LEFT',
|
||||
'RIGHT',
|
||||
'NEXT',
|
||||
'PREVIOUS',
|
||||
'REWIND',
|
||||
'FASTFORWARD',
|
||||
'MUTE',
|
||||
'VOLUME_UP',
|
||||
'VOLUME_DOWN',
|
||||
'RED',
|
||||
'GREEN',
|
||||
'BLUE',
|
||||
'YELLOW'
|
||||
]
|
||||
}
|
||||
});
|
||||
this.addAction('remoteLong', {
|
||||
title: 'Long Press',
|
||||
input: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ENTER',
|
||||
'FASTFORWARD',
|
||||
'REWIND',
|
||||
'UP',
|
||||
'DOWN',
|
||||
'LEFT',
|
||||
'RIGHT'
|
||||
]
|
||||
}
|
||||
});
|
||||
this.addAction('cancelKeys', {
|
||||
title: 'Cancel Presses'
|
||||
});
|
||||
this.addProperty(new IRUSBProperty(this, 'playing', {
|
||||
title: 'Playing',
|
||||
type: 'boolean'
|
||||
}));
|
||||
this.addProperty(new Property(this, 'app', {
|
||||
title: 'Application',
|
||||
type: 'string',
|
||||
readOnly: true
|
||||
}));
|
||||
this.responseQueue = [];
|
||||
this.socket.connect(port, ip, () => {
|
||||
this.connectedNotify(true);
|
||||
this.adapter.handleDeviceAdded(this);
|
||||
});
|
||||
this.socket.on('close', () => {
|
||||
this.connectedNotify(false);
|
||||
});
|
||||
this.socket.on('data', (data) => {
|
||||
for(const waitingOn of this.responseQueue) {
|
||||
if(data.startsWith(waitingOn.code)) {
|
||||
waitingOn.callback(data.slice(waitingOn.code.length).trim());
|
||||
this.responseQueue.splice(this.responseQueue.indexOf(waitingOn), 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//TODO event for incoming IR
|
||||
console.log('Unexpected packet', data);
|
||||
});
|
||||
this.interval = setTimeout(() => this.updateState(), 5000);
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
async updateState() {
|
||||
const playing = await this.sendCommand('GETPLAY');
|
||||
const app = await this.sendCommand('GETFG');
|
||||
console.log(playing, app);
|
||||
const isPlaying = playing === '1\r';
|
||||
this.findProperty('playing').setCachedValueAndNotify(isPlaying);
|
||||
this.findProperty('app').setCachedValueAndNotify(app);
|
||||
// app !== screensaver
|
||||
this.findProperty('power').setCachedValueAndNotify(isPlaying && app);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.interval);
|
||||
this.socket.destroy();
|
||||
}
|
||||
|
||||
async sendKey(keyName, long = false) {
|
||||
let pressType = long ? 'long' : 'short';
|
||||
const keyInfo = HID_MAP[keyName];
|
||||
const startCode = CONTROL_CODE[keyInfo.type][pressType];
|
||||
return this.sendCommand(`HIDCODE${startCode}${keyInfo.code}${keyInfo.modifier}`);
|
||||
}
|
||||
|
||||
sendCommand(command) {
|
||||
return new Promise((resolve) => {
|
||||
const fullCommand = `Q${command}\r`;
|
||||
this.responseQueue.push({
|
||||
code: fullCommand,
|
||||
callback(response) {
|
||||
resolve(response.split('\r').filter((part) => part.length && part !== 'OK').join('\r'));
|
||||
}
|
||||
});
|
||||
this.socket.write(fullCommand);
|
||||
});
|
||||
}
|
||||
|
||||
reconnect(ip, port) {
|
||||
if(this.socket.readyState !== 'open') {
|
||||
this.socket.connect(port, ip, () => {
|
||||
this.connectedNotify(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Action} action
|
||||
*/
|
||||
async performAction(action) {
|
||||
switch(action.getName()) {
|
||||
case 'launch':
|
||||
return this.sendCommand(`LAUNCH ${action.getInput()}`);
|
||||
case 'remoteShort':
|
||||
return this.sendKey(action.getInput(), false);
|
||||
case 'remoteLong':
|
||||
return this.sendKey(action.getInput(), true);
|
||||
case 'cancelKeys':
|
||||
return this.sendCommand('HIDCODE0000000');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IRUSBAdapter extends Adapter {
|
||||
constructor(addonManager) {
|
||||
super(addonManager, manifest.id, manifest.id);
|
||||
addonManager.addAdapter(this);
|
||||
|
||||
this.startDiscovering();
|
||||
}
|
||||
|
||||
async startDiscovering() {
|
||||
//TODO close socket on unload!
|
||||
this.socket = dgram.createSocket({
|
||||
type: 'udp4'
|
||||
});
|
||||
await new Promise((resolve) => this.socket.bind(1904, resolve));
|
||||
this.socket.setBroadcast(true);
|
||||
this.socket.addMembership('239.255.255.250');
|
||||
this.socket.on('message', (message, info) => {
|
||||
const string = message.toString('ascii');
|
||||
if(string.startsWith('NOTIFY \n')) {
|
||||
const [ header, uuid, ip, port ] = string.split('\n');
|
||||
this.onDevice(uuid.trim(), ip.trim(), port.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDevice(uuid, ip, port) {
|
||||
if(!this.devices[uuid]) {
|
||||
new IRUSBDevice(this, uuid, ip, port);
|
||||
}
|
||||
else {
|
||||
this.devices[uuid].reconnect(ip, port);
|
||||
}
|
||||
}
|
||||
|
||||
handleDeviceRemoved(device) {
|
||||
device.destroy();
|
||||
super.handleDeviceRemoved(device);
|
||||
}
|
||||
|
||||
async unload() {
|
||||
await new Promise((resolve) => {
|
||||
this.socket.close(resolve);
|
||||
});
|
||||
return super.unload();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (addonManager) => {
|
||||
new IRUSBAdapter(addonManager);
|
||||
};
|
19
manifest.json
Normal file
19
manifest.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"manifest_version": 1,
|
||||
"id": "irusb-adapter",
|
||||
"name": "IrUSB",
|
||||
"short_name": "IrUSB",
|
||||
"version": "0.1.0",
|
||||
"description": "Adapter for Video Lan IrUSB devices (Android TV, nVidia Shield with an adapter)",
|
||||
"homepage_url": "https://git.humanoids.be/freaktechnik/irusb-adapter",
|
||||
"license": "MIT",
|
||||
"author": "Martin Giger",
|
||||
"gateway_specific_settings": {
|
||||
"webthings": {
|
||||
"exec": "{nodeLoader} {path}",
|
||||
"strict_min_version": "0.10.0",
|
||||
"strict_max_version": "*",
|
||||
"primary_type": "adapter"
|
||||
}
|
||||
}
|
||||
}
|
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "irusb-adapter",
|
||||
"version": "1.0.0",
|
||||
"description": "webthings.io gateway adapter for the Video Storm IrUSB device. Allows control of Android TV devices like nVidia Shield over Network.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.humanoids.be:222/freaktechnik/irusb-adapter.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webthingio"
|
||||
],
|
||||
"author": "Martin Giger (https://humanoids.be)",
|
||||
"license": "MIT"
|
||||
}
|
Loading…
Reference in a new issue