Add parcel dimensioner source
This commit is contained in:
parent
b181294f29
commit
ab3d2435c9
257
src/dimensioner.py
Normal file
257
src/dimensioner.py
Normal 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)
|
||||||
|
|
@ -87,8 +87,8 @@ print("Boot complete")
|
|||||||
|
|
||||||
|
|
||||||
# VID and PID of the USB device.
|
# VID and PID of the USB device.
|
||||||
VID = 0xF055 # USB Vendor ID
|
VID = 0x1209 # USB Vendor ID
|
||||||
PID = 0x9999 # USB Product ID
|
PID = 0xA001 # USB Product ID
|
||||||
MANU = "PostalPortal LLC" # USB manufacturer string
|
MANU = "PostalPortal LLC" # USB manufacturer string
|
||||||
PROD = "PostalPoint Kiosk Controller" # USB product string
|
PROD = "PostalPoint Kiosk Controller" # USB product string
|
||||||
INTERFACE = "PostalPoint" # Interface string
|
INTERFACE = "PostalPoint" # Interface string
|
Loading…
x
Reference in New Issue
Block a user