diff --git a/.gitignore b/.gitignore index b43e169b..ddc8e2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules +audio/venv + npm-debug.log arduino/**/*.hex arduino/**/*.elf diff --git a/README.md b/README.md index 3d0495b8..91020671 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,13 @@ En `PROGRAM_Transition.js` pueden ver un ejemplo de agarrar varios programas dis Si estás familiarizado con la IDE de Arduino podés usarla para compilar y subir los scripts del directorio `arduino/`. Recordá configurar la localización del proyecto (sketchbook) al directorio `arduino/` de este repositorio. Si preferís usar tu editor favorito, la herramienta `arduino-cli` también funciona muy bien. Podés encontrar instrucciones completas de cómo bajarla y usarla en https://github.com/arduino/arduino-cli. + +### esp32 Olimex + +Leer https://www.olimex.com/Products/IoT/ESP32/_resources/Arudino-ESP32.txt para compilar programas en arduino Ide + +Ejemplos básicos de programa que usa ethernet: +- https://raw.githubusercontent.com/espressif/arduino-esp32/1.0.3/libraries/WiFi/examples/ETH_LAN8720/ETH_LAN8720.ino +- https://github.com/OLIMEX/ESP32-POE/blob/master/SOFTWARE/ARDUINO/ESP32_PoE_Ethernet_Arduino/ESP32_PoE_Ethernet_Arduino.ino + +Nota: El MTU default de esp32 y UDP es 1500 bytes. Al mandar paquetes más grandes nada falla pero nunca llegan. diff --git a/arduino/esp32-ethernet/esp32-ethernet.ino b/arduino/esp32-ethernet/esp32-ethernet.ino new file mode 100644 index 00000000..b70663c2 --- /dev/null +++ b/arduino/esp32-ethernet/esp32-ethernet.ino @@ -0,0 +1,264 @@ +// This is needed to prevent horrible crashes of esp32 probably caused by ethernet interrupts when data arrives +#define FASTLED_ALLOW_INTERRUPTS 0 +//#define FASTLED_INTERRUPT_RETRY_COUNT 1 + +#include +#include +#include + + +// Ethernet and connection protocol stuff +static bool eth_connected = false; +bool connected = false; + +unsigned long lastPerfStatus = millis(); +unsigned long lastFrame = millis(); + +int frameCount = 0; +int count = 0; +byte lastC = 0; + +WiFiUDP udp; // create UDP object + + +// Protocol defined strings coupled with lights server +char StringPerf[] = "PERF"; +char StringAlive[] = "YEAH"; + + +// ============================================================================ +// COMPILE TIME CONFIG GOES HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +#define STRIP_NUM_LEDS 300 // EACH ONE of the two strips will have that many leds + +#define DATA_PIN 2 +#define DATA_PIN2 16 + +#define POWER_MILLIAMPS 1000 // Max combined mah power consumption by all the strips + +unsigned int localUdpPort = 2222; // Local port number + +// ============================================================================ + + +// LED strips data structures and logic + +constexpr byte ENCODING_RGB = 4; // The only supported encoding by this device + +int NUM_LEDS = STRIP_NUM_LEDS*2; // Total number of leds in all strips +char ledsBuffer[2 * 3 * 150 + 2]; // buffer to hold incoming packet + +// Define the array of leds +CRGB leds[STRIP_NUM_LEDS * 2]; + +void setup() { + Serial.begin(250000); + Serial.println("Serial connected"); + + // setupUDPConnection(2222, 2); // ESP32 ETH 2 + // setupUDPConnection(4444, 4); // ESP32 ETH 4 + + WiFi.onEvent(WiFiEvent); + ETH.begin(); + + // Static ethernet config. Comment out for automatic DHCP + ETH.config( + IPAddress(192, 168, 2, 101), + IPAddress(192, 168, 2, 1), + IPAddress(255, 255, 255, 0), + IPAddress(192, 168, 2, 1), + IPAddress(192, 168, 2, 1) + ); + + setupLeds(600, 2, 16); +} + + +void broadcastAlive() { + Serial.println("Broadcasting I exist..."); + + IPAddress remoteIp(255, 255, 255, 255); + + // Broadcast a metadata string with the form "YEAH = =..." + udp.beginPacket(remoteIp, localUdpPort); + udp.print(StringAlive); + udp.print(" leds="); + udp.print(STRIP_NUM_LEDS*2); + udp.print(" datapin1="); + udp.print(DATA_PIN); + udp.print(" datapin2="); + udp.print(DATA_PIN2); + udp.endPacket(); +} + +void broadcastPerf(int frames) { + IPAddress remoteIp(255, 255, 255, 255); + udp.beginPacket(remoteIp, localUdpPort); + udp.print(StringPerf); + String framesString = String(frames); + char frameChar[5]; + framesString.toCharArray(frameChar, 5); + udp.print(frameChar); + udp.endPacket(); + + Serial.print("Broadcasting PERF "); + Serial.println(frameChar); +} + +bool checkForNewUDPMsg(char packetBuffer[]) { + int packetSize = udp.parsePacket(); + + if (packetSize == 0) { + return false; + } + + // // For debugging + //Serial.print("Received packet of size "); + //Serial.println(packetSize); + + udp.read(packetBuffer, packetSize); + + byte c = packetBuffer[0]; + if (c - lastC > 1) { + Serial.print("Missed "); + Serial.print(c - lastC - 1, DEC); + Serial.print(" - packet #"); + Serial.println(c, DEC); + } + + // // For debugging + // if ((c % 50) == 0) { + // Serial.print("Received packet #"); + // Serial.println(c, DEC); + // } + + lastC = c; + + return true; +} + +void WiFiEvent(WiFiEvent_t event) +{ + switch (event) { + case SYSTEM_EVENT_ETH_START: + Serial.println("ETH Started"); + //set eth hostname here + ETH.setHostname("esp32-ethernet"); + break; + case SYSTEM_EVENT_ETH_CONNECTED: + Serial.println("ETH Connected"); + break; + case SYSTEM_EVENT_ETH_GOT_IP: + Serial.print("ETH MAC: "); + Serial.print(ETH.macAddress()); + Serial.print(", IPv4: "); + Serial.print(ETH.localIP()); + if (ETH.fullDuplex()) { + Serial.print(", FULL_DUPLEX"); + } + Serial.print(", "); + Serial.print(ETH.linkSpeed()); + Serial.println("Mbps"); + + // Start UDP port and set global eth_connected flag + udp.begin(localUdpPort); + Serial.print("Begin udp in port "); + Serial.print(localUdpPort); + Serial.println("."); + + eth_connected = true; + break; + case SYSTEM_EVENT_ETH_DISCONNECTED: + Serial.println("ETH Disconnected"); + eth_connected = false; + break; + case SYSTEM_EVENT_ETH_STOP: + Serial.println("ETH Stopped"); + eth_connected = false; + break; + default: + break; + } +} + +void setupLeds(int numLeds, int dataPin1, int dataPin2) +{ + NUM_LEDS = numLeds; + + FastLED.addLeds(leds, 0, STRIP_NUM_LEDS); + FastLED.addLeds(leds, STRIP_NUM_LEDS, STRIP_NUM_LEDS); + + FastLED.setMaxPowerInVoltsAndMilliamps(5, POWER_MILLIAMPS); + + FastLED.showColor(CRGB::Black); + + for (int i = 0; i < 2; i++) + { + leds[0 + i * STRIP_NUM_LEDS] = CRGB::Black; + leds[1 + i * STRIP_NUM_LEDS] = CRGB::Red; + leds[2 + i * STRIP_NUM_LEDS] = CRGB::Green; + leds[3 + i * STRIP_NUM_LEDS] = CRGB::Blue; + } + + FastLED.show(); +} + +void writeLedFrame(char data[], int offset) +{ + int chunk = data[0 + offset]; + int encoding = data[1 + offset]; + + int chunkOffset = chunk*300; + int ledsInPacket = NUM_LEDS; + if(ledsInPacket > 300) { + ledsInPacket = min(NUM_LEDS, chunkOffset + 300) - chunkOffset; + } + + if (encoding == ENCODING_RGB) + { + for (int i = 0; i < ledsInPacket; i++) + { + int r = data[2 + i * 3 + offset]; + int g = data[2 + 1 + i * 3 + offset]; + int b = data[2 + 2 + i * 3 + offset]; + + leds[i+chunkOffset].setRGB(r, g, b); + } + } + else + { + Serial.println("Unexpected encoding byte"); + } + + if(chunk == ceil(NUM_LEDS/300) - 1) { + FastLED.show(); + frameCount++; + lastFrame = millis();; + } +} + +void loop() { + if (!eth_connected) { + return; + } + + unsigned long nowMs = millis(); + if (nowMs - lastPerfStatus > 1000) { + lastPerfStatus = nowMs; + if (!connected) { + broadcastAlive(); + } else { + broadcastPerf(frameCount); + frameCount = 0; + } + } + + if (checkForNewUDPMsg(ledsBuffer)) { + writeLedFrame(ledsBuffer, 1); + connected = true; + } else { + if (nowMs - lastFrame > 2000) { + connected = false; + } + } +} diff --git a/server/package.json b/server/package.json index dc34ab5e..572d049a 100644 --- a/server/package.json +++ b/server/package.json @@ -23,6 +23,7 @@ "dgram": "^1.0.1", "lodash": "^4.17.15", "moment": "^2.24.0", + "nanotimer": "^0.3.15", "node-fetch": "^2.6.6", "performance-now": "^2.1.0", "pino": "^5.15.0", diff --git a/server/setups/conga.json b/server/setups/conga.json index 500ae342..19f21ceb 100644 --- a/server/setups/conga.json +++ b/server/setups/conga.json @@ -3,39 +3,13 @@ "shapeMapping": "conga", "lights": 645, "outputDevices": { - "serial1": { - "type": "serial", - "params": { - "numberOfLights": 45, - "baudRate": 1500000, - "devicePortWindows": "COM3", - "devicePortUnix": "/dev/ttyACM0" - } - }, - "serial2": { - "type": "serial", - "params": { - "numberOfLights": 300, - "baudRate": 1500000, - "devicePortWindows": "COM4", - "devicePortUnix": "/dev/ttyACM0" - } - }, - "serialTest": { - "type": "serial", - "params": { - "numberOfLights": 600, - "baudRate": 1500000, - "devicePortWindows": "COM5", - "devicePortUnix": "/dev/ttyACM0" - } - }, "udp": { - "type": "udp-wled", + "type": "udp-chunked", "params": { "numberOfLights": 300, - "ip": "192.168.0.177", - "udpPort": 21324 + "name": "", + "ip": "192.168.2.101", + "udpPort": 2222 } } }, @@ -44,7 +18,7 @@ "from": 0, "to":600, "baseIndex": 0, - "deviceName": "serialTest" + "deviceName": "udp" } ] } diff --git a/server/src/DeviceMultiplexer.js b/server/src/DeviceMultiplexer.js index 5d435be8..7a8e8315 100644 --- a/server/src/DeviceMultiplexer.js +++ b/server/src/DeviceMultiplexer.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const DeviceSerial = require('./devices/serial'); const DeviceUDP = require('./devices/udp'); const DeviceUDPWLED = require('./devices/udp-wled'); +const LightDeviceUDPChunked = require("./devices/udp-chunked"); function initDevicesFromConfig(outputDevices) { let devices = {}; @@ -19,6 +20,9 @@ function initDevicesFromConfig(outputDevices) { case 'udp-wled': device = new DeviceUDPWLED(params); break; + case 'udp-chunked': + device = new LightDeviceUDPChunked(params); + break; default: throw new Error(`Invalid device type: ${type}`); } @@ -91,7 +95,8 @@ module.exports = class DeviceMultiplexer { return { status: d.status, deviceId: d.deviceId, - lastFps: d.lastFps + lastFps: d.lastFps, + metadata: d.metadata || {} }; }) ); diff --git a/server/src/ProgramScheduler.js b/server/src/ProgramScheduler.js index f3e08720..c4081d26 100644 --- a/server/src/ProgramScheduler.js +++ b/server/src/ProgramScheduler.js @@ -2,6 +2,10 @@ const _ = require("lodash"); const ColorUtils = require("./light-programs/utils/ColorUtils"); const audioEmitter = require("./audioEmitter"); +let lastFlushTime = new Date().valueOf(); + +const NanoTimer = require('nanotimer'); + module.exports = class ProgramScheduler { constructor(program) { @@ -16,6 +20,7 @@ module.exports = class ProgramScheduler { this.startTime = Date.now(); this.config = config; this.draw = draw; + this.timer = new NanoTimer(); this.program.init(); @@ -25,32 +30,42 @@ module.exports = class ProgramScheduler { // TODO: find a way to remove this this.program.timeInMs = this.timeInMs; - let start = Date.now(); - - this.program.drawFrame( - colorsArray => draw(_.map(colorsArray, col => - ColorUtils.dim(col, this.config.globalBrightness))), - audioEmitter, - ); - - let drawingTimeMs = Date.now() - start; - let remainingTime = 1000 / this.config.fps - drawingTimeMs; - - if (drawingTimeMs > 20) { - console.log( - `Time tick took: ${drawingTimeMs}ms (${remainingTime}ms remaining)` - ); - } - // Schedule next frame for the remaing time considering how long it took to do the drawing - // We wait at least 5ms in order to throttle CPU to give room for IO, serial and other critical stuff - this.nextTickTimeout = setTimeout(frame, Math.max(5, remainingTime)); + + let startFrameTime = Date.now(); + + const flushFrameData = colorsArray => { + const endFrameTime = Date.now(); + let drawingTimeMs = endFrameTime - startFrameTime; + let frameLength = Math.round(1000 / this.config.fps); + let remainingTime = frameLength - (endFrameTime - lastFlushTime); + + if (drawingTimeMs > 10) { + console.log(`Time tick took: ${drawingTimeMs}ms (${remainingTime}ms remaining)`); + } + // Schedule next frame for the remaing time considering how long it took to do the drawing + // We wait at least 3ms in order to throttle CPU to give room for IO, serial and other critical stuff + this.timer.setTimeout(() => { + const now = Date.now().valueOf(); + if (Math.abs(now - lastFlushTime - frameLength) > 3) { + console.log(`${now - lastFlushTime}ms (render ${drawingTimeMs}ms, scheduled ${remainingTime}, took ${now - endFrameTime})`); + } + + clearInterval(this.nextTickTimeout); + lastFlushTime = now; + draw(_.map(colorsArray, col => ColorUtils.dim(col, this.config.globalBrightness))); + + frame(); + }, '', remainingTime + 'm'); + }; + + this.program.drawFrame(flushFrameData, audioEmitter); }; this.nextTickTimeout = setTimeout(frame, 1); } stop() { - clearTimeout(this.nextTickTimeout); + this.timer.clearTimeout(); } restart() { @@ -60,7 +75,7 @@ module.exports = class ProgramScheduler { } get config() { - return this.program.config; + return this.program.config; } set config(config) { @@ -72,3 +87,14 @@ module.exports = class ProgramScheduler { } } + +// var NanoTimer = require('nanotimer'); +// timer = new NanoTimer(); +// c = new Date().valueOf(); +// var log = () => { +// let now = new Date().valueOf(); +// let diff = now - c; +// console.log(diff); +// c = now; +// }; +// timer.setInterval(log,null, '16m') diff --git a/server/src/devices/base.js b/server/src/devices/base.js index edec5618..7f7a881a 100644 --- a/server/src/devices/base.js +++ b/server/src/devices/base.js @@ -22,6 +22,7 @@ exports.LightDevice = class LightDevice { this.STATUS_OFF = "off"; this.STATUS_CONNECTING = "connecting"; + this.STATUS_WAITING = "waiting"; this.STATUS_RUNNING = "running"; this.STATUS_ERROR = "error"; diff --git a/server/src/devices/encodings.js b/server/src/devices/encodings.js index a2b55fdc..da59cb3d 100644 --- a/server/src/devices/encodings.js +++ b/server/src/devices/encodings.js @@ -50,10 +50,27 @@ class VGAEncoder extends Encoder { } } +class RGBChunkedEncoder extends Encoder { + constructor(maxChunkSize) { + super(); + this.maxChunkSize = maxChunkSize; + } -class RGBEncoder extends Encoder { - writeHeader(lights) { - this.write([4]); + encode(lights) { + let chunkCount = Math.ceil(lights.length / this.maxChunkSize); + let chunks = []; + + for(let c= 0; c < chunkCount;c++) { + let buf = [4]; + + for (let i = c*this.maxChunkSize; i < Math.min(lights.length, (c+1)*this.maxChunkSize); i++) { + buf.push(lights[i][0], lights[i][1], lights[i][2]); + } + + chunks.push(buf); + } + + return chunks; } writePixel(pos, r, g, b) { @@ -61,6 +78,11 @@ class RGBEncoder extends Encoder { } } + +class RGBEncoder extends Encoder { + +} + class WLEDRGBEncoder extends RGBEncoder { writeHeader(lights) { // See https://kno.wled.ge/interfaces/udp-realtime/ @@ -99,5 +121,6 @@ module.exports = { RGBEncoder, VGAEncoder, RGB565Encoder, - WLEDRGBEncoder + WLEDRGBEncoder, + RGBChunkedEncoder } diff --git a/server/src/devices/udp-chunked.js b/server/src/devices/udp-chunked.js new file mode 100644 index 00000000..149e998a --- /dev/null +++ b/server/src/devices/udp-chunked.js @@ -0,0 +1,177 @@ +const dns = require('dns'); +const dgram = require("dgram"); +const now = require("performance-now"); +const logger = require("pino")({ prettyPrint: true }); + +const { LightDevice } = require("./base"); +const { RGBChunkedEncoder} = require("./encodings"); + +const RECONNECT_TIME = 3000; + +module.exports = class LightDeviceUDPChunked extends LightDevice { + constructor({ numberOfLights, ip, name, udpPort }) { + super(numberOfLights, "E " + (name || ip)); + + this.expectedIp = ip; + this.name = name; + if (name && !ip) { + dns.lookup(name, (err, address) => { + if (err) { + logger.info(`failed to resolve ${name}`); + } else { + logger.info(`resolved ${name} to ${address}`); + this.expectedIp = address; + } + }); + } + this.udpPort = udpPort; + + this.encoder = new RGBChunkedEncoder(300); + + this.freshData = false; + this.connected = false; + + this.packageCount = 0; + + this.setupCommunication(); + } + + sendNextFrame() { + if (this.connected && this.freshData) { + let chunks = this.encoder.encode(this.state); + + for(let c = 0;c < chunks.length;c++) { + const data = [this.packageCount % 256, c, ... chunks[c]] + this.flush(data); + this.packageCount++; + } + + this.freshData = false; + } + } + + handleArduinoData(data) { + if (data) { + data = data.trim(); + + if (data.startsWith("YEAH")) { + let [,leds, datapin1, datapin2] = data.match(/YEAH leds=(\w+) datapin1=(\w+) datapin2=(\w+)/) || []; + this.metadata = {leds, datapin1, datapin2}; + + logger.info("Reconnected "+JSON.stringify(this.metadata)); + this.updateStatus(this.STATUS_WAITING); + } else if (data.startsWith("PERF")) { + data = data.replace(/[^\w]+/gi, ""); + this.lastFps = parseInt(data.substring(4) || 0); + this.updateStatus(this.STATUS_RUNNING); + } else { + logger.info(`UNEXPECTED MSG'${data}'`); + } + } else { + logger.info(`No data received`); + } + + clearTimeout(this.reconnectTimeout); + + this.reconnectTimeout = setTimeout(() => { + this.connected = false; + this.metadata = {}; + this.lastFps = null; + this.updateStatus(this.STATUS_CONNECTING); + logger.info(`no data`); + }, RECONNECT_TIME); + } + + // Override parent + logDeviceState() { + if (this.status === this.STATUS_RUNNING) { + if (now() - this.lastPrint > 250) { + logger.info(`FPS: ${this.lastFps}`.green); + this.lastPrint = now(); + } + } + } + + name() { + return this.name || this.ip; + } + + flush(data) { + let payload = Buffer.from(data); + this.udpSocket.send( + payload, + 0, + payload.length, + this.remotePort, + this.remoteAddress, + (err) => { + if (err) { + this.handleError(err); + } + } + ); + } + + setupCommunication() { + this.udpSocket = dgram.createSocket("udp4"); + + this.udpSocket.on("listening", this.handleListening.bind(this)); + this.udpSocket.on("message", this.handleMessage.bind(this)); + this.udpSocket.on("error", this.handleError.bind(this)); + this.udpSocket.on("close", this.handleClose.bind(this)); + + this.udpSocket.bind(this.udpPort); + + setInterval(() => { + // Es UDP, no esperamos respuesta + if (this.connected) { + this.sendNextFrame(); + } + }, 16); + } + + handleListening() { + const address = this.udpSocket.address(); + this.updateStatus(this.STATUS_CONNECTING); + logger.info( + "UDP Server listening on " + address.address + ":" + address.port + ); + } + + handleMessage(message, remote) { + // logger.info(message.toString(), remote.address) + if (message.toString().startsWith("YEAH") && + !message.toString().endsWith(this.name)) { + logger.warn("UDP message came from %s, expected %s", + message.toString().substr(4), this.name); + return; + } else if (this.expectedIp && remote.address !== this.expectedIp) { + logger.warn("UDP message came from %s, expected %s", remote.address, this.expectedIp); + return; + } + + this.remotePort = remote.port; + this.remoteAddress = remote.address; + + if (!this.connected) { + logger.info(`Connected to ${this.remoteAddress}:${this.remotePort}`); + this.connected = true; + this.updateStatus(this.STATUS_WAITING); + } + + this.handleArduinoData(message.toString()); + } + + handleClose() { + this.handleError("socket closed. Falta manejarlo"); + } + + // open errors will be emitted as an error event + handleError(err) { + this.udpSocket.close(); + this.updateStatus(this.STATUS_ERROR); + logger.error("Error: " + err.message); + // Create socket again + setTimeout(() => this.setupCommunication(), 500); + } +}; diff --git a/server/src/index.js b/server/src/index.js index 5815ac94..1351f49c 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -18,22 +18,22 @@ require('./hackProgramReloadOnRestart')(controller); controller.start(); console.log('Available audio devices:\n', listDevices()); - -const audioInput = new AudioInput({deviceIndex: 1,}); -// audioInput.on('audioframe', audioEmitter.updateFrame.bind(audioEmitter)); - -const audioInput2 = new AudioInput({deviceIndex: 3,}); -audioInput2.on('audioframe', (frame) => { - audioEmitter.frame2 = frame; -}); -audioInput2.start(); - -// Second audio input test -audioInput.on('audioframe', (frame) => { - audioEmitter.currentFrame = {... frame, ... _.mapKeys(audioEmitter.frame2, (v,k) => 'mic2_'+k)}; - audioEmitter.ready = true; - audioEmitter.emit('audioframe', audioEmitter.currentFrame); -}); +// +const audioInput = new AudioInput({deviceIndex: 0}); +audioInput.on('audioframe', audioEmitter.updateFrame.bind(audioEmitter)); + +// const audioInput2 = new AudioInput({deviceIndex: 3,}); +// audioInput2.on('audioframe', (frame) => { +// audioEmitter.frame2 = frame; +// }); +// audioInput2.start(); +// +// // Second audio input test +// audioInput.on('audioframe', (frame) => { +// audioEmitter.currentFrame = frame; +// audioEmitter.ready = true; +// audioEmitter.emit('audioframe', audioEmitter.currentFrame); +// }); audioInput.start(); diff --git a/web/src/DevicesStatus.tsx b/web/src/DevicesStatus.tsx index b980a418..d023dbd6 100644 --- a/web/src/DevicesStatus.tsx +++ b/web/src/DevicesStatus.tsx @@ -1,3 +1,4 @@ +import _ from "lodash"; import React from "react"; import { Device } from "./types"; @@ -5,7 +6,17 @@ interface Props { devices: Device[]; } -export class DevicesStatus extends React.Component { +interface State { + showMetadata: boolean; +} + + +export class DevicesStatus extends React.Component { + constructor(props: Props) { + super(props); + this.state = {showMetadata: false}; + } + render() { const { devices } = this.props; @@ -14,23 +25,38 @@ export class DevicesStatus extends React.Component { case "running": return "success"; case "connecting": + case "waiting": return "warning"; case "error": return "danger"; } } - return ( -
- {devices.map(device => ( - - {device.deviceId} ({device.status.toUpperCase()}) {device.lastFps} - - ))} -
- ); + return
+ {devices.map(device => { + let extendedMetadata = {FPS: device.lastFps || '-'}; + if(this.state.showMetadata) { + extendedMetadata = {... extendedMetadata, ... device.metadata}; + } + + let metadata =
+ {_.map(extendedMetadata, (val, key) => + {key} {val} + )} +
; + + let statusText = device.status.toUpperCase(); + let status = null; + if(statusText !== 'RUNNING') { + status =
{statusText}
; + } + + return + {device.deviceId} + {status} + {metadata} + ; + })} +
; } } diff --git a/web/src/types.ts b/web/src/types.ts index fb708fdd..cb989665 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -19,6 +19,7 @@ export type CurrentProgramParameters = { } export interface Device { + metadata: any; deviceId: string; status: string; lastFps: number;