All files board_calculation_serial.ts

100% Statements 205/205
100% Branches 19/19
100% Functions 6/6
100% Lines 205/205

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 2071x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 17x 17x 17x 17x 17x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 26x 24x 24x 24x 24x 24x 24x 24x 26x 216x 216x 24x 24x 24x 24x 26x 1x 1x 1x 1x 23x 23x 26x 8x 8x 8x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 184x 184x 23x 23x 1x 1x 1x 1x 1x 1x 1x 1x 1x 24x 24x 192x 192x 24x 24x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 92x 92x 92x 92x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x    
/**
 * @fileoverview Binary Frame Processor — Renesas Power Profiler (Node.js backend)
 *
 * Parses 12-byte binary frames received over the serial port from the
 * Renesas EK-RA6B1 power-profiler board.
 *
 * Frame layout (12 bytes, big-endian):
 * ```
 *  Byte  0      : 0xAA  (start marker 1)
 *  Byte  1      : 0x55  (start marker 2)
 *  Bytes 2–9   : 64-bit counter (big-endian unsigned) — identifies each frame
 *                 Inner layout reused for channel data:
 *                   Bytes 2–3: CH1 (Int16 BE — low-range current sense)
 *                   Bytes 4–5: CH2 (Int16 BE — high-range current sense)
 *                   Bytes 6–7: CH3 (Int16 BE — voltage channel 1)
 *                   Bytes 8–9: CH4 (Int16 BE — voltage channel 2)
 *  Byte 10      : Digital channels D0–D7 (packed bits, LSB = D0)
 *  Byte 11      : CRC = sum of bytes 2–10, masked to 8 bits
 * ```
 *
 * The `FrameProcessor` class is stateful: it tracks cumulative frame counts
 * and CRC errors to detect missing packets. Call `reset()` when a new
 * acquisition session starts to clear all counters.
 *
 * @module board_calculation_serial
 */
 
import logger from "../utils/logger";
 
/**
 * Result returned by `processCounterFrame()` when the frame is valid.
 * All multi-byte fields are returned as signed BigInt to accommodate the
 * full signed 16-bit ADC range without data loss.
 */
export interface FrameResult {
    /** Sequential frame counter (total valid frames since last reset) */
    frame: number;
    /** CH1 ADC reading — low-range current sense amplifier (Int16 BE) */
    ch1: bigint;
    /** CH2 ADC reading — high-range current sense amplifier (Int16 BE) */
    ch2: bigint;
    /** CH3 ADC reading — voltage channel 1 (Int16 BE) */
    ch3: bigint;
    /** CH4 ADC reading — voltage channel 2 (Int16 BE) */
    ch4: bigint;
    /** Frame time index (equals `frame` — used for X-axis in graphs) */
    time: number;
    /** Digital channel D0 state (0 or 1) */
    d0: number;
    d1: number;
    d2: number;
    d3: number;
    d4: number;
    d5: number;
    d6: number;
    /** Digital channel D7 state (0 or 1) */
    d7: number;
    success: true;
}
 
/** Returned by `processCounterFrame()` when CRC validation fails or the packet is too short */
export interface FrameResultFail {
    success: false;
}
 
/**
 * Stateful parser for the Renesas EK-RA6B1 12-byte binary frame format.
 *
 * Maintains running statistics about the stream:
 *  - Total frames received
 *  - CRC error count
 *  - Missing-packet count (gap detection via counter comparison)
 *
 * Thread-safety: not thread-safe. All calls must come from the same Node.js
 * event-loop tick (the serial-port `data` event handler is single-threaded).
 */
export class FrameProcessor {
    private counter_value: bigint = 0n;
    private counter_last_value: bigint | null = null;
    private counter_total_frames: number = 0;
    private counter_crc_errors: number = 0;
    private crc_errors_last_sec: number = 0;
    private counter_missing_packets: number = 0;
    private missed_frames_last_sec: number = 0;
 
