UNPKG

vtally

Version:

An affordable and reliable Tally Light that works via WiFi based on NodeMCU / ESP8266. Supports multiple video mixers.

330 lines (329 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const nodemcu_tool_1 = __importDefault(require("nodemcu-tool")); const TallyDevice_1 = __importDefault(require("./TallyDevice")); const TallySettingsIni_1 = __importDefault(require("./TallySettingsIni")); const tmp_promise_1 = __importDefault(require("tmp-promise")); const fs_1 = require("fs"); const baudRate = 115200; const fileName = "tally-settings.ini"; let mutex = false; const tryToAquireMutex = () => { if (!mutex) { mutex = true; return true; } else { return false; } }; class NodeMcuConnector { // injectable for easier testing constructor(nodemcu = nodemcu_tool_1.default) { this.nodemcu = nodemcu; this.nodemcu.onError((error) => { console.error(error); }); } withMutex(fn) { return new Promise((resolve, reject) => { const interval = setInterval(() => { const mutexAquired = tryToAquireMutex(); if (mutexAquired) { clearInterval(interval); if (this.nodemcu.isConnected()) { console.warn("Serial terminal was not closed by previous process."); this.nodemcu.disconnect(); } resolve(true); } }, 100); }) .then(() => { return fn(); }) .finally(() => { mutex = false; }); } static async getLocalFiles() { let dirName = __dirname + "/../../esp8266"; // path in release package const files = await fs_1.promises.readdir(dirName).catch(e => { dirName = __dirname + "/../../../tally/out"; // path during development return fs_1.promises.readdir(dirName); }); console.debug(`Files from ${dirName} will be flashed.`); const filteredFiles = files.filter(file => file.endsWith(".lc") || file.endsWith(".lua")); return Promise.all(filteredFiles.map(async (file) => { const stats = await fs_1.promises.stat(dirName + "/" + file); return { fileName: file, filePath: `${dirName}/${file}`, fileSize: stats.size, }; })); } static async doFilesNeedUpdate(filesOnNodemcu) { const localFiles = await NodeMcuConnector.getLocalFiles(); return localFiles.some(localFile => { return filesOnNodemcu.every(nodeMcuFile => nodeMcuFile.name !== localFile.fileName || nodeMcuFile.size !== localFile.fileSize); }); } sleep(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } // gracefull connection that retries a few times async connect(path) { await this.nodemcu.connect(path, baudRate, false); // check connection does not always work the first time, so we try it multiple times if necessary let retries = 3; while (true) { try { return await this.nodemcu.checkConnection(); } catch (e) { if (retries === 0) { throw e; } await this.sleep(100); } retries--; } } async execute(idempotentCommand) { let retries = 3; while (true) { try { const foo = await this.nodemcu.execute(`${idempotentCommand}; print("ok")`); if (foo === null || !foo.response) { throw new Error("Did not get a response for executing the command."); } if (foo.response.toString().includes("error")) { throw new Error(foo.response.toString()); } if (!foo.response.toString().includes("ok")) { throw new Error(`response did not include an "ok": ${foo.response.toString()}`); } return foo; } catch (e) { if (retries === 0) { throw e; } await this.sleep(100); } retries--; } } async getDevice() { const tallyDevice = new TallyDevice_1.default(); const localFiles = await NodeMcuConnector.getLocalFiles(); const updatePossible = localFiles.length > 0; if (!updatePossible) { tallyDevice.update = "not-available"; } try { return await this.withMutex(async () => { const list = await this.nodemcu.listDevices(); const device = list[0]; if (device) { tallyDevice.path = device.path; tallyDevice.vendorId = device.vendorId; tallyDevice.productId = device.productId; await this.connect(device.path); const deviceInfo = await this.nodemcu.deviceInfo(); tallyDevice.chipId = deviceInfo.chipID; tallyDevice.flashId = deviceInfo.flashID; tallyDevice.nodeMcuVersion = deviceInfo.version; tallyDevice.nodeMcuModules = deviceInfo.modules; const fsinfo = await this.nodemcu.fsinfo(); if (updatePossible) { tallyDevice.update = await NodeMcuConnector.doFilesNeedUpdate(fsinfo.files) ? "updateable" : "up-to-date"; } const settingsFileExists = fsinfo.files.some(file => file.name === fileName); if (settingsFileExists) { const res = await this.nodemcu.download(fileName); tallyDevice.tallySettings = new TallySettingsIni_1.default(res.toString()); } } return tallyDevice; }); } catch (e) { tallyDevice.errorMessage = e; return tallyDevice; } finally { if (this.nodemcu && this.nodemcu.isConnected()) { await this.nodemcu.disconnect(); } } } async program(path, onProgress) { const files = await NodeMcuConnector.getLocalFiles(); const progress = { inititalizeDone: false, connectionDone: false, filesUploaded: 0, filesTotal: files.length, rebootDone: false, allDone: false, error: false, }; onProgress(progress); try { await this.withMutex(async () => { progress.inititalizeDone = true; onProgress(progress); await this.connect(path); progress.connectionDone = true; onProgress(progress); for (const file of files) { await this.saveFileUpload(file.fileName, file.filePath); progress.filesUploaded = progress.filesUploaded + 1; onProgress(progress); } await this.hardReset(path); progress.rebootDone = true; onProgress(progress); progress.allDone = true; onProgress(progress); }); } catch (e) { console.error(`programming failed because of: ${e}`); progress.error = true; onProgress(progress); return false; } finally { if (this.nodemcu && this.nodemcu.isConnected()) { this.nodemcu.disconnect(); } } } async writeTallySettingsIni(path, settingsIniString, onProgress) { const settingsIni = new TallySettingsIni_1.default(settingsIniString); const progress = { tallyName: settingsIni.getTallyName(), inititalizeDone: false, connectionDone: false, uploadDone: false, rebootDone: false, allDone: false, error: false, }; onProgress(progress); try { if (!settingsIni.getTallyName()) { throw new Error(`Exeptected ${fileName} to contain a tally.name, but it was empty.`); } if (!settingsIni.getStationSsid()) { throw new Error(`Exeptected ${fileName} to contain a station ssid, but it was empty.`); } if (!settingsIni.getHubIp()) { throw new Error(`Exeptected ${fileName} to contain a hub.ip name, but it was empty.`); } await this.withMutex(async () => { progress.inititalizeDone = true; onProgress(progress); await this.connect(path); progress.connectionDone = true; onProgress(progress); await this.saveContentUpload(fileName, settingsIniString); progress.uploadDone = true; onProgress(progress); await this.hardReset(path); progress.rebootDone = true; onProgress(progress); progress.allDone = true; onProgress(progress); }); return true; } catch (e) { console.error(`${fileName} upload failed because of:`, e); progress.error = true; onProgress(progress); return false; } finally { if (this.nodemcu && this.nodemcu.isConnected()) { this.nodemcu.disconnect(); } } } async hardReset(path) { await this.nodemcu.hardreset(); await this.nodemcu.disconnect(); await new Promise(resolve => { setTimeout(resolve, 1000); }); // sleep await this.connect(path); await new Promise(resolve => { setTimeout(resolve, 3000); }); // sleep const failTimeout = setTimeout(() => { throw new Error("Could not connect to NodeMCU after hardreset."); }, 10000); let rebootSuccess = false; while (!rebootSuccess) { try { await this.nodemcu.checkConnection(); rebootSuccess = true; } catch (e) { rebootSuccess = false; } } clearTimeout(failTimeout); } /** * uploads content via nodemcu-tool * * @param filePath the file path on nodemcu * @param content the file content */ async saveContentUpload(filePath, content) { const { path: tmpPath, cleanup: tmpCleanup } = await tmp_promise_1.default.file({}); try { await fs_1.promises.writeFile(tmpPath, content); await this.saveFileUpload(filePath, tmpPath); } finally { tmpCleanup(); } } /** * uploads a file via nodemcu-tool and does some verification * * @param remoteFilePath the file path on nodemcu * @param localFilePath the local file path */ async saveFileUpload(remoteFilePath, localFilePath) { if (!this.nodemcu.isConnected()) { throw new Error("Expected to have an already established connection to NodeMCU, but did not."); } const copyFileName = remoteFilePath + ".swp"; try { await this.nodemcu.upload(localFilePath, copyFileName, {}, () => { }); await this.sleep(1000); const gotContent = await this.nodemcu.download(copyFileName); const localContent = await fs_1.promises.readFile(localFilePath).then(buffer => buffer.toString()); if (gotContent.toString() !== localContent) { throw new Error(`Uploaded file does not match downloaded file. Expected file size of ${localContent.length}, but got ${gotContent.length}`); } // rename file await this.removeFileIfExists(remoteFilePath); await this.execute(`file.rename("${copyFileName}", "${remoteFilePath}")`); } finally { await this.removeFileIfExists(copyFileName); } } async removeFileIfExists(filePath) { return this.execute(`if file.exists("${filePath}") then file.remove("${filePath}") end`); } } exports.default = NodeMcuConnector;