Add parcel dimensioner source

This commit is contained in:
Skylar Ittner 2025-03-23 17:00:38 -06:00
parent b181294f29
commit ab3d2435c9
3 changed files with 259 additions and 2 deletions

257
src/dimensioner.py Normal file
View File

@ -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)

View File

@ -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