Skip to content

Commit

Permalink
feat: REST api (#78)
Browse files Browse the repository at this point in the history
Co-authored-by: Julian Waller <git@julusian.co.uk>
Co-authored-by: arc-alex <ac@omnivox.dk>
  • Loading branch information
3 people authored Aug 17, 2023
1 parent 8024bff commit 5e924b8
Show file tree
Hide file tree
Showing 10 changed files with 1,384 additions and 695 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Each device will appear in companion as its own 'satellite' device, and can be c

Note: This connects over the satellite device api which uses port TCP 16622.

[![Satellite Getting Started](http://img.youtube.com/vi/eNnUxRl4yP4/0.jpg)](http://www.youtube.com/watch?v=eNnUxRl4yP4 "Remote Stream Deck control with Companion Satellite")
[![Satellite Getting Started](http://img.youtube.com/vi/eNnUxRl4yP4/0.jpg)](http://www.youtube.com/watch?v=eNnUxRl4yP4 'Remote Stream Deck control with Companion Satellite')

## Getting started

Expand Down Expand Up @@ -41,10 +41,10 @@ To manually build the latest version for your machine:

It can be built and run as a systemd service on a pi or other linux machine


No images are provided for this, but the process has been written to be a single script.

As root, run the following:

```
curl https://raw.githubusercontent.com/bitfocus/companion-satellite/master/pi-image/install.sh | sh
```
Expand All @@ -53,6 +53,17 @@ After this, you can use `sudo satellite-update` to change the version it has ins

Note: This script will create a new user called `satellite`, which Satellite will be run as and will own the configuration.

### REST API

The default rest port is 9999
a GET request to `http://Satellite-IP:9999/api/host` will return the current target ip in plain text
a GET request to `http://Satellite-IP:9999/api/port` will return the current target port in plain text
a GET request to `http://Satellite-IP:9999/api/config` will return the current target port and ip as json

a POST request to `http://Satellite-IP:9999/api/host` with json body `{"host": "newhostip"}` or plain text `newhostip` will connect the satellite to that ip or hostname
a POST request to `http://Satellite-IP:9999/api/port` with `{"port": 16622}` or plain text `16622` will connect the satellite to that port
a POST request to `http://Satellite-IP:9999/api/config` with `{"host": "newhostip", "port": 16622}` will connect the satellite to that ip/hostname and port

## Development

NodeJS 16 is required
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@electron/notarize": "^2.1.0",
"@sofie-automation/code-standard-preset": "~2.4.7",
"@types/electron-prompt": "^1.6.1",
"@types/koa": "^2.13.8",
"@types/koa-router": "^7.4.4",
"@types/node": "^18.16.19",
"@types/node-hid": "^1.3.1",
"cross-env": "^7.0.3",
Expand All @@ -59,6 +61,9 @@
"eventemitter3": "^4.0.7",
"exit-hook": "^2.2.1",
"infinitton-idisplay": "^1.1.2",
"koa": "^2.14.2",
"koa-body": "^6.0.1",
"koa-router": "^12.0.0",
"meow": "^9.0.0",
"node-hid": "npm:@julusian/hid@2.5.0-3",
"semver": "^7.5.4",
Expand Down
5 changes: 4 additions & 1 deletion pi-image/satellite-config
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
COMPANION_IP=127.0.0.1

# If you are connecting through a router or firewall which has remapped the port, you will need to change that here to match
COMPANION_PORT=16622
COMPANION_PORT=16622

# Port for the REST server (0 to disable)
REST_PORT=9999
3 changes: 2 additions & 1 deletion pi-image/satellite.service
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Type=simple
User=satellite
WorkingDirectory=/usr/local/src/companion-satellite
EnvironmentFile=/boot/satellite-config
ExecStart=/opt/fnm/aliases/default/bin/node /usr/local/src/companion-satellite/dist/main.js $COMPANION_IP $COMPANION_PORT
Environment=SATELLITE_CONFIG_PATH=/boot/satellite-config
ExecStart=/opt/fnm/aliases/default/bin/node /usr/local/src/companion-satellite/dist/main.js $COMPANION_IP $COMPANION_PORT $REST_PORT
Restart=on-failure
KillSignal=SIGINT
TimeoutStopSec=60
Expand Down
10 changes: 10 additions & 0 deletions pi-image/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ cp pi-image/50-satellite.rules /etc/udev/rules.d/

# update startup script
cp pi-image/satellite.service /etc/systemd/system

# ADD REST_PORT to old config files
if grep -q REST_PORT /boot/satellite-config; then
echo "config ok"
else
echo "
# Port for the REST server (0 to disable)
REST_PORT=9999" >> /boot/satellite-config
fi

systemctl daemon-reload

# install some scripts
Expand Down
18 changes: 17 additions & 1 deletion src/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,25 @@ export class CardGenerator {
context2d.fillStyle = '#ffffff'

context2d.fillText(`Remote: ${remoteIp}`, 10, height - 10)
context2d.fillText(`Status: ${status}`, 10, height - 30)
context2d.fillText(`Local: ${getIPAddress()}`, 10, height - 30)
context2d.fillText(`Status: ${status}`, 10, height - 50)

// return result
return Buffer.from(context2d.getImageData(0, 0, width, height).data)
}
}
//#TODO: make llocal ip reactive
import { networkInterfaces } from 'os'
function getIPAddress() {
for (let devName in networkInterfaces()) {
let iface = networkInterfaces()[devName];
if (iface) {
for (let i = 0; i < iface.length; i++) {
let alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal)
return alias.address;
}
}
}
return '0.0.0.0';
}
4 changes: 2 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export type CompanionSatelliteClientEvents = {
log: [string]
connected: []
disconnected: []
ipChange: [string]
ipChange: [host: string, port: number]

draw: [DeviceDrawProps]
brightness: [{ deviceId: string; percent: number }]
Expand Down Expand Up @@ -221,7 +221,7 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
this._connectionActive = true

setImmediate(() => {
this.emit('ipChange', host)
this.emit('ipChange', host, port)
})

this._host = host
Expand Down
43 changes: 42 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { CompanionSatelliteClient } from './client'
import { DeviceManager } from './devices'
import { DEFAULT_PORT } from './lib'

import { RestServer } from './rest'
import * as fs from 'fs/promises'

const cli = meow(
`
Usage
$ companion-satellite hostname [port]
$ companion-satellite hostname [port] [REST port]
Examples
$ companion-satellite 192.168.1.100
$ companion-satellite 192.168.1.100 16622
$ companion-satellite 192.168.1.100 16622 9999
`,
{}
)
Expand All @@ -21,27 +25,64 @@ if (cli.input.length === 0) {
}

let port = DEFAULT_PORT
let rest_port = 0
if (cli.input.length > 1) {
port = Number(cli.input[1])
if (isNaN(port)) {
cli.showHelp(1)
}
if (cli.input.length > 2) {
rest_port = Number(cli.input[2])
if (isNaN(rest_port)) {
cli.showHelp(1)
}
}
}

console.log('Starting')

const client = new CompanionSatelliteClient({ debug: true })
const devices = new DeviceManager(client)
const server = new RestServer(client)

client.on('log', (l) => console.log(l))
client.on('error', (e) => console.error(e))

const configFilePath = process.env.SATELLITE_CONFIG_PATH
if (configFilePath) {
// Update the config file on changes, if a path is provided
client.on('ipChange', (newIP, newPort) => {
updateEnvironmentFile(configFilePath, { COMPANION_IP: newIP, COMPANION_PORT: String(newPort) }).catch((e) => {
console.log(`Failed to update config file:`, e)
})
})
}

exitHook(() => {
console.log('Exiting')
client.disconnect()
devices.close().catch(() => null)
server.close()
})

client.connect(cli.input[0], port).catch((e) => {
console.log(`Failed to connect`, e)
})
server.open(rest_port)

async function updateEnvironmentFile(filePath: string, changes: Record<string, string>): Promise<void> {
const data = await fs.readFile(filePath, 'utf-8')

let lines = data.split(/\r?\n/)

for (let i = 0; i < lines.length; i++) {
for (const key in changes) {
if (lines[i].startsWith(key)) {
lines[i] = key + '=' + changes[key]
}
}
}

const newData = lines.join('\n')
await fs.writeFile(filePath, newData, 'utf-8')
}
108 changes: 108 additions & 0 deletions src/rest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as Koa from 'koa'
import * as Router from 'koa-router'
import koaBody from 'koa-body'
import http = require('http')
import { CompanionSatelliteClient } from './client'

export class RestServer {
private _cs_client: CompanionSatelliteClient
private server: http.Server | undefined
private app: Koa
private router: Router

constructor(client: CompanionSatelliteClient) {
this._cs_client = client
this.app = new Koa()
this.router = new Router()

//GET
this.router.get('/api/host', async (ctx: any) => {
ctx.body = this._cs_client.host
})
this.router.get('/api/port', (ctx: any) => {
ctx.body = this._cs_client.port
})
this.router.get('/api/config', (ctx: any) => {
ctx.body = { host: this._cs_client.host, port: this._cs_client.port }
})

//POST
this.router.post('/api/host', koaBody(), async (ctx: any) => {
let host = ''
if (ctx.request.type == 'application/json') {
host = ctx.request.body['host']
} else if (ctx.request.type == 'text/plain') {
host = ctx.request.body
}

if (host) {
this._cs_client.connect(host, this._cs_client.port).catch((e) => {
console.log('set host failed:', e)
})
ctx.body = 'OK'
} else {
ctx.status = 400
ctx.body = 'Invalid host'
}
})
this.router.post('/api/port', koaBody(), async (ctx: any) => {
let newPort = NaN
if (ctx.request.type == 'application/json') {
newPort = Number(ctx.request.body['port'])
} else if (ctx.request.type == 'text/plain') {
newPort = Number(ctx.request.body)
}

if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
this._cs_client.connect(this._cs_client.host, newPort).catch((e) => {
console.log('set port failed:', e)
})
ctx.body = 'OK'
} else {
ctx.status = 400
ctx.body = 'Invalid port'
}
})
this.router.post('/api/config', koaBody(), async (ctx: any) => {
if (ctx.request.type == 'application/json') {
const host = ctx.request.body['host']
const port = Number(ctx.request.body['port'])

if (!host) {
ctx.status = 400
ctx.body = 'Invalid host'
} else if (isNaN(port) || port <= 0 || port > 65535) {
ctx.status = 400
ctx.body = 'Invalid port'
} else {
this._cs_client.connect(host, port).catch((e) => {
console.log('update config failed:', e)
})
}
ctx.body = 'OK'
}
})

this.app.use(this.router.routes()).use(this.router.allowedMethods())
}

public open(port: Number): void {
this.close()

if (port != 0) {
this.server = this.app.listen(port)
console.log(`REST server starting: port: ${port}`)
} else {
console.log('REST server not starting: port 0')
}
}

public close(): void {
if (this.server && this.server.listening) {
this.server.close()
this.server.closeAllConnections()
delete this.server
console.log('The rest server is closed')
}
}
}
Loading

0 comments on commit 5e924b8

Please sign in to comment.