/ adafruit_as726x.py
adafruit_as726x.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2017 Dean Miller 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_as726x`
 24  ====================================================
 25  
 26  Driver for the AS726x spectral sensors
 27  
 28  * Author(s): Dean Miller
 29  """
 30  
 31  import time
 32  from adafruit_bus_device.i2c_device import I2CDevice
 33  from micropython import const
 34  
 35  try:
 36      import struct
 37  except ImportError:
 38      import ustruct as struct
 39  
 40  __version__ = "0.0.0-auto.0"
 41  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_AS726x.git"
 42  
 43  _AS726X_ADDRESS = const(0x49)
 44  
 45  _AS726X_HW_VERSION = const(0x00)
 46  _AS726X_FW_VERSION = const(0x02)
 47  _AS726X_CONTROL_SETUP = const(0x04)
 48  _AS726X_INT_T = const(0x05)
 49  _AS726X_DEVICE_TEMP = const(0x06)
 50  _AS726X_LED_CONTROL = const(0x07)
 51  
 52  # for reading sensor data
 53  _AS7262_V_HIGH = const(0x08)
 54  _AS7262_V_LOW = const(0x09)
 55  _AS7262_B_HIGH = const(0x0A)
 56  _AS7262_B_LOW = const(0x0B)
 57  _AS7262_G_HIGH = const(0x0C)
 58  _AS7262_G_LOW = const(0x0D)
 59  _AS7262_Y_HIGH = const(0x0E)
 60  _AS7262_Y_LOW = const(0x0F)
 61  _AS7262_O_HIGH = const(0x10)
 62  _AS7262_O_LOW = const(0x11)
 63  _AS7262_R_HIGH = const(0x12)
 64  _AS7262_R_LOW = const(0x13)
 65  
 66  _AS7262_V_CAL = const(0x14)
 67  _AS7262_B_CAL = const(0x18)
 68  _AS7262_G_CAL = const(0x1C)
 69  _AS7262_Y_CAL = const(0x20)
 70  _AS7262_O_CAL = const(0x24)
 71  _AS7262_R_CAL = const(0x28)
 72  
 73  # hardware registers
 74  _AS726X_SLAVE_STATUS_REG = const(0x00)
 75  _AS726X_SLAVE_WRITE_REG = const(0x01)
 76  _AS726X_SLAVE_READ_REG = const(0x02)
 77  _AS726X_SLAVE_TX_VALID = const(0x02)
 78  _AS726X_SLAVE_RX_VALID = const(0x01)
 79  
 80  _AS7262_VIOLET = const(0x08)
 81  _AS7262_BLUE = const(0x0A)
 82  _AS7262_GREEN = const(0x0C)
 83  _AS7262_YELLOW = const(0x0E)
 84  _AS7262_ORANGE = const(0x10)
 85  _AS7262_RED = const(0x12)
 86  _AS7262_VIOLET_CALIBRATED = const(0x14)
 87  _AS7262_BLUE_CALIBRATED = const(0x18)
 88  _AS7262_GREEN_CALIBRATED = const(0x1C)
 89  _AS7262_YELLOW_CALIBRATED = const(0x20)
 90  _AS7262_ORANGE_CALIBRATED = const(0x24)
 91  _AS7262_RED_CALIBRATED = const(0x28)
 92  
 93  _AS726X_NUM_CHANNELS = const(6)
 94  
 95  _COLOR_REGS = (
 96      _AS7262_VIOLET,
 97      _AS7262_BLUE,
 98      _AS7262_GREEN,
 99      _AS7262_YELLOW,
100      _AS7262_ORANGE,
101      _AS7262_RED,
102  )
103  _COLOR_REGS_CALIBRATED = (
104      _AS7262_VIOLET_CALIBRATED,
105      _AS7262_BLUE_CALIBRATED,
106      _AS7262_GREEN_CALIBRATED,
107      _AS7262_YELLOW_CALIBRATED,
108      _AS7262_ORANGE_CALIBRATED,
109      _AS7262_RED_CALIBRATED,
110  )
111  
112  # pylint: disable=too-many-instance-attributes
113  # pylint: disable=too-many-public-methods
114  # pylint: disable=invalid-name
115  # pylint: disable=no-else-return
116  # pylint: disable=inconsistent-return-statements
117  
118  
119  class AS726x:
120      """AS726x spectral sensor base class.
121  
122         """
123  
124      MODE_0 = 0b00
125      """Continuously gather samples of violet, blue, green and yellow. Orange and red are skipped
126         and read zero."""
127  
128      MODE_1 = 0b01
129      """Continuously gather samples of green, yellow, orange and red. Violet and blue are skipped
130         and read zero."""
131  
132      MODE_2 = 0b10  # default
133      """Continuously gather samples of all colors"""
134  
135      ONE_SHOT = 0b11
136      """Gather a single sample of all colors and then stop"""
137  
138      GAIN = (1, 3.7, 16, 64)
139  
140      INDICATOR_CURRENT_LIMITS = (1, 2, 4, 8)
141  
142      DRIVER_CURRENT_LIMITS = (12.5, 25, 50, 100)
143  
144      def __init__(self):
145          self._driver_led = False
146          self._indicator_led = False
147          self._driver_led_current = AS726x.DRIVER_CURRENT_LIMITS.index(12.5)
148          self._indicator_led_current = AS726x.INDICATOR_CURRENT_LIMITS.index(1)
149          self._conversion_mode = AS726x.MODE_2
150          self._integration_time = 0
151          self._gain = AS726x.GAIN.index(1)
152          self.buf2 = bytearray(2)
153  
154          # reset device
155          self._virtual_write(_AS726X_CONTROL_SETUP, 0x80)
156  
157          # wait for it to boot up
158          time.sleep(1)
159  
160          # try to read the version reg to make sure we can connect
161          version = self._virtual_read(_AS726X_HW_VERSION)
162  
163          # TODO: add support for other devices
164          if version != 0x40:
165              raise ValueError(
166                  "device could not be reached or this device is not supported!"
167              )
168  
169          self.integration_time = 140
170          self.conversion_mode = AS726x.MODE_2
171          self.gain = 64
172  
173      @property
174      def driver_led(self):
175          """True when the driver LED is on. False otherwise."""
176          return self._driver_led
177  
178      @driver_led.setter
179      def driver_led(self, val):
180          val = bool(val)
181          if self._driver_led == val:
182              return
183          self._driver_led = val
184          enable = self._virtual_read(_AS726X_LED_CONTROL)
185          enable &= ~(0x1 << 3)
186          self._virtual_write(_AS726X_LED_CONTROL, enable | (val << 3))
187  
188      @property
189      def indicator_led(self):
190          """True when the indicator LED is on. False otherwise."""
191          return self._indicator_led
192  
193      @indicator_led.setter
194      def indicator_led(self, val):
195          val = bool(val)
196          if self._indicator_led == val:
197              return
198          self._indicator_led = val
199          enable = self._virtual_read(_AS726X_LED_CONTROL)
200          enable &= ~(0x1)
201          self._virtual_write(_AS726X_LED_CONTROL, enable | val)
202  
203      @property
204      def driver_led_current(self):
205          """The current limit for the driver LED in milliamps. One of:
206  
207             - 12.5 mA
208             - 25 mA
209             - 50 mA
210             - 100 mA"""
211          return self._driver_led_current
212  
213      @driver_led_current.setter
214      def driver_led_current(self, val):
215          if val not in AS726x.DRIVER_CURRENT_LIMITS:
216              raise ValueError("Must be 12.5, 25, 50 or 100")
217          if self._driver_led_current == val:
218              return
219          self._driver_led_current = val
220          state = self._virtual_read(_AS726X_LED_CONTROL)
221          state &= ~(0x3 << 4)
222          state = state | (AS726x.DRIVER_CURRENT_LIMITS.index(val) << 4)
223          self._virtual_write(_AS726X_LED_CONTROL, state)
224  
225      @property
226      def indicator_led_current(self):
227          """The current limit for the indicator LED in milliamps. One of:
228  
229             - 1 mA
230             - 2 mA
231             - 4 mA
232             - 8 mA"""
233          return self._indicator_led_current
234  
235      @indicator_led_current.setter
236      def indicator_led_current(self, val):
237          if val not in AS726x.INDICATOR_CURRENT_LIMITS:
238              raise ValueError("Must be 1, 2, 4 or 8")
239          if self._indicator_led_current == val:
240              return
241          self._indicator_led_current = val
242          state = self._virtual_read(_AS726X_LED_CONTROL)
243          state &= ~(0x3 << 1)
244          state = state | (AS726x.INDICATOR_CURRENT_LIMITS.index(val) << 4)
245          self._virtual_write(_AS726X_LED_CONTROL, state)
246  
247      @property
248      def conversion_mode(self):
249          """The conversion mode. One of:
250  
251             - `MODE_0`
252             - `MODE_1`
253             - `MODE_2`
254             - `ONE_SHOT`"""
255          return self._conversion_mode
256  
257      @conversion_mode.setter
258      def conversion_mode(self, val):
259          val = int(val)
260          assert self.MODE_0 <= val <= self.ONE_SHOT
261          if self._conversion_mode == val:
262              return
263          self._conversion_mode = val
264          state = self._virtual_read(_AS726X_CONTROL_SETUP)
265          state &= ~(val << 2)
266          self._virtual_write(_AS726X_CONTROL_SETUP, state | (val << 2))
267  
268      @property
269      def gain(self):
270          """The gain for the sensor"""
271          return self._gain
272  
273      @gain.setter
274      def gain(self, val):
275          if val not in AS726x.GAIN:
276              raise ValueError("Must be 1, 3.7, 16 or 64")
277          if self._gain == val:
278              return
279          self._gain = val
280          state = self._virtual_read(_AS726X_CONTROL_SETUP)
281          state &= ~(0x3 << 4)
282          state |= AS726x.GAIN.index(val) << 4
283          self._virtual_write(_AS726X_CONTROL_SETUP, state)
284  
285      @property
286      def integration_time(self):
287          """The integration time in milliseconds between 2.8 and 714 ms"""
288          return self._integration_time
289  
290      @integration_time.setter
291      def integration_time(self, val):
292          val = int(val)
293          if not 2.8 <= val <= 714:
294              raise ValueError("Out of supported range 2.8 - 714 ms")
295          if self._integration_time == val:
296              return
297          self._integration_time = val
298          self._virtual_write(_AS726X_INT_T, int(val / 2.8))
299  
300      def start_measurement(self):
301          """Begin a measurement.
302  
303             This will set the device to One Shot mode and values will not change after `data_ready`
304             until `start_measurement` is called again or the `conversion_mode` is changed."""
305          state = self._virtual_read(_AS726X_CONTROL_SETUP)
306          state &= ~(0x02)
307          self._virtual_write(_AS726X_CONTROL_SETUP, state)
308  
309          self.conversion_mode = self.ONE_SHOT
310  
311      def read_channel(self, channel):
312          """Read an individual sensor channel"""
313          return (self._virtual_read(channel) << 8) | self._virtual_read(channel + 1)
314  
315      def read_calibrated_value(self, channel):
316          """Read a calibrated sensor channel"""
317          val = bytearray(4)
318          val[0] = self._virtual_read(channel)
319          val[1] = self._virtual_read(channel + 1)
320          val[2] = self._virtual_read(channel + 2)
321          val[3] = self._virtual_read(channel + 3)
322          return struct.unpack("!f", val)[0]
323  
324      @property
325      def data_ready(self):
326          """True if the sensor has data ready to be read, False otherwise"""
327          return (self._virtual_read(_AS726X_CONTROL_SETUP) >> 1) & 0x01
328  
329      @property
330      def temperature(self):
331          """The temperature of the device in Celsius"""
332          return self._virtual_read(_AS726X_DEVICE_TEMP)
333  
334      @property
335      def violet(self):
336          """Calibrated violet (450nm) value"""
337          return self.read_calibrated_value(_AS7262_VIOLET_CALIBRATED)
338  
339      @property
340      def blue(self):
341          """Calibrated blue (500nm) value"""
342          return self.read_calibrated_value(_AS7262_BLUE_CALIBRATED)
343  
344      @property
345      def green(self):
346          """Calibrated green (550nm) value"""
347          return self.read_calibrated_value(_AS7262_GREEN_CALIBRATED)
348  
349      @property
350      def yellow(self):
351          """Calibrated yellow (570nm) value"""
352          return self.read_calibrated_value(_AS7262_YELLOW_CALIBRATED)
353  
354      @property
355      def orange(self):
356          """Calibrated orange (600nm) value"""
357          return self.read_calibrated_value(_AS7262_ORANGE_CALIBRATED)
358  
359      @property
360      def red(self):
361          """Calibrated red (650nm) value"""
362          return self.read_calibrated_value(_AS7262_RED_CALIBRATED)
363  
364      @property
365      def raw_violet(self):
366          """Raw violet (450nm) 16-bit value"""
367          return self.read_channel(_AS7262_VIOLET)
368  
369      @property
370      def raw_blue(self):
371          """Raw blue (500nm) 16-bit value"""
372          return self.read_channel(_AS7262_BLUE)
373  
374      @property
375      def raw_green(self):
376          """Raw green (550nm) 16-bit value"""
377          return self.read_channel(_AS7262_GREEN)
378  
379      @property
380      def raw_yellow(self):
381          """Raw yellow (570nm) 16-bit value"""
382          return self.read_channel(_AS7262_YELLOW)
383  
384      @property
385      def raw_orange(self):
386          """Raw orange (600nm) 16-bit value"""
387          return self.read_channel(_AS7262_ORANGE)
388  
389      @property
390      def raw_red(self):
391          """Raw red (650nm) 16-bit value"""
392          return self.read_channel(_AS7262_RED)
393  
394      def _virtual_read(self, addr):
395          raise NotImplementedError("Must be implemented.")
396  
397      def _virtual_write(self, addr, value):
398          raise NotImplementedError("Must be implemented.")
399  
400  
401  class AS726x_I2C(AS726x):
402      """AS726x spectral sensor via I2C.
403  
404         :param ~busio.I2C i2c_bus: The I2C bus connected to the sensor
405         """
406  
407      def __init__(self, i2c_bus, address=_AS726X_ADDRESS):
408          self.i2c_device = I2CDevice(i2c_bus, address)
409          super().__init__()
410  
411      def _read_u8(self, command):
412          """read a single byte from a specified register"""
413          buf = self.buf2
414          buf[0] = command
415          with self.i2c_device as i2c:
416              i2c.write(buf, end=1)
417              i2c.readinto(buf, end=1)
418          return buf[0]
419  
420      def __write_u8(self, command, abyte):
421          """Write a command and 1 byte of data to the I2C device"""
422          buf = self.buf2
423          buf[0] = command
424          buf[1] = abyte
425          with self.i2c_device as i2c:
426              i2c.write(buf)
427  
428      def _virtual_read(self, addr):
429          """read a virtual register"""
430          while True:
431              # Read slave I2C status to see if the read buffer is ready.
432              status = self._read_u8(_AS726X_SLAVE_STATUS_REG)
433              if (status & _AS726X_SLAVE_TX_VALID) == 0:
434                  # No inbound TX pending at slave. Okay to write now.
435                  break
436          # Send the virtual register address (setting bit 7 to indicate a pending write).
437          self.__write_u8(_AS726X_SLAVE_WRITE_REG, addr)
438          while True:
439              # Read the slave I2C status to see if our read data is available.
440              status = self._read_u8(_AS726X_SLAVE_STATUS_REG)
441              if (status & _AS726X_SLAVE_RX_VALID) != 0:
442                  # Read data is ready.
443                  break
444          # Read the data to complete the operation.
445          data = self._read_u8(_AS726X_SLAVE_READ_REG)
446          return data
447  
448      def _virtual_write(self, addr, value):
449          """write a virtual register"""
450          while True:
451              # Read slave I2C status to see if the write buffer is ready.
452              status = self._read_u8(_AS726X_SLAVE_STATUS_REG)
453              if (status & _AS726X_SLAVE_TX_VALID) == 0:
454                  break  # No inbound TX pending at slave. Okay to write now.
455          # Send the virtual register address (setting bit 7 to indicate a pending write).
456          self.__write_u8(_AS726X_SLAVE_WRITE_REG, (addr | 0x80))
457          while True:
458              # Read the slave I2C status to see if the write buffer is ready.
459              status = self._read_u8(_AS726X_SLAVE_STATUS_REG)
460              if (status & _AS726X_SLAVE_TX_VALID) == 0:
461                  break  # No inbound TX pending at slave. Okay to write data now.
462  
463          # Send the data to complete the operation.
464          self.__write_u8(_AS726X_SLAVE_WRITE_REG, value)
465  
466  
467  class AS726x_UART(AS726x):
468      """AS726x spectral sensor via UART.
469  
470         :param ~busio.UART uart: The UART connected to the sensor
471         """
472  
473      def __init__(self, uart):
474          self._uart = uart
475          self._uart.baudrate = 115200
476          super().__init__()
477  
478      def read_channel(self, channel):
479          """Read an individual sensor channel"""
480          return self._virtual_read(channel)
481  
482      def read_calibrated_value(self, channel):
483          """Read a calibrated sensor channel"""
484          return self._virtual_read(channel)
485  
486      def _uart_xfer(self, cmd):
487          self._uart.reset_input_buffer()
488          cmd += "\n"
489          self._uart.write(cmd.encode())
490          time.sleep(0.1)
491          if self._uart.in_waiting:
492              resp = self._uart.read(self._uart.in_waiting)
493              return resp.rstrip(b" OK\n")
494          return None
495  
496      def _virtual_read(self, addr):
497          if addr == _AS726X_HW_VERSION:
498              # just return what is expected
499              return 0x40
500          elif addr == _AS726X_DEVICE_TEMP:
501              return int(self._uart_xfer("ATTEMP"))
502          elif addr == _AS726X_LED_CONTROL:
503              LED_IND = int(self._uart_xfer("ATLED0"))
504              LED_DRV = int(self._uart_xfer("ATLED1"))
505              return LED_IND << 3 | LED_DRV
506          elif addr == _AS726X_CONTROL_SETUP:
507              GAIN = int(self._uart_xfer("ATGAIN"))
508              BANK = int(self._uart_xfer("ATTCSMD"))
509              return GAIN << 4 | BANK << 2 | 1 << 1
510          elif addr in _COLOR_REGS:
511              resp = self._uart_xfer("ATDATA")
512              resp = resp.decode().split(",")
513              return int(resp[_COLOR_REGS.index(addr)])
514          elif addr in _COLOR_REGS_CALIBRATED:
515              resp = self._uart_xfer("ATCDATA")
516              resp = resp.decode().split(",")
517              return float(resp[_COLOR_REGS_CALIBRATED.index(addr)])
518  
519      def _virtual_write(self, addr, value):
520          if addr == _AS726X_CONTROL_SETUP:
521              # check for reset
522              if (value >> 7) & 0x01:
523                  self._uart.write(b"ATRST\n")
524                  return
525              # otherwise proceed
526              GAIN = (value >> 4) & 0x03
527              BANK = (value >> 2) & 0x03
528              self._uart_xfer("ATGAIN={}".format(GAIN))
529              self._uart_xfer("ATTCSMD={}".format(BANK))
530          elif addr == _AS726X_LED_CONTROL:
531              ICL_DRV = (value >> 4) & 0x07
532              LED_DRV = 100 if value & 0x08 else 0
533              ICL_IND = (value >> 1) & 0x07
534              LED_IND = 100 if value & 0x01 else 0
535              self._uart_xfer("ATLED0={}".format(LED_IND))
536              self._uart_xfer("ATLED1={}".format(LED_DRV))
537              self._uart_xfer("ATLEDC={}".format(ICL_DRV << 4 | ICL_IND))
538          elif addr == _AS726X_INT_T:
539              value = int(value / 2.8)
540              self._uart_xfer("ATINTTIME={}".format(value))
541  
542  
543  # pylint: enable=too-many-instance-attributes
544  # pylint: enable=too-many-public-methods
545  # pylint: enable=invalid-name
546  # pylint: enable=no-else-return
547  # pylint: enable=inconsistent-return-statements