# Copyright 2026 PostalPortal LLC
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that
# the following conditions are met:
#   1. Redistributions of source code must retain
#      the above copyright notice, this list of conditions
#      and the following disclaimer.
#   2. Redistributions in binary form must reproduce
#      the above copyright notice, this list of conditions
#      and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#   3. Neither the name of the copyright holder
#       nor the names of its contributors may be used
#       to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.

import machine
import usb.device
from usb.device.hid import HIDInterface
from config import *
from scanmode import setInhibitScan, setScanModeIndex, setModeList, setModeListIsSet, setModeID, queueNewListReportForProcessing, getCurrentModeID
from feedback import feedbackBuzzer, firmwareUpdateMessage
from watchdog import feedwatchdog
import time
from sys import stdout
from utime import sleep_ms

usbinterface = None

class USBHIDInterface(HIDInterface):
    def __init__(self):
        super().__init__(
            bytes([
                0x05, 0x8C,           # Usage Page (barcode scanner)
                0x09, 0x02,           # Usage (Barcode Scanner)
                0xA1, 0x01,           # Collection (Application) 
                0x09, 0x12,           #   Usage (Scanned Data Report)
                0xA1, 0x02,           #     Collection (Logical)
                0x85, 0x02,           #       Report ID 2
                0x15, 0x00,           #         Logical Minimum 0
                0x26, 0xFF, 0x00,     #         Logical Maximum 255
                0x75, 0x08,           #         Report Size (8 bits)
                0x95, 0x01,           #         Report Count (1 byte long)
                0x05, 0x01,           #           Usage Page 0x01 (Generic Desktop Ctrls)
                0x09, 0x3B,           #           Usage (Byte count)
                0x81, 0x02,           #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
                0x95, 0x03,           #         Report Count (3 bytes long)
                0x05, 0x8C,           #           Usage Page (barcode scanner)
                0x09, 0xFB,           #           Usage (0xFB, Symbology Identifier 1)
                0x09, 0xFC,           #           Usage (0xFC, Symbology Identifier 2)
                0x09, 0xFD,           #           Usage (0xFD, Symbology Identifier 3)
                0x81, 0x02,           #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
                0x95, 0x38,           #         Report Count (56 bytes long)
                0x05, 0x8C,           #           Usage Page (barcode scanner)
                0x09, 0xFE,           #           Usage 0xFE (Decoded Data)
                0x82, 0x02, 0x01,     #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Buffered Bytes)
                0x95, 0x01,           #         Report Count (1 byte)
                0x06, 0x66, 0xFF,     #           Usage Page (Vendor Defined 0xFF66, everyone does this)
                0x09, 0x04,           #           Usage (0x04)
                0x81, 0x02,           #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
                0x95, 0x01,           #         Report Count (1 byte)
                0x06, 0x69, 0xFF,     #           Usage Page (Vendor Defined 0xFF69
                0x09, 0x55,           #           Usage (0x55)
                0x81, 0x02,           #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
                0x05, 0x8C,           #         Usage Page (Bar Code Scanner Page)
                0x25, 0x01,           #           Logical Maximum (1)
                0x75, 0x01,           #           Report Size (1 bit)
                0x95, 0x08,           #           Report Count (8 * size = 1 byte total)
                0x09, 0xFF,           #           Usage (0xFF, Decode Data Continued)
                0x81, 0x02,           #           Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
                0xC0,                 #     End Collection (Logical)
                0x09, 0x14,           #   Usage (Trigger report)
                0xA1, 0x02,           #     Collection (Logical)
                0x85, 0x04,           #       Report ID (4, trigger report from host)
                0x15, 0x00,           #         Logical Minimum (0)
                0x25, 0x01,           #         Logical Maximum (1)
                0x75, 0x01,           #         Report Size (1)
                0x95, 0x08,           #         Report Count (8)
                0x09, 0x00,           #         Usage (0x00, filler bit)
                0x09, 0x5F,           #         Usage (0x5F, prevent read of barcodes)
                0x09, 0x60,           #         Usage (0x60, initiate barcode read)
                0x09, 0x00,           #         Usage (0x00, filler)
                0x09, 0x84,           #         Usage (0x84, sound powerup beep)
                0x09, 0x85,           #         Usage (0x85, sound error beep)
                0x09, 0x86,           #         Usage (0x86, sound success beep)
                0x09, 0x87,           #         Usage (0x87, sound not-on-file beep)
                0x91, 0x82,           #         Output (Data,Var,Abs,Volatile)
                0xC0,                 #     End Collection (Logical)
                0x06, 0x69, 0xFF,     #  Usage Page (Vendor 0xFF69)
                0x09, 0x30,           #   Usage (Vendor-defined: Configuration report for setting up scan modes)
                0xA1, 0x02,           #     Collection (Logical)
                0x85, 0x05,           #       Report ID (5)
                0x15, 0x00,           #         Logical Minimum (0)
                0x26, 0xFF, 0x00,     #         Logical Maximum (255)
                0x75, 0x08,           #         Report Size (8 bits)
                0x95, 0x01,           #         Report Count (1 byte)
                0x05, 0x01,           #           Usage Page (Generic Desktop)
                0x09, 0x3B,           #           Usage (Byte Count)
                0x91, 0x02,           #           Output (Data,Var,Abs)
                0x95, 0x3E,           #         Report Count (62 bytes)
                0x06, 0x69, 0xFF,     #           Usage Page (Vendor Defined 0xFF69)
                0x09, 0x05,           #           Usage (0x05, Config ASCII Data)
                0x92, 0x02, 0x01,     #           Output (Data,Var,Abs,Buffered Bytes)
                0xC0,                 #     End Collection (Logical)
                0x06, 0x69, 0xFF,     #  Usage Page (Vendor 0xFF69)
                0x09, 0x31,           #   Usage (Vendor-defined: Scan mode switch)
                0xA1, 0x02,           #     Collection (Logical)
                0x85, 0x55,           #       Report ID (0x55)
                0x15, 0x00,           #         Logical Minimum (0)
                0x26, 0xFF, 0x00,     #         Logical Maximum (255)
                0x75, 0x08,           #         Report Size (8 bits)
                0x95, 0x01,           #         Report Count (1 byte)
                0x06, 0x69, 0xFF,     #           Usage Page (Vendor 0xFF69)
                0x09, 0x55,           #           Usage (0x55)
                0x91, 0x02,           #           Output (Data,Var,Abs)
                0xC0,                 #     End Collection (Logical)
                0x06, 0x69, 0xFF,     #  Usage Page (Vendor 0xFF69)
                0x09, 0xF1,           #   Usage (Vendor-defined: Firmware update trigger)
                0xA1, 0x02,           #     Collection (Logical)
                0x85, 0xF1,           #       Report ID (0xF1)
                0x15, 0x00,           #         Logical Minimum (0)
                0x26, 0xFF, 0x00,     #         Logical Maximum (255)
                0x75, 0x08,           #         Report Size (8 bits)
                0x95, 0x01,           #         Report Count (1 byte)
                0x06, 0x69, 0xFF,     #           Usage Page (Vendor 0xFF69)
                0x09, 0xF1,           #           Usage (0xF1)
                0x91, 0x02,           #           Output (Data,Var,Abs)
                0xC0,                 #     End Collection (Logical)
                0xC0,                 # End Collection (Application)
                #
                # NOTES:
                #
                # Report ID 2 is 63-bytes to the host.
                #  Byte 0 is the number of characters of barcode data to send in this report.
                #  Bytes 1, 2, and 3 are the AIM barcode identifier string. Nobody really uses this, some cheap scanners fudge it to QR code.
                #  Bytes 4 through 60 are the barcode data.  Padded with NUL (0x00) bytes if the barcode is shorter than the available space.
                #  Bytes 61 and 62 are vendor-specific, this isn't part of the HID spec but everyone does it (see report 5 below for what we do)
                #    because they're copying everyone else. Honeywell uses it for another symbology ID.
                #  Byte 63 is 0x01 (b00000001) if the barcode data couldn't fit in a single report;
                #    it means more reports are coming right away with the rest of the data.
                #    If byte 63 is 0x00 it means there's no more barcode to send and the host can process it now.
                #
                # Report ID 4 is a single byte from the host: 76543210
                #  If pos. 7, 6, 5, or 4 is 1, the host wants us to make a particular beep sound/light indication.
                #  If pos. 1 is 1, host wants us to not send any barcode data or do any scans.
                #  If pos. 2 is 1, host wants us to start reading for barcodes without the user pressing a button.
                #
                # Report ID 5 is 63 bytes from the host to this device.
                #  This is a custom report for this specific device.
                #  The first byte is the data length.
                #  The rest of it is a "list" of ASCII strings, delimited with a NUL byte.
                #  The strings are displayed on our display and can be switched between by the user.
                #  The index of the string (as sent by the host) is sent as the second vendor byte in report 2.
                #  This allows the user to select a scan context/UI mode in the program, and the program
                #  can switch to it automatically when a barcode is scanned in that mode.
                #
                # Report ID 0x55 is a single byte from the host.
                #  It tells us to change our scan context to a string index from the most recent Report 5 list,
                #  overriding the user's last selection. This is for when the user changed modes in software,
                #  to keep the state in sync between the scanner and PC.
                #
                # Report ID 0xF1 triggers the Pi Pico to switch to firmware update mode when the byte sent is also 0xF1.
                #  The Pico will re-enumerate and appear as a USB flash drive so the firmware file can be dropped in.
            ]),
            set_report_buf=bytearray(64),
            protocol=0x00,
            interface_str=INTERFACE,
        )

    def on_set_report(self, report_data, report_id, report_type):
        #createAndSendBarcodeReports(bytes([report_id, report_type, report_data[0], report_data[1], reverse_byte(report_data[1])]))
        
        if report_id == 0x04:
            report_byte = report_data[1]
            # HID POS trigger report
            # Disable/enable scanning based on bit flag
            setInhibitScan(report_byte >> 1 & 1 == 1)
            
            if (report_byte >> 7 & 1 == 1):
                # not-on-file beep, 10000000
                feedbackBuzzer("NAF")
            elif (report_byte >> 6 & 1 == 1):
                # success beep, 01000000
                feedbackBuzzer("OK")
            elif (report_byte >> 5 & 1 == 1):
                # error beep, 00100000
                feedbackBuzzer("ERR")
            elif (report_byte >> 4 & 1 == 1):
                # powerup beep, 00010000
                feedbackBuzzer("POW")
            
        elif report_id == 0xF1 and report_data[1] == 0xF1:
            # Enter firmware update mode
            firmwareUpdateMessage()
            machine.bootloader()
        
        elif report_id == 0x05:
            # Got mode list from host
            # Don't process it here though since we're running in an interrupt
            queueNewListReportForProcessing(report_data[:])
            
        elif report_id == 0x55:
            # Host says to switch modes
            setModeID(report_data[1])

    def send_data(self, data=None):
        if data is None:
            return True
        start = time.ticks_ms()
        while self.busy():
            # Wait for queue to open up, but drop this report on timeout 
            # because the host isn't paying attention to us and this will deadlock if we wait forever
            if time.ticks_diff(time.ticks_ms(), start) > 10:
                return False
            machine.idle()
        self.send_report(data)
        return True
            
