diff --git a/.gitignore b/.gitignore index db8141a..c0c4d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ apikey.txt -ptt_daemon \ No newline at end of file +ptt_daemon +config.js \ No newline at end of file diff --git a/ClientQuery.js b/ClientQuery.js index 9156fdf..0514965 100644 --- a/ClientQuery.js +++ b/ClientQuery.js @@ -8,7 +8,7 @@ function ClientQuery() { this.onMessage = function (data) { }; this.log = function (msg) { - if (!this.debug) { console.log(msg) } + if (!this.debug) { console.log('TS3:', msg) } } this.parse = function (res) { @@ -42,13 +42,18 @@ function ClientQuery() { }.bind(this) this.notifyOn = function (action, args, callback) { - console.log('Register notify ' + action) + if (this.actions[action] === undefined) { + console.log('TS3: Register notify ' + action) + } else if (this.actions[action] !== callback) { + console.log('TS3: Change notify ' + action) + } + this.actions[action] = callback; this.send(`clientnotifyregister event=${action} ${args}`) } this.notifyOff = function (action, args) { - console.log('Unregister notify ' + action) + console.log('TS3: Unregister notify ' + action) this.send(`clientnotifyunregister event=${action} ${args}`) .then(() => { this.actions[action] = undefined; @@ -59,7 +64,7 @@ function ClientQuery() { this.log('Request: ' + data) return new Promise(function (resolve, reject) { this.sock.write(data + '\n', 'utf8', (res) => { - console.log('Sent: ' + data) + console.log('TS3: Sent: ' + data) let func = (data) => { if (data.toString() != 'error id=0 msg=ok\n\r') { @@ -89,9 +94,7 @@ function ClientQuery() { this.reInitActions = function () { let act = this.actions; - console.log('actions', Object.keys(act)) Object.keys(act).forEach(function (key) { - console.log('addNotifyMemes Lol', key, 'schandlerid=1', typeof (act[key])) this.notifyOn(key, 'schandlerid=1', act[key]) }.bind(this)); } @@ -127,7 +130,7 @@ function ClientQuery() { sock.on('close', () => { - console.log('Socket closed, reopening..'); + console.log('TS3: Socket closed, reopening..'); sock.destroy(); this.connect(host, port, apikey); }); diff --git a/README.md b/README.md index e69de29..8e39a7a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,23 @@ +# ts3_remote_ham + +A simple Node.js app that triggers the PTT line of a transceiver when someone in the same channel of a TeamSpeak 3 server starts speaking. + +## Requirements +- TeamSpeak 3 with the [Client Query](https://www.myteamspeak.com/addons/L2FkZG9ucy85NDNkZDgxNi03ZWYyLTQ4ZDctODJiOC1kNjBjM2I5YjEwYjM%3D) plugin, which is pre-installed by default. +- If using the serial port, GCC to compile the ptt_daemon.c utility. + +## Supported setups +- FLrig via its XML-RPC server. This should be enabled by default in FLrig. +- microHam microKeyer via [mhuxd](https://github.com/dj5qv/mhuxd) or really any tty device (RS-232, USB UART adapters). The `ptt_daemon` utility is responsible for enabling the RTS line of the serial port (high = PTT on). + +## Usage: +1. Clone the repository, enter it. +2. `$ npm i` to install the dependencies. +3. `$ npm run compile` to compile the ptt_daemon (optional). +4. Start TeamSpeak. +5. Go to the menu Tools > Options > Addons and click the button *Disabled* to enable the Client Query plugin. +6. Open the plugin's settings and copy the API key. Paste it into the `config.js` file created automatically in step 2. +7. `$ npm run start` to run the program. +8. If using the serial port implementation, change the provider to "serial" and set the path in `config.js`. + +Be aware that by running this program on a public server may lead to unauthorized transmissions using your rig, or even damage the rig by exceeding the recommended long-time power limits. Make sure to not let the remote client linger unsupervised. You can mute the reception by pressing the *Mute Speakers/Headphones* button (a "sound muted" voice line will be played) through remote desktop on the remote client. It is recommended to change the local capture activation method to push to talk during remote operation. diff --git a/config.sample.js b/config.sample.js new file mode 100644 index 0000000..73bafb5 --- /dev/null +++ b/config.sample.js @@ -0,0 +1,22 @@ +const config = {}; + +config.teamspeak = { + ip: '127.0.0.1', + port: '25639', + + // Key returned by the Client Query plugin in TeamSpeak. Can also be found in ~/.ts3client/clientquery.ini + // It should look something like LKT9-ZLDM-LFK0-CLSB-UDEE-2YVJ. + apiKey: '', +}; + +config.provider = 'flrig'; // flrig or serial +config.providerOptions = { + serial: { + port: '/dev/mhuxd/ptt1', // path to the serial port of the radio or the VSP created by mhuxd + }, + flrig: { + url: 'http://127.0.0.1:12345', // URL to the FLrig XML-RPC server + }, +}; + +export default config; \ No newline at end of file diff --git a/daemon.js b/daemon.js index f00f6bf..4b0ea63 100644 --- a/daemon.js +++ b/daemon.js @@ -1,36 +1,37 @@ import * as fs from 'fs'; import ClientQuery from "./ClientQuery.js"; import flrig from './providers/flrig.js'; -import mhuxd from './providers/mhuxd.js'; +import serial from './providers/serial.js'; +import config from './config.js'; -const apikey = fs.readFileSync('./apikey.txt').toString(); const client = new ClientQuery(); try { - await client.connect('127.0.0.1', '25639', apikey); + await client.connect(config.teamspeak.host, config.teamspeak.port, config.teamspeak.apiKey); } catch (error) { - console.error('Cannot connect to TS3 query. Is TeamSpeak running?', error); + console.error('Cannot connect to TS3 query. Is TeamSpeak running?', error); } - // client.request('sendtextmessage targetmode=2 msg=Node.JS').then( res =>{ console.log(res.toString()) }); await new Promise(r => setTimeout(r, 50)); const speakingUsers = {}; const whoamiData = client.parse((await client.request('whoami')).toString()); -const provider = flrig(); +const provider = { + flrig, serial +}[config.provider](config.providerOptions); client.notifyOn('notifytalkstatuschange', '', data => { - let args = client.parse(data.toString()); - if (args.clid === whoamiData.clid) - return; + let args = client.parse(data.toString()); + if (args.clid === whoamiData.clid) + return; - speakingUsers[args.clid] = parseInt(args.status) === 1; - if (Object.values(speakingUsers).find(el => el)) { - // someone is speaking - provider.pttDown(); - } else { - // nobody is speaking - provider.pttUp(); - } + speakingUsers[args.clid] = parseInt(args.status) === 1; + if (Object.values(speakingUsers).find(el => el)) { + // someone is speaking + provider.pttDown(); + } else { + // nobody is speaking + provider.pttUp(); + } }); diff --git a/package-lock.json b/package-lock.json index a0c551d..8a95528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "ts3-remote", "version": "1.0.0", + "hasInstallScript": true, "license": "GPL", "dependencies": { "@foxglove/xmlrpc": "^1.3.0", diff --git a/package.json b/package.json index 8997db9..a450b64 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "TS3 remote client for MicroKeyer PTT.", "main": "daemon.js", "scripts": { - "start": "node daemon.js" + "start": "node daemon.js", + "compile": "gcc -o ptt_daemon ptt_daemon.c", + "postinstall": "cp -n config.sample.js config.js" }, "type": "module", "author": "ericek111", diff --git a/providers/flrig.js b/providers/flrig.js index 1910ecd..2ba23e1 100644 --- a/providers/flrig.js +++ b/providers/flrig.js @@ -1,14 +1,22 @@ import { XmlRpcClient } from "@foxglove/xmlrpc"; -export default function flrig() { - const client = new XmlRpcClient(`http://127.0.0.1:12345`); +export default function flrig(opts) { + const client = new XmlRpcClient(opts.flrig.url); + + const setPtt = async (state) => { + try { + await client.methodCall('rig.set_ptt', [state]); + } catch (ex) { + console.error(`Cannot send rig.set_ptt(${state}) to flrig (${opts.flrig.url}) -- perhaps it's not running? ${ex.code ?? 'unknown'} at ${ex.erroredSysCall ?? '?'}`); + } + } return { pttDown: () => { - client.methodCall('rig.set_ptt', [1]); + setPtt(1); }, pttUp: () => { - client.methodCall('rig.set_ptt', [0]); + setPtt(0); } }; }; \ No newline at end of file diff --git a/providers/mhuxd.js b/providers/mhuxd.js deleted file mode 100644 index 7ce2dcc..0000000 --- a/providers/mhuxd.js +++ /dev/null @@ -1,12 +0,0 @@ -import { spawn } from 'child_process'; - -export default function() { - return { - pttDown: () => { - spawn('/home/omega/Documents/ts3_remote/ptt_daemon', ['/dev/mhuxd/fsk1', '1']); - }, - pttUp: () => { - spawn('/home/omega/Documents/ts3_remote/ptt_daemon', ['/dev/mhuxd/fsk1', '0']); - } - }; -}; \ No newline at end of file diff --git a/providers/serial.js b/providers/serial.js new file mode 100644 index 0000000..d264853 --- /dev/null +++ b/providers/serial.js @@ -0,0 +1,12 @@ +import { spawn } from 'child_process'; + +export default function serial(opts) { + return { + pttDown: () => { + spawn('./ptt_daemon', [opts.serial.port, '1']); + }, + pttUp: () => { + spawn('./ptt_daemon', [opts.serial.port, '0']); + } + }; +}; \ No newline at end of file diff --git a/ptt_daemon.c b/ptt_daemon.c index c1ebe42..ab08326 100644 --- a/ptt_daemon.c +++ b/ptt_daemon.c @@ -24,8 +24,9 @@ int main(int argc, char **argv) int flags; ioctl(fd, TIOCMGET, &flags); + // The below flags can be replaced by TIOCM_DTR to use the DTR line instead, See $ man "TIOCMSET(2const)" if (rtsEnable != 0) { - flags |= TIOCM_RTS; + flags |= TIOCM_RTS; // brings the RTS line high } else { flags &= ~TIOCM_RTS; }