"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScannerReader = void 0;
const fs = __importStar(require("fs"));
const events_1 = require("events");
const log_1 = require("../log");
const serial_tty_1 = require("./serial-tty");
// Barcode scanner handler. Two read modes auto-selected from the device path:
//
//   1. HID-raw (/dev/hidraw*) — Linux only. Scanner is in default
//      HID-keyboard mode, the kernel binds it to a hidraw device, and we
//      parse 8-byte HID input reports (modifier + reserved + keycode + ...).
//      Keycode 0x28 (Enter) commits the buffer as a scan.
//
//   2. CDC-serial (/dev/cu.usbmodem*, /dev/cu.usbserial*, /dev/ttyUSB*,
//      /dev/ttyACM*, COM*) — works on macOS / Linux / Windows. Scanner has
//      been switched out of HID-keyboard mode into USB-COM (CDC) by scanning
//      the vendor's setup barcode. Data arrives as ASCII bytes terminated by
//      CR / LF / CRLF; we split on those boundaries and emit each non-empty
//      line as a scan. No modifier / keycode parsing needed.
//
// Why this matters: macOS / Windows don't expose HID devices as readable
// device files, so HID-mode scanners can't be agent-driven on those
// platforms. Switching to CDC mode (vendor setup barcode) makes them
// readable as a regular serial port — that's the supported path on Mac.
//
// Permissions on Linux: hidraw needs read access (root, input group, or
// udev rule). Serial devices need access too (dialout / uucp group on most
// distros, or root).
const HID_KEYMAP = {
    // [un-shifted, shifted]
    0x04: ['a', 'A'], 0x05: ['b', 'B'], 0x06: ['c', 'C'], 0x07: ['d', 'D'],
    0x08: ['e', 'E'], 0x09: ['f', 'F'], 0x0a: ['g', 'G'], 0x0b: ['h', 'H'],
    0x0c: ['i', 'I'], 0x0d: ['j', 'J'], 0x0e: ['k', 'K'], 0x0f: ['l', 'L'],
    0x10: ['m', 'M'], 0x11: ['n', 'N'], 0x12: ['o', 'O'], 0x13: ['p', 'P'],
    0x14: ['q', 'Q'], 0x15: ['r', 'R'], 0x16: ['s', 'S'], 0x17: ['t', 'T'],
    0x18: ['u', 'U'], 0x19: ['v', 'V'], 0x1a: ['w', 'W'], 0x1b: ['x', 'X'],
    0x1c: ['y', 'Y'], 0x1d: ['z', 'Z'],
    0x1e: ['1', '!'], 0x1f: ['2', '@'], 0x20: ['3', '#'], 0x21: ['4', '$'],
    0x22: ['5', '%'], 0x23: ['6', '^'], 0x24: ['7', '&'], 0x25: ['8', '*'],
    0x26: ['9', '('], 0x27: ['0', ')'],
    0x2c: [' ', ' '], 0x2d: ['-', '_'], 0x2e: ['=', '+'],
    0x2f: ['[', '{'], 0x30: [']', '}'], 0x31: ['\\', '|'],
    0x33: [';', ':'], 0x34: ["'", '"'], 0x36: [',', '<'],
    0x37: ['.', '>'], 0x38: ['/', '?'],
};
const KEY_ENTER = 0x28;
class ScannerReader extends events_1.EventEmitter {
    constructor(opts) {
        super();
        this.buffer = '';
        this.stream = null;
        this.closed = false;
        this.opts = opts;
        this.mode = pickMode(opts.path);
    }
    start() {
        try {
            // CR-terminated / unterminated readers (most Bluetooth scanners) need
            // the TTY in raw mode or the kernel buffers their data forever.
            if (this.mode === 'serial')
                (0, serial_tty_1.setSerialRaw)(this.opts.path);
            this.stream = fs.createReadStream(this.opts.path);
            this.stream.on('data', (chunk) => this.onChunk(chunk));
            this.stream.on('error', (e) => {
                log_1.log.error(`scanner ${this.opts.endpointName} error`, { msg: e.message });
                this.emit('error', e);
            });
            this.stream.on('close', () => {
                if (!this.closed)
                    this.emit('disconnected');
            });
            log_1.log.info(`scanner attached`, { endpoint: this.opts.endpointName, path: this.opts.path });
        }
        catch (e) {
            log_1.log.error(`scanner ${this.opts.endpointName} open failed`, { msg: e.message });
            throw e;
        }
    }
    stop() {
        this.closed = true;
        this.stream?.destroy();
    }
    onChunk(chunk) {
        if (this.mode === 'serial') {
            this.parseSerial(chunk);
        }
        else {
            this.parseHid(chunk);
        }
    }
    /**
     * CDC serial scanners emit ASCII bytes followed by a terminator (CR, LF,
     * or CRLF; vendor-configurable). Buffer the chunk, split on \r/\n, emit
     * each non-empty line as a scan. Most scanners default to CR but Tera
     * D5100 in USB-COM mode emits CRLF — splitting on either handles both
     * without configuration.
     */
    parseSerial(chunk) {
        this.buffer += chunk.toString('utf-8');
        let idx;
        while ((idx = this.buffer.search(/[\r\n]/)) !== -1) {
            const code = this.buffer.slice(0, idx).trim();
            // Eat both CR and LF if it's a CRLF terminator so we don't emit empty
            // strings from the second character.
            this.buffer = this.buffer.slice(idx + 1).replace(/^[\r\n]/, '');
            if (!code)
                continue;
            const min = this.opts.minLength ?? 4;
            if (code.length >= min)
                this.emit('scan', code);
        }
    }
    /**
     * HID-keyboard mode (/dev/hidraw*). 8-byte input reports: modifier byte +
     * reserved + up to 6 keycodes. Barcode scanners only emit one keycode at
     * a time. Enter (0x28) commits the buffer.
     */
    parseHid(chunk) {
        for (let i = 0; i + 7 < chunk.length; i += 8) {
            const modifier = chunk[i];
            const keycode = chunk[i + 2];
            if (keycode === 0)
                continue; // key-up event
            if (keycode === KEY_ENTER) {
                const code = this.buffer;
                this.buffer = '';
                const min = this.opts.minLength ?? 4;
                if (code.length >= min) {
                    this.emit('scan', code);
                }
                continue;
            }
            const map = HID_KEYMAP[keycode];
            if (!map)
                continue;
            const shift = (modifier & 0x22) !== 0; // L/R shift
            this.buffer += shift ? map[1] : map[0];
        }
    }
}
exports.ScannerReader = ScannerReader;
function pickMode(path) {
    // /dev/hidraw* is the only HID-raw device file we know how to parse.
    // Everything else (serial-like) goes through the line-based parser:
    //   /dev/cu.usbmodem*, /dev/cu.usbserial*  (macOS CDC)
    //   /dev/ttyUSB*, /dev/ttyACM*             (Linux serial)
    //   COM*                                   (Windows, future)
    if (/^\/dev\/hidraw\d+$/.test(path))
        return 'hid';
    return 'serial';
}
//# sourceMappingURL=scanner.js.map