/ 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