/ adafruit_dymoscale.py
adafruit_dymoscale.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2019 ladyada for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_dymoscale` 24 ================================================================================ 25 26 CircuitPython interface for DYMO scales. 27 28 29 * Author(s): ladyada 30 31 Implementation Notes 32 -------------------- 33 34 **Software and Dependencies:** 35 36 * Adafruit CircuitPython firmware for the supported boards: 37 https://github.com/adafruit/circuitpython/releases 38 """ 39 40 import time 41 from pulseio import PulseIn 42 from micropython import const 43 44 OUNCES = const(0x0B) # data in weight is in ounces 45 GRAMS = const(0x02) # data in weight is in grams 46 PULSE_WIDTH = 72.5 47 48 __version__ = "0.0.0-auto.0" 49 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DymoScale.git" 50 51 # pylint: disable=too-few-public-methods 52 class ScaleReading: 53 """Dymo Scale Data""" 54 55 units = None # what units we're measuring 56 stable = None # is the measurement stable? 57 weight = None # the weight! 58 59 60 class DYMOScale: 61 """Interface to a DYMO postal scale. 62 """ 63 64 def __init__(self, data_pin, units_pin, timeout=1.0): 65 """Sets up a DYMO postal scale. 66 :param ~pulseio.PulseIn data_pin: The data pin from the Dymo scale. 67 :param ~digitalio.DigitalInOut units_pin: The grams/oz button from the Dymo scale. 68 :param double timeout: The timeout, in seconds. 69 """ 70 self.timeout = timeout 71 # set up the toggle pin 72 self.units_pin = units_pin 73 # set up the dymo data pin 74 self.dymo = PulseIn(data_pin, maxlen=96, idle_state=True) 75 76 @property 77 def weight(self): 78 """Weight in grams""" 79 reading = self.get_scale_data() 80 if reading.units == OUNCES: 81 reading.weight *= 28.35 82 reading.units = GRAMS 83 return reading 84 85 def toggle_unit_button(self, switch_units=False): 86 """Toggles the unit button on the dymo. 87 :param bool switch_units: Simulates pressing the units button. 88 """ 89 toggle_times = 0 90 if switch_units: # press the button once 91 toggle_amt = 2 92 else: # toggle and preserve current unit state 93 toggle_amt = 4 94 while toggle_times < toggle_amt: 95 self.units_pin.value ^= 1 96 time.sleep(2) 97 toggle_times += 1 98 99 def _read_pulse(self): 100 """Reads a pulse of SPI data on a pin that corresponds to DYMO scale 101 output protocol (12 bytes of data at about 14KHz). 102 """ 103 timestamp = time.monotonic() 104 self.dymo.pause() 105 self.dymo.clear() 106 self.dymo.resume() 107 while len(self.dymo) < 35: 108 if (time.monotonic() - timestamp) > self.timeout: 109 raise RuntimeError( 110 "Timed out waiting for data - is the scale turned on?" 111 ) 112 self.dymo.pause() 113 114 def get_scale_data(self): 115 """Reads a pulse of SPI data and analyzes the resulting data. 116 """ 117 self._read_pulse() 118 bits = [0] * 96 # there are 12 bytes = 96 bits of data 119 bit_idx = 0 # we will count a bit at a time 120 bit_val = False # first pulses will be LOW 121 for i in range(len(self.dymo)): 122 if self.dymo[i] == 65535: # check for the pulse between transmits 123 break 124 num_bits = int( 125 self.dymo[i] / PULSE_WIDTH + 0.5 126 ) # ~14KHz == ~7.5us per clock 127 bit = 0 128 while bit < num_bits: 129 bits[bit_idx] = bit_val 130 bit_idx += 1 131 if bit_idx == 96: # we have read all the data we wanted 132 break 133 bit += 1 134 bit_val = not bit_val 135 data_bytes = [0] * 12 # alllocate data array 136 for byte_n in range(12): 137 the_byte = 0 138 for bit_n in range(8): 139 the_byte <<= 1 140 the_byte |= bits[byte_n * 8 + bit_n] 141 data_bytes[byte_n] = the_byte 142 # do some very basic data checking 143 if data_bytes[0] != 3 and data_bytes[0] != 2: 144 raise RuntimeError("Bad data capture") 145 if data_bytes[1] != 3 or data_bytes[7] != 4 or data_bytes[8] != 0x1C: 146 raise RuntimeError("Bad data capture") 147 if data_bytes[9] != 0 or data_bytes[10] or data_bytes[11] != 0: 148 raise RuntimeError("Bad data capture") 149 reading = ScaleReading() 150 # parse out the data_bytes 151 reading.stable = data_bytes[2] & 0x4 152 reading.units = data_bytes[3] 153 reading.weight = data_bytes[5] + (data_bytes[6] << 8) 154 if data_bytes[2] & 0x1: 155 reading.weight *= -1 156 if reading.units == OUNCES: 157 if data_bytes[4] & 0x80: 158 data_bytes[4] -= 0x100 159 reading.weight *= 10 ** data_bytes[4] 160 return reading