mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 16:12:38 +00:00
126 lines
7.6 KiB
JavaScript
126 lines
7.6 KiB
JavaScript
"use strict";
|
|
const assert = require("node:assert/strict");
|
|
const builder = require("../docs/_javascript/notification_builder.js");
|
|
|
|
assert.deepEqual(builder.pulse("50,300,40,20,500"), [50,300,40,20,500]);
|
|
for (const [time, on] of [[0,true],[49,true],[50,false],[349,false],[350,true],[390,false],[410,true],[910,false]])
|
|
assert.equal(builder.level(builder.pulse("50,300,40,20,500"), time), on);
|
|
for (const bad of ["0", "50,", "50,,20", "60001", "-5", "50 20", "1,1,1,1,1,1,1,1,1,1,1,1,1"])
|
|
assert.throws(() => builder.pulse(bad));
|
|
assert.equal(builder.melody("vip:d=8,o=5,b=120:c,c#,16p,4g.6").length, 4);
|
|
for (const bad of ["abc", "x:d=8,o=3,b=120:c", "x:d=8,o=5,b=0:c", "x:d=3,o=5,b=120:c",
|
|
"x:d=8,o=5,b=120:b#7", "x:d=8,o=5,b=120:c,", "x:d=8,o=5,b=120:c..", "x:d=8,o=5,b=120:p#",
|
|
"x:d=8,o=04,b=120:c", "x:d=8,o=5,b=120:c04", "x:d=8,o=5,b=120:c007", "x:d=08,o=5,b=120:c", "x:d=8,o=5,b=120:04c"])
|
|
assert.throws(() => builder.melody(bad));
|
|
for (const config of Object.values(builder.EXAMPLES)) {
|
|
const complete = {...config, id: config.kind === "contact" ? "01".repeat(32) : config.id};
|
|
const commands = builder.commands(complete);
|
|
assert.ok(commands.every(cmd => cmd.length + 4 <= 175));
|
|
const dm = builder.notificationText(complete);
|
|
assert.ok(dm.startsWith("!notify "));assert.ok(!dm.includes("gpio="));assert.ok(dm.length <= 159);
|
|
}
|
|
const vip = {...builder.EXAMPLES.vip, id: "01".repeat(32), remote: "on"};
|
|
assert.ok(builder.commands(vip).includes("set notify.remote contact:" + vip.id + " on"));
|
|
const room = {...vip, kind: "room"};
|
|
assert.ok(builder.commands(room).includes("set notify.remote room:" + room.id + " on"));
|
|
const longPost = {...vip, led: Array(9).fill("60000").join(","), repeat: "65535"};
|
|
assert.equal(builder.notificationText(longPost).length, 151);
|
|
assert.throws(() => builder.notificationText({...longPost, kind: "room"}), /150 characters for a room post/);
|
|
assert.throws(() => builder.commands({...builder.EXAMPLES.food, remote: "on"}));
|
|
assert.throws(() => builder.commands({...vip, id: "1234"}));
|
|
// Local examples must play before choosing a recipient. Saved rules still need a key.
|
|
assert.equal(builder.parseOutputs(builder.EXAMPLES.vip).notes.length, 4);
|
|
assert.throws(() => builder.commands(builder.EXAMPLES.vip), /full 64-digit/);
|
|
assert.throws(() => builder.parseOutputs({...vip, gpio: "64:50,50"}), /GPIO needs/);
|
|
assert.throws(() => builder.parseOutputs({...vip, screen: "invalid"}), /Screen must/);
|
|
// Verify the actual PCM file sent to the browser: note pitch, rests, and one cycle.
|
|
const wav = builder.melodyWav("test:d=4,o=5,b=120:a,p,a6");
|
|
const waveView = new DataView(wav.buffer), rate = waveView.getUint32(24, true);
|
|
assert.equal(new TextDecoder().decode(wav.slice(0, 4)), "RIFF");
|
|
assert.equal(new TextDecoder().decode(wav.slice(8, 12)), "WAVE");
|
|
assert.equal(new TextDecoder().decode(wav.slice(36, 40)), "data");
|
|
assert.equal(waveView.getUint32(4, true), wav.length - 8);
|
|
assert.equal(waveView.getUint16(20, true), 1); // PCM
|
|
assert.equal(waveView.getUint16(22, true), 1); // Mono
|
|
assert.equal(waveView.getUint16(34, true), 16);
|
|
assert.equal(waveView.getUint32(40, true), rate * 1.5 * 2);
|
|
assert.equal(wav.length, 44 + rate * 1.5 * 2);
|
|
function samples(from, to) {
|
|
return Array.from({length: Math.round((to - from) * rate)}, (_, i) => waveView.getInt16(44 + (Math.round(from * rate) + i) * 2, true));
|
|
}
|
|
function frequency(values) {
|
|
const crossings = values.slice(1).filter((sample, i) => sample > 0 && values[i] <= 0).length;
|
|
return crossings * rate / values.length;
|
|
}
|
|
const firstNote = samples(0.01, 0.49), lastNote = samples(1.01, 1.49);
|
|
assert.ok(Math.abs(frequency(firstNote) - 880) < 3);
|
|
assert.ok(Math.abs(frequency(lastNote) - 1760) < 3);
|
|
assert.ok(Math.sqrt(firstNote.reduce((sum, sample) => sum + sample * sample, 0) / firstNote.length) > 6000);
|
|
assert.ok(samples(0.5, 1).every(sample => sample === 0));
|
|
assert.equal(waveView.getInt16(44, true), 0);
|
|
assert.equal(waveView.getInt16(wav.length - 2, true), 0);
|
|
assert.equal(builder.melodyWav("off").length, 44);
|
|
assert.equal(builder.melodyWav("inherit").length, 44);
|
|
assert.throws(() => builder.melodyWav("invalid"));
|
|
// The browser warmup must preserve every sample, including the opening notes.
|
|
const warmedWav = builder.melodyWav("test:d=4,o=5,b=120:a,p,a6", 1000);
|
|
const leadBytes = rate * 2;
|
|
assert.equal(warmedWav.length, wav.length + leadBytes);
|
|
assert.ok(warmedWav.slice(44, 44 + leadBytes).every(byte => byte === 0));
|
|
assert.deepEqual(warmedWav.slice(44 + leadBytes), wav.slice(44));
|
|
assert.equal(new DataView(warmedWav.buffer).getUint32(40, true), warmedWav.length - 44);
|
|
assert.equal(builder.melodyWav("off", 1000).length, 44);
|
|
for (const lead of [-1, 2001, 0.5, NaN]) assert.throws(() => builder.melodyWav("off", lead));
|
|
console.log("Sound preview: audible PCM, note pitch, silence, duration, and recipient-free examples passed");
|
|
const encoded = builder.encodeCommand("notify.stop", "A7");
|
|
assert.equal(encoded[0], 60);assert.equal(encoded[3], 0x42);
|
|
assert.equal(new TextDecoder().decode(encoded.slice(4)), "A7|notify.stop");
|
|
const decoder = new builder.FrameDecoder();
|
|
const payload = Buffer.from("A7|OK - alerts stopped");
|
|
const framed = Uint8Array.from([62,payload.length+1,0,0x1d,...payload]);
|
|
assert.deepEqual(decoder.push(framed.slice(0, 2)), []);
|
|
assert.deepEqual(decoder.push(framed.slice(2, 8)), []);
|
|
const result = decoder.push(framed.slice(8));assert.equal(result.length, 1);
|
|
assert.equal(new TextDecoder().decode(result[0].slice(1)), "A7|OK - alerts stopped");
|
|
assert.deepEqual(new builder.FrameDecoder().push(Uint8Array.from([62,255,255,...framed])), [result[0]]);
|
|
assert.throws(() => builder.encodeCommand("x".repeat(173), "00"));
|
|
console.log("Notification builder: pulse, melody, examples, permissions, and fragmented USB frame tests passed");
|
|
|
|
async function transportTests() {
|
|
let incoming, requestedSignals, opened;
|
|
const seen = [];
|
|
const port = {
|
|
readable: new ReadableStream({start(controller) { incoming = controller; }}),
|
|
writable: new WritableStream({write(bytes) {
|
|
const text = new TextDecoder().decode(bytes.slice(4));
|
|
const tag = text.slice(0, 2), command = text.slice(3);
|
|
seen.push(command);
|
|
const frame = value => {
|
|
const body = new TextEncoder().encode(value);
|
|
return Uint8Array.from([62, body.length + 1, 0, 0x1d, ...body]);
|
|
};
|
|
incoming.enqueue(frame("zz|ignored unrelated reply"));
|
|
const reply = frame(tag + "|OK");
|
|
incoming.enqueue(reply.slice(0, 2));incoming.enqueue(reply.slice(2, 7));incoming.enqueue(reply.slice(7));
|
|
}}),
|
|
async open(options) { opened = options; },
|
|
async setSignals(signals) { requestedSignals = signals; },
|
|
async close() {}
|
|
};
|
|
const client = new builder.SerialClient(port);
|
|
await client.open();assert.equal(opened.baudRate, 115200);assert.equal(requestedSignals.dataTerminalReady, true);
|
|
const first = client.command("notify.test room:" + "02".repeat(32));
|
|
await assert.rejects(client.command("notify.stop"), /Wait for the current command/);
|
|
assert.equal(await first, "OK");
|
|
assert.equal(await client.command("notify.stop"), "OK");
|
|
assert.equal(seen.length, 2);await client.close();
|
|
// A failed write must reject promptly and clear the pending request.
|
|
const failed = new builder.SerialClient({
|
|
...port, readable: new ReadableStream(),
|
|
writable: new WritableStream({write() { throw new Error("Cable removed"); }})
|
|
});
|
|
await failed.open();await assert.rejects(failed.command("get notify"), /Cable removed/);
|
|
assert.equal(failed.pending, null);await failed.close();
|
|
}
|
|
transportTests().then(() => console.log("USB transport: tagged round trips, busy rejection, disconnect failure and clean close passed"), error => { console.error(error);process.exitCode = 1; });
|