    /**
     * Attempts to parse one 12-byte frame starting at `offset` inside `pkt`.
     *
     * Validates the CRC (sum of bytes 2–10 & 0xFF) before extracting channel
     * values. Gap detection compares the 64-bit frame counter against the
     * previous valid counter to identify dropped packets.
     *
     * @param pkt    - Raw byte array containing one or more frames.
     * @param offset - Byte index of the first `0xAA` start marker in `pkt`.
     * @returns `FrameResult` on success, `FrameResultFail` on CRC error or
     *          insufficient data.
     */
    public processCounterFrame(pkt: Uint8Array, offset: number = 0): FrameResult | FrameResultFail {
        if (pkt.length < offset + 12) { return { success: false }; }
 
        const counter = this.readBigUInt64BE(pkt, offset + 2);
        this.counter_value = counter;
 
        // Validate CRC: sum of payload bytes 2–10, truncated to 8 bits
        const crc = pkt[offset + 11];
        let expected_crc = 0;
        for (let i = offset + 2; i <= offset + 10; i++) {
            expected_crc += pkt[i];
        }
        expected_crc &= 0xFF;
 
        this.counter_total_frames += 1;
 
        if (crc !== expected_crc) {
            this.counter_crc_errors += 1;
            this.crc_errors_last_sec += 1;
            return { success: false };
        }
 
        // Detect gaps in the sequential frame counter
        if (this.counter_last_value !== null && counter !== this.counter_last_value + 1n) {
            this.counter_missing_packets += 1;
            this.missed_frames_last_sec += 1;
        }
        this.counter_last_value = counter;
 
        // Extract ADC channel values (Int16 big-endian)
        const ch1 = this.readInt16BE(pkt, offset + 2);
        const ch2 = this.readInt16BE(pkt, offset + 4);
        const ch3 = this.readInt16BE(pkt, offset + 6);
        const ch4 = this.readInt16BE(pkt, offset + 8);
 
        // Digital channel byte: each bit represents one channel state
        const dig = pkt[offset + 10];
        const frame = this.counter_total_frames;
        const time = this.counter_total_frames;
        const [d0, d1, d2, d3, d4, d5, d6, d7] = this.decodeBits(dig);
 
        return {
            frame, ch1, ch2, ch3, ch4, time,
            d0, d1, d2, d3, d4, d5, d6, d7,
            success: true,
        };
    }
 
    /**
     * Unpacks a single byte into 8 individual bit values (LSB first).
     *
     * @param dig - Packed digital-channel byte from the frame.
     * @returns Array of 8 numbers, each `0` or `1`, index 0 = D0 (LSB).
     */
    private decodeBits(dig: number): number[] {
        const bits: number[] = [];
        for (let i = 0; i < 8; i++) {
            bits.push((dig >> i) & 1);
        }
        return bits;
    }
 
    /**
     * Reads an unsigned 64-bit big-endian integer as a native `bigint`.
     *
     * @param data   - Source byte array.
     * @param offset - Start byte index.
     * @returns Parsed unsigned 64-bit value.
     */
    private readBigUInt64BE(data: Uint8Array, offset: number): bigint {
        let result = 0n;
        for (let i = 0; i < 8; i++) {
            result = (result << 8n) + BigInt(data[offset + i]);
        }
        return result;
    }
 
    /**
     * Reads a signed 16-bit big-endian integer and returns it as a `bigint`.
     *
     * Two's-complement conversion is applied for values ≥ 0x8000.
     *
     * @param data  - Source byte array.
     * @param index - Start byte index.
     * @returns Signed 16-bit value in the range −32768–32767.
     */
    private readInt16BE(data: Uint8Array, index: number): bigint {
        const val = (data[index] << 8) | data[index + 1];
        const returnValue = val >= 0x8000 ? val - 0x10000 : val;
        return BigInt(returnValue);
    }
 
    /**
     * Resets all frame counters and error statistics.
     *
     * Must be called before each new acquisition session to prevent stale
     * counter values from appearing on the graph X-axis.
     */
    public reset() {
        this.counter_value = 0n;
        this.counter_last_value = null;
        this.counter_total_frames = 0;
        this.counter_crc_errors = 0;
        this.crc_errors_last_sec = 0;
        this.counter_missing_packets = 0;
        this.missed_frames_last_sec = 0;
    }
}