diff --git a/src/dimensioner.py b/src/dimensioner.py new file mode 100644 index 0000000..23ff8d7 --- /dev/null +++ b/src/dimensioner.py @@ -0,0 +1,257 @@ +# Copyright 2025 PostalPortal LLC and Netsyms Technologies 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. + +# +# Interface between an ultrasonic rangefinder and USB HID, for dimensioning packages. +# + +# HID report bytes to PC: +# 1. Status: 0x01 if fault detected, 0x02 if at zero, +# 0x03 if distance unstable, 0x04 if OK, 0x05 if reading under zero, +# 0x06 if outside max sensor range, 0x07 if below minimum range, +# 0x08 if bad command from host, 0x09 if distance too large to fit in two bytes, 0x00 for other errors. +# If status is 0x01, 0x06, 0x07, 0x08, 0x09, or 0x00, bytes 2-6 will be 0x00. +# 2. Units: 0x01 for mm (default on boot), 0x02 for cm, 0x03 for in, 0x04 for feet +# 3. Sign: 0x00 if measurement positive or zero, 0x01 if negative (under zero) +# 4. First byte of distance to target +# 5. Second byte of distance to target +# 6. Fractional part of distance, .00 to .99 (0x00 to 0x63) +# +# Distance bytes allow transmitting distances from 0.00 to 65535.99 (0x0000 0x00 to 0xffff 0x63) +# Sign byte also allows distances under zero, down to -65535 + +# HID report from PC: +# First byte: +# 0x00: Request re-zeroing (use currently read distance as the distance to the empty measuring platform) +# 0x01: Set units to mm +# 0x02: Set units to cm +# 0x03: Set units to in +# 0x04: Set units to ft +# 0xAC: Set ambient temperature for calibration to the following byte in degrees C (ignored on US-100 sensor) + +from sys import stdin, exit +import math +from machine import Pin, time_pulse_us +from utime import sleep, sleep_us +import usb.device +from micropython import const +from usb.device.hid import HIDInterface + +print("PostalPoint(r) Parcel Dimensioner") +print("Firmware version 1.0") + +# +# Hardware configuration +# +SENSOR_TYPE = "PING" # "PING" for Parallax PING))) or "US-100" for US-100 +SENSOR_MAX_MM = 3000 # Maximum range sensor supports and is accurate within +SENSOR_MIN_MM = 40 # Minimum range below which sensor is not accurate +SENSOR_PIN = 4 # Analog pin for PING sensor +SENSOR_TX = 4 # Digital pin for US-100 +SENSOR_RX = 5 # Digital pin for US-100 +ZERO_RANGE_MM = 3 # +/- mm away from zero before a non-zero value is transmitted +SAMPLE_SIZE = 10 # Number of times to sample distance per reading, averaging the results + +# +# USB configuration +# +VID = 0x1209 # USB Vendor ID +PID = 0xA002 # USB Product ID +MANU = "PostalPortal LLC" # USB manufacturer string +PROD = "PostalPoint Parcel Dimensioner" # USB product string +INTERFACE = "PostalPoint" # Interface string + +class USBHIDInterface(HIDInterface): + # Very basic synchronous USB keypad HID interface + + def __init__(self): + super().__init__( + bytes([ + 0x06, 0x69, 0xff, # Usage Page (Vendor Defined) + 0x09, 0x01, # Usage (Vendor Usage 1) + 0x19, 0x01, + 0x29, 0x01, + 0xa1, 0x01, # Collection (Application) + 0x15, 0x00, # Logical Minimum (0) + 0x26, 0xff, 0x00, # Logical Maximum (255) + 0x85, 0x02, # Report ID 2 + 0x09, 0x01, # Usage (Vendor Usage 1) + 0x95, 0x01, # Report Count (1 byte) + 0x75, 0x30, # Report Size (48 bits/6 bytes) + 0x81, 0x00, # Input (Data,Ary,Abs) + 0x85, 0x02, # Report ID 2 + 0x09, 0x01, # Usage (Vendor Usage 1) + 0x95, 0x01, # Report Count (1 bytes) + 0x75, 0x10, # Report Size (16 bits/2 bytes) + 0x91, 0x01, # Output (Data,Ary,Abs) + 0xc0 # End Collection + ]), + 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] == 0x00: + # Command to set zero distance + setZeroDistance() + elif report_data[0] > 0x00 and report_data[0] < 0x05: + # Set units to send measurements in + reportUnits = getUnitsFromByte(report_data[0]) + elif report_data[0] == 0xAC: + # Set ambient temperature in degrees C + ambientTempC = report_data[1] + else: + # Can't understand command + send_data(b'\x01\x08\x00\x00\x00\x00\x00') + + def send_data(self, data): + while self.busy(): + machine.idle() + 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) + +zeroDistance = 100 +reportUnits = "mm" +ambientTempC = 20 + + +def setZeroDistance(): + zeroDistance = getAverageDistanceMM() + +def convertMMToUnits(mm, units="in"): + if units == "mm": + return mm + elif units == "cm": + return mm / 10.0 + elif units == "in": + return mm / 25.4 + elif units == "ft": + return convertMMToUnits(mm, "in") / 12.0 + else: + return 0 + +def getDistanceMM(): + if SENSOR_TYPE == "PING": + pin = Pin(SENSOR_PIN, Pin.OUT) + pin.value(0) + sleep_us(5) + pin.value(1) + sleep_us(5) + pin.value(0) + pin = Pin(SENSOR_PIN, Pin.IN) + pulseDuration = time_pulse_us(pin, 1, timeout_us=500000) # Timeout after half a second + if pulseDuration < 0: + # -1 or -2 means we timed out waiting for the pulse + return pulseDuration + pulseDuration = pulseDuration / 2 # Convert round-trip time to one-way time + speedOfSoundMetersPerSecond = (331.5 + (0.6 * ambientTempC)) + speedOfSoundMMPerUS = speedOfSoundMetersPerSecond / 1000.0 + return pulseDuration * speedOfSoundMMPerUS + elif SENSOR_TYPE == "US-100": + pass # TODO + return 0 + +def getAverageDistanceMM(samples = SAMPLE_SIZE): + i = 0 + total = 0.0 + while i < samples: + total = total + getDistanceMM() + sleep_us(250) # PING datasheet says wait a minimum of 200us between measurements + return total / samples + +def getUnitsAsByte(units): + if units == "mm": + return 0x01 + elif units == "cm": + return 0x02 + elif units == "in": + return 0x03 + elif units == "ft": + return 0x04 + else: + return 0x00 + +def getUnitsFromByte(byte): + if byte == 0x01: + return "mm" + elif byte == 0x02: + return "cm" + elif byte == 0x03: + return "in" + elif byte == 0x04: + return "ft" + else: + return "mm" + +while True: + sleep(.25) + dist = getAverageDistanceMM() + if dist < 0: + # distance measurement timed out, ultrasonic ping never came back? + usbinterface.send_data(b'\x01\x01\x00\x00\x00\x00\x00') + continue + size = zeroDistance - dist + sizeInUnits = convertMMToUnits(size, reportUnits) + reportData = bytearray([]) + if size < SENSOR_MIN_MM: + # Under minimum accurate range + usbinterface.send_data(b'\x01\x07\x00\x00\x00\x00\x00') + continue + if size > SENSOR_MAX_MM: + # Above maximum accurate range + usbinterface.send_data(b'\x01\x06\x00\x00\x00\x00\x00') + continue + if abs(sizeInUnits) > 65535: + # Value too large to fit in available bytes + usbinterface.send_data(b'\x01\x09\x00\x00\x00\x00\x00') + continue + + negativeByte = 0x00 + statusByte = 0x04 + if size < 0: + negativeByte = 0x01 + statusByte = 0x05 + + if size < ZERO_RANGE_MM and size > ZERO_RANGE_MM * -1: + # Within zero range, send zero + usbinterface.send_data(b'\x01\x02' + bytes([getUnitsAsByte(reportUnits), negativeByte]) + b'\x00\x00\x00') + continue + + distanceBytes = math.floor(sizeInUnits).to_bytes(2, 'big') + round(sizeInUnits % 1 * 100).to_bytes(1, 'big') + + usbinterface.send_data(b'\x01' + bytes([statusByte, getUnitsAsByte(reportUnits), negativeByte]) + distanceBytes) + diff --git a/src/main-hid.py b/src/kiosk-hid.py similarity index 98% rename from src/main-hid.py rename to src/kiosk-hid.py index 4eae9a4..298ddc5 100644 --- a/src/main-hid.py +++ b/src/kiosk-hid.py @@ -87,8 +87,8 @@ print("Boot complete") # VID and PID of the USB device. -VID = 0xF055 # USB Vendor ID -PID = 0x9999 # USB Product ID +VID = 0x1209 # USB Vendor ID +PID = 0xA001 # USB Product ID MANU = "PostalPortal LLC" # USB manufacturer string PROD = "PostalPoint Kiosk Controller" # USB product string INTERFACE = "PostalPoint" # Interface string diff --git a/src/main.py b/src/kiosk-serial.py similarity index 100% rename from src/main.py rename to src/kiosk-serial.py