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 = 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
|
Loading…
x
Reference in New Issue
Block a user