diff --git a/README.md b/README.md index feffc18..3bd0c9c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # PostalPoint Hardware -This repository contains open source hardware and software for package drop-off lockers and parcel dimensioning. +This repository contains open source hardware and software for package drop-off lockers, parcel dimensioning, and barcode scanning. -Devices are built around the Pi Pico and MicroPython, and support both bidirectional USB HID and serial communication with a host computer. +Devices are built around the Pi Pico and MicroPython, and support bidirectional USB HID communication with a host computer. # Devices @@ -24,6 +24,23 @@ Dependencies: Install these MicroPython packages on the Pico: usb_device_hid, ss See source code in src/kiosk and schematics. +## Barcode Scanner + +WIP. + +Code to build a hardware middle layer between low-cost off-the-shelf barcode scanner engines (using TTL communication) and a PC, using the USB HID POS specification for barcode scanners. The HID POS barcode scanner protocol is superior to the normal keyboard emulation scanner behavior in every way, except that hardly anyone implements it. + +The current code identifies the Pico as a barcode scanner to the host computer and sends a "scan" every few seconds. Getting that to work was probably the hardest part. + +TODO: + +- [ ] Read TTL barcode data from a scan engine +- [ ] Host-to-Pico USB communication for feedback (unusable/unrecognized barcode, etc) +- [ ] Scan beeper +- [ ] Trigger button +- [ ] OLED display for scan status, info, feedback +- [ ] Host-to-Pico list of scan "modes": enumerated strings to display on the OLED, with a hardware button to cycle through them, and the Pico sends the selected mode ID along with the barcode data. The host also sends a report to the Pico when it wants to change the current mode. This allows the scanner and the PC software to stay in sync with what the user is doing. For example, PostalPoint shipping software has several screens, and different barcodes are expected to be scanned in each screen. The user can then switch screens by switching modes on the scanner, and vice versa, allowing the scanner to navigate the program. + # License See LICENSE.md. diff --git a/build/barcodescanner.sh b/build/barcodescanner.sh new file mode 100755 index 0000000..59cad54 --- /dev/null +++ b/build/barcodescanner.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +mkdir -p workspace/src/barcodescanner +cp ../src/barcodescanner/main.py workspace/src/barcodescanner/main.py +cp ../src/barcodescanner/manifest.py workspace/src/barcodescanner/manifest.py +cd workspace +git clone https://github.com/micropython/micropython.git --branch=master --depth=1 +cd micropython +make -C ports/rp2 clean +make -C ports/rp2 submodules +make -j 4 -C mpy-cross +cd ports/rp2 +make -j 4 FROZEN_MANIFEST=../../../../src/barcodescanner/manifest.py +cd ../../../../ +mkdir -p out +mv workspace/micropython/ports/rp2/build-RPI_PICO/firmware.uf2 out/barcodescanner.uf2 diff --git a/src/barcodescanner/main.py b/src/barcodescanner/main.py new file mode 100644 index 0000000..1cf130d --- /dev/null +++ b/src/barcodescanner/main.py @@ -0,0 +1,152 @@ +# 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. + + +from sys import stdin, exit +from _thread import start_new_thread +import machine +from machine import Pin, USBDevice +from utime import sleep +import usb.device +from micropython import const +from usb.device.hid import HIDInterface + +print("PostalPoint(r) Barcode Scanner") +print("Firmware version 0.0.1") + + +# VID and PID of the USB device. +VID = 0x1209 # USB Vendor ID +PID = 0xA003 # USB Product ID +MANU = "PostalPortal LLC" # USB manufacturer string +PROD = "PostalPoint Barcode Scanner" # USB product string +INTERFACE = "PostalPoint" # Interface string + +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, 0x02, # Report Count (2 bytes) + 0x06, 0x66, 0xFF, # Usage Page (Vendor Defined 0xFF66) + 0x09, 0x04, # Usage (0x04) + 0x09, 0x00, # Usage (0x00) + 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) + 0xC0, # End Collection (Application) + + ]), + set_report_buf=bytearray(1), + protocol=const(0x00), + interface_str=INTERFACE, + ) + + def on_set_report(self, report_data, _report_id, _report_type): + if report_data[0] == 0x50: + print("Entering firmware update mode, power cycle to undo. Goodbye for now!") + machine.bootloader() + + def send_data(self, data=None): + while self.busy(): + machine.idle() + if data is None: + return + else: + self.send_report(data) + +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 send_data(d): + print(d) + +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 createAndSendReports(barcodeData): + global usbinterface + chunks = split(barcodeData, 56) + for i, chunk in enumerate(chunks): + lastByte = b'\x01' + if len(chunks) - 1 == i: + lastByte = b'\x00' + report = b'\x02' + bytes([len(chunk)]) + b'\x00\x00\x00' + pad_data(chunk, 56) + b'\x00\x00' + lastByte + usbinterface.send_data(report) + return + + +try: + while True: + sleep(1) + #createAndSendReports(b'POSTALPOINTROCKS') + createAndSendReports(b'very long barcode please to help very long barcode please to help very long barcode please to help very long barcode please to help very long barcode please to help') + sleep(10) + + +except KeyboardInterrupt: # trap Ctrl-C input + terminateThread = True # signal second 'background' thread to terminate + exit() diff --git a/src/barcodescanner/manifest.py b/src/barcodescanner/manifest.py new file mode 100644 index 0000000..347f237 --- /dev/null +++ b/src/barcodescanner/manifest.py @@ -0,0 +1,4 @@ +# Build manifest for PostalPoint Kiosk Controller (USB HID) +include("$(MPY_DIR)/ports/rp2/boards/manifest.py") +require("usb-device-hid") +module("main.py")