/ 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