def initUSBHID():
    global usbinterface, USBHIDInterface
    usbinterface = USBHIDInterface()
    usb.device.get().active(False)
    usb.device.get().config(usbinterface, builtin_driver=True, manufacturer_str=MANU, product_str=PROD, id_vendor=VID, id_product=PID)
    usb.device.get().active(True)

def split(data, n):
    return [data[i:i+n] for i in range(0, len(data), n)]

def pad_data(chunk, size):
    if len(chunk) >= size:
        return chunk[:size]
    return chunk + b'\x00' * (size - len(chunk))

def createAndSendBarcodeReports(barcodeData):
    global usbinterface
    barcodeSymbologyAIM = b'\x00\x00\x00'
    if AIM_PREFIX:
        barcodeSymbologyAIM = bytes(barcodeData[:3])
        barcodeData = barcodeData[3:]
    chunks = split(barcodeData, 56)
    for i, chunk in enumerate(chunks):
        isLastReport = len(chunks) - 1 == i
        lastByte = b'\x01'
        if isLastReport:
            lastByte = b'\x00'
        report = b'\x02' + bytes([len(chunk)]) + barcodeSymbologyAIM + pad_data(chunk, 56) + b'\x00' + bytes([getCurrentModeID()]) + lastByte
        if USBHID_ENABLED:
            if usbinterface.send_data(report):
                if not isLastReport:
                    sleep_ms(50) # Wait a few milliseconds before sending the next report so it won't get dropped
                print(report)
            else:
                break # Stop sending barcode over USB, the host didn't get this chunk but it might get the next and only have half a barcode
        feedwatchdog()
    # Write barcode data to serial out for non-HID system compatibility
    try:
        stdout.buffer.write(barcodeData.rstrip(b"\r\n")) # Strip any trailing newlines before sending our own
        feedwatchdog()
        stdout.buffer.write(b"\r\n")
    except:
        pass
