"use strict"; const assert = require("assert"); const fs = require("fs"); const path = require("path"); const vm = require("vm"); const decoder = require("../docs/_javascript/telemetry_decoder.js"); let passed = 0; function test(name, callback) { callback(); passed += 1; process.stdout.write(`ok ${passed} - ${name}\n`); } function assertDecoderError(callback, pattern) { assert.throws( callback, (error) => error instanceof decoder.TelemetryDecodeError && pattern.test(error.message) ); } const TEMPERATURE_PACKET = "3E00545442311122334455667788800092651E0008000102354A4E5082"; const VOLTAGE_PACKET = "3E00545642311122334455667788800092651E000800010264C8FEFFDC"; const EXTERNAL_VOLTAGE_PACKET = "3E00495642311122334455667788800092651E00020800000004019026927109C427107FFF"; function pack15(codes) { const output = Buffer.alloc(Math.ceil((codes.length * 15) / 8)); let bitOffset = 0; codes.forEach((code) => { for (let bit = 14; bit >= 0; bit -= 1, bitOffset += 1) { if (code & (1 << bit)) { output[Math.floor(bitOffset / 8)] |= 1 << (7 - (bitOffset % 8)); } } }); return output; } function externalPage(epoch, channel, codes) { const header = Buffer.alloc(8); header[0] = 0x14; header.writeUInt32LE(epoch, 1); header[5] = 30; header[6] = codes.length; header[7] = channel; return `> ${Buffer.concat([header, pack15(codes)]).toString("base64")}`; } function externalPacket(epoch, channel, codes) { const header = Buffer.alloc(20); header.write("IVB1", 0, "ascii"); Buffer.from("1122334455667788", "hex").copy(header, 4); header.writeUInt32LE(epoch, 12); header.writeUInt16LE(30, 16); header[18] = channel; header[19] = codes.length; return Buffer.concat([ Buffer.from("3e00", "hex"), header, pack15(codes), ]).toString("hex").toUpperCase(); } // Run the real browser entry point. The DOM boundary starts with the empty host // shipped in Markdown and exposes only controls inserted by the actual script. function browserFixture(readyState = "complete") { const fields = new Map(); const examples = new Map(); const downloads = []; const blobs = new Map(); const events = new Map(); function element(tagName = "div", attributes = {}) { const handlers = new Map(); return { tagName, value: "", checked: false, hidden: false, textContent: "", children: [], classList: { add() {} }, getAttribute(name) { return attributes[name] ?? null; }, addEventListener(name, callback) { handlers.set(name, callback); }, emit(name, event = {}) { assert.ok(handlers.has(name), `Missing ${name} handler on ${tagName}`); return handlers.get(name)(event); }, append(...children) { this.children.push(...children); }, appendChild(child) { this.children.push(child); }, replaceChildren(...children) { this.children = children; }, focus() { this.focused = true; }, click() { downloads.push({ filename: this.download, blob: blobs.get(this.href) }); }, remove() {}, }; } const root = element(); let markup = "
Enable JavaScript on the documentation website to use the decoder.
"; Object.defineProperty(root, "innerHTML", { get() { return markup; }, set(value) { markup = value; fields.clear(); examples.clear(); for (const match of value.matchAll(/<([a-z][a-z0-9]*)\b([^>]*)>/gi)) { const attributes = Object.fromEntries( Array.from(match[2].matchAll(/([\w-]+)="([^"]*)"/g), (item) => [item[1], item[2]]) ); const node = element(match[1], attributes); node.hidden = /(?:^|\s)hidden(?:\s|$)/.test(match[2]); if (attributes["data-role"]) fields.set(attributes["data-role"], node); if (attributes["data-telemetry-example"]) { examples.set(attributes["data-telemetry-example"], node); } } }, }); root.querySelector = (selector) => { const match = selector.match(/^\[data-role='([^']+)'\]$/); return match ? fields.get(match[1]) ?? null : null; }; root.querySelectorAll = (selector) => selector === "[data-telemetry-example]" ? Array.from(examples.values()) : []; const context = { atob: globalThis.atob, URLSearchParams, location: { search: "" }, Blob: class { constructor(parts, options) { this.parts = parts; this.options = options; } }, URL: { createObjectURL(blob) { const url = `blob:${blobs.size}`; blobs.set(url, blob); return url; }, revokeObjectURL(url) { blobs.delete(url); }, }, document: { readyState, body: element("body"), createElement: element, querySelector: (selector) => selector === "[data-telemetry-decoder]" ? root : null, addEventListener: (name, callback) => events.set(name, callback), }, }; vm.runInNewContext(fs.readFileSync( path.join(__dirname, "../docs/_javascript/telemetry_decoder.js"), "utf8" ), context); return { root, fields, examples, downloads, boot() { events.get("DOMContentLoaded")(); } }; } test("GitHub readers can open the hosted decoder from its packet section", () => { const documentation = fs.readFileSync( path.join(__dirname, "../docs/telemetry_decoder.md"), "utf8" ); const hostedDecoder = "https://mikecarper.github.io/MeshCore/telemetry_decoder/#decode-a-packet"; const introduction = documentation.split("## Send raw telemetry packets")[0]; assert.ok(introduction.includes( 'class="meshcore-hosted-doc-link">/); const configuration = fs.readFileSync(path.join(__dirname, "../mkdocs.yml"), "utf8"); assert.match(configuration, /^\s+- _javascript\/telemetry_decoder\.js\?v=\S+$/m, "A versioned script prevents the old cached script from using the new empty host."); }); test("browser initialization creates the form and wires every example", () => { const fixture = browserFixture(); assert.match(fixture.root.innerHTML, /