|
| 1 | +#!/usr/bin/python |
| 2 | + |
| 3 | +import time |
| 4 | +import warnings |
| 5 | +from Adafruit_I2C import Adafruit_I2C |
| 6 | + |
| 7 | +def _sleepms(ms): |
| 8 | + """A wrapper around time.sleep that tries to be as accurate as possible. |
| 9 | + May still overshoot the requested sleep time, depending on the system's |
| 10 | + clock accuracy.""" |
| 11 | + # Experience shows that adding an extra millisecond is sometimes necessary, |
| 12 | + # even with the sleep loop below. |
| 13 | + target = time.time() + (ms + 1) / 1000.0 |
| 14 | + while time.time() < target: |
| 15 | + time.sleep(target - time.time()) |
| 16 | + |
| 17 | +class SensorSaturatedException(Exception): |
| 18 | + """Used when the sensor is saturated and cannot provide accurate readings.""" |
| 19 | + def __init__(self, channel): |
| 20 | + if type(channel) == int: |
| 21 | + self.msg('Channel {0} is saturated.'.format(channel)) |
| 22 | + else: |
| 23 | + self.msg('Both channels are saturated.') |
| 24 | + |
| 25 | + |
| 26 | +class _integrationTime: |
| 27 | + _DATA = { |
| 28 | + # time bits scale max |
| 29 | + 13: [ 13.7, 0x00, 11.0/322.0, 5047], |
| 30 | + 101: [101, 0x01, 81.0/322.0, 37177], |
| 31 | + 402: [402, 0x02, 322.0/322.0, 65535], |
| 32 | + } |
| 33 | + def __init__(self, time): |
| 34 | + if not time in _integrationTime._DATA.keys(): |
| 35 | + raise IndexError, 'Incorrect integration time: {0}; must be one of {1}'.format(time, sorted(_integrationTime._DATA.keys())) |
| 36 | + self.key = time |
| 37 | + self.time = _integrationTime._DATA[time][0] |
| 38 | + self.bits = _integrationTime._DATA[time][1] |
| 39 | + self.scale = _integrationTime._DATA[time][2] |
| 40 | + self.max = _integrationTime._DATA[time][3] |
| 41 | + |
| 42 | +class TSL2561(object): |
| 43 | + """Main class for interacting with a TSL2561 light sensor. |
| 44 | +
|
| 45 | + The constructor takes several named parameters, all optional: |
| 46 | + * address - The I2C address of the TSL2561 chipset. The class defines three |
| 47 | + constants for the possible chipset addresses: |
| 48 | + * ADDR_LOW - The address used when the ADDR SEL terminal is connected to |
| 49 | + ground. |
| 50 | + * ADDR_FLOAT - The address used when the ADDR SEL terminal is not |
| 51 | + connected to anything. This is the default address. |
| 52 | + * ADDR_HIGH - The address used when the ADDR SEL terminal is connected to |
| 53 | + V_DD (the bus voltage). |
| 54 | + * mode - The mode of operation for the lux readings. The class defines two |
| 55 | + constants for the two possible operating modes: |
| 56 | + * MODE_CONTINUOUS - The sensor is activated when the TSL2561 object is |
| 57 | + initialized. The `lux` property will return immediately with whatever |
| 58 | + the sensor's current reading is, but that reading is only updated at |
| 59 | + intervals specified by the `integrationTime` parameter. This mode is ideal |
| 60 | + for relatively continuous readings. This is the default mode. |
| 61 | + * MODE_SINGLE - The sensor is normally off. Every time the `lux` property is |
| 62 | + read, the sensor is turned on, the program sleeps for `integrationTime` |
| 63 | + milliseconds, the sensor data is read, the sensor is turned off, and the |
| 64 | + lux value is returned. This mode is ideal if sensor readings are only |
| 65 | + needed occasionally and you don't want the sensor drawing current all the |
| 66 | + time. |
| 67 | + * gain - The gain applied to sensor readings. See the `gain` class property |
| 68 | + for more information and acceptable values. Defaults to 1. |
| 69 | + * integrationTime - The integration time in milliseconds for a sensor |
| 70 | + reading. See the `integrationTime` class property for more information |
| 71 | + and acceptable values. Defaults to 402. |
| 72 | + * package - A string indicating which TSL2561 package is in use. Takes one |
| 73 | + of two values: |
| 74 | + * 'T' - TMB package. This is the default. |
| 75 | + * 'CS' - Chipscale package. |
| 76 | + * debug - Controls whether additional debugging information is printed as |
| 77 | + the class performs its functions. Defaults to False. |
| 78 | +
|
| 79 | + To use the class, simply instantiate it and then check the `lux` parameter |
| 80 | + for sensor readings. The `gain` and `integrationTime` properties may be |
| 81 | + used to alter the sensor's parameters between readings. |
| 82 | +
|
| 83 | + Manual integration intervals are not currently supported. |
| 84 | + Interrupts are not currently supported. |
| 85 | + """ |
| 86 | + |
| 87 | + # Address |
| 88 | + ADDR_LOW = 0x29 |
| 89 | + ADDR_FLOAT = 0x39 # Default address (pin left floating) |
| 90 | + ADDR_HIGH = 0x49 |
| 91 | + |
| 92 | + # Operating modes |
| 93 | + MODE_CONTINUOUS = 1 |
| 94 | + MODE_SINGLE = 2 |
| 95 | + _MODES = set([MODE_CONTINUOUS, MODE_SINGLE]) |
| 96 | + |
| 97 | + _REG_MOD = { |
| 98 | + 'COMMAND': 0x80, # Must be 1 |
| 99 | + 'CLEAR': 0x40, # Clears any pending interrupt (write 1 to clear) |
| 100 | + 'WORD': 0x20, # 1 = read/write word (rather than byte) |
| 101 | + 'BLOCK': 0x10, # 1 = using block read/write |
| 102 | + } |
| 103 | + |
| 104 | + _REGISTER = { |
| 105 | + 'CONTROL': 0x00, |
| 106 | + 'TIMING': 0x01, |
| 107 | + 'THRESHHOLDL': 0x02, # 16-bit |
| 108 | + 'THRESHHOLDH': 0x04, # 16-bit |
| 109 | + 'INTERRUPT': 0x06, |
| 110 | + 'CRC': 0x08, # Not for end use. |
| 111 | + 'ID': 0x0A, |
| 112 | + 'CHAN0': 0x0C, # 16-bit |
| 113 | + 'CHAN1': 0x0E, # 16-bit |
| 114 | + } |
| 115 | + |
| 116 | + _CONTROL = { |
| 117 | + 'ON': 0x03, |
| 118 | + 'OFF': 0x00, |
| 119 | + } |
| 120 | + |
| 121 | + _GAIN = { |
| 122 | + 1: 0x00, |
| 123 | + 16: 0x10, |
| 124 | + } |
| 125 | + |
| 126 | + # For calculating lux. |
| 127 | + # General formula is: B * CH0 - M1 * CH1 * (CH1/CH0)^M2 |
| 128 | + # "K" is the ratio of CH1/CH0 and determines which set of coefficients are |
| 129 | + # used. |
| 130 | + # Coefficients are from the "Calculating Lux" section of the datasheet. |
| 131 | + # The TMB and Chipscale packages have different coeffecients. At |
| 132 | + # instantiation time, the dicts here are replaced by the appropriate |
| 133 | + # component lists. |
| 134 | + _K = { |
| 135 | + 'T': [0.500, 0.610, 0.800, 1.300], |
| 136 | + 'CS': [0.520, 0.650, 0.800, 1.300], |
| 137 | + } |
| 138 | + _B = { |
| 139 | + 'T': [0.0304, 0.0224, 0.0128, 0.00146, 0.0], |
| 140 | + 'CS': [0.0315, 0.0229, 0.0157, 0.00338, 0.0], |
| 141 | + } |
| 142 | + _M1 = { |
| 143 | + 'T': [0.0620, 0.0310, 0.0153, 0.00112, 0.0], |
| 144 | + 'CS': [0.0593, 0.0291, 0.0180, 0.00260, 0.0], |
| 145 | + } |
| 146 | + _M2 = { |
| 147 | + 'T': [1.4, 0.0, 0.0, 0.0, 0.0], |
| 148 | + 'CS': [1.4, 0.0, 0.0, 0.0, 0.0], |
| 149 | + } |
| 150 | + |
| 151 | + _gain = 1 |
| 152 | + _integrationTime = _integrationTime(402) |
| 153 | + |
| 154 | + def __init__(self, address=ADDR_FLOAT, mode=MODE_CONTINUOUS, gain=_gain, integrationTime=_integrationTime.key, package='T', debug=False): |
| 155 | + if mode not in self._MODES: |
| 156 | + raise ValueError('Incorrect value passed as operating mode.') |
| 157 | + if not address in [TSL2561.ADDR_LOW, TSL2561.ADDR_FLOAT, TSL2561.ADDR_HIGH]: |
| 158 | + addr_msg = '0x{0:x} is not a standard TSL2561 address; continuing anyway'.format(address) |
| 159 | + warnings.warn(addr_msg) |
| 160 | + self.debug = debug |
| 161 | + self._i2c = Adafruit_I2C(address, debug=debug) |
| 162 | + id = self._i2c.readU8(TSL2561._REGISTER['ID']) |
| 163 | + if id < 0 or not id & 0x0A: |
| 164 | + raise EnvironmentError, 'Device at 0x{0:x} does not appear to be a TSL2561.'.format(address) |
| 165 | + |
| 166 | + self.mode = mode |
| 167 | + if self.mode == TSL2561.MODE_CONTINUOUS: |
| 168 | + self._poweron() |
| 169 | + |
| 170 | + self.gain = gain |
| 171 | + self.integrationTime = integrationTime |
| 172 | + self._K = self._K[package] |
| 173 | + self._B = self._B[package] |
| 174 | + self._M1 = self._M1[package] |
| 175 | + self._M2 = self._M2[package] |
| 176 | + |
| 177 | + @property |
| 178 | + def gain(self): |
| 179 | + """The gain applied to sensor readings. |
| 180 | +
|
| 181 | + Valid values are 1 and 16. In low-light conditions, a 16x gain will |
| 182 | + provide more accurate results. In bright-light conditions, 1x gain |
| 183 | + should be used, as 16x might oversaturate the sensor. |
| 184 | + """ |
| 185 | + return self._gain |
| 186 | + |
| 187 | + @gain.setter |
| 188 | + def gain(self, value): |
| 189 | + if not value in TSL2561._GAIN.keys(): |
| 190 | + raise IndexError, 'Incorrect gain: {0}; must be one of {1}'.format(value, sorted(TSL2561._GAIN.keys())) |
| 191 | + self._gain = value |
| 192 | + self._setTiming() |
| 193 | + |
| 194 | + @property |
| 195 | + def integrationTime(self): |
| 196 | + """The amount of time, in milliseconds, to spend collecting data for each reading. |
| 197 | +
|
| 198 | + Valid values are 13, 101, and 402. Lower values return more quickly, |
| 199 | + but are less accurate, especially in low light conditions. 402ms is the |
| 200 | + standard setting. |
| 201 | + """ |
| 202 | + return self._integrationTime.key |
| 203 | + |
| 204 | + @integrationTime.setter |
| 205 | + def integrationTime(self, value): |
| 206 | + self._integrationTime = _integrationTime(value) |
| 207 | + self._setTiming() |
| 208 | + |
| 209 | + @property |
| 210 | + def lux(self): |
| 211 | + """The current sensor reading in Lux. |
| 212 | +
|
| 213 | + If the operating mode is MODE_SINGLE, this property will take |
| 214 | + `integrationTime` milliseconds before it returns. If the operating mode |
| 215 | + is MODE_CONTINUOUS, the property will return immediately, regardless of |
| 216 | + whether the sensor has completed a new cycle in the time since the last |
| 217 | + reading. |
| 218 | + """ |
| 219 | + (chan0, chan1) = self._getData() |
| 220 | + |
| 221 | + chscale = self._integrationTime.scale * self.gain |
| 222 | + schan0 = chan0 / chscale |
| 223 | + schan1 = chan1 / chscale |
| 224 | + |
| 225 | + if schan0 != 0: |
| 226 | + ratio = schan1 / schan0 |
| 227 | + else: |
| 228 | + ratio = 0 |
| 229 | + if self.debug: |
| 230 | + print 'ratio: {0}'.format(ratio) |
| 231 | + idx = self._getRatioIdx(ratio) |
| 232 | + if self.debug: |
| 233 | + print 'ratio index: {0}'.format(idx) |
| 234 | + |
| 235 | + # Calculate |
| 236 | + if self._M2[idx] > 0: |
| 237 | + lux = self._B[idx] * schan0 - self._M1[idx] * schan0 * ratio**self._M2[idx] |
| 238 | + else: |
| 239 | + lux = self._B[idx] * schan0 - self._M1[idx] * schan1 |
| 240 | + |
| 241 | + # Minimum value of zero |
| 242 | + if lux < 0: |
| 243 | + lux = 0 |
| 244 | + |
| 245 | + return lux |
| 246 | + |
| 247 | + def _poweron(self): |
| 248 | + self._i2c.write8( |
| 249 | + TSL2561._REGISTER['CONTROL'] | TSL2561._REG_MOD['COMMAND'], |
| 250 | + TSL2561._CONTROL['ON']) |
| 251 | + if not self._i2c.readU8(TSL2561._REGISTER['CONTROL'] | TSL2561._REG_MOD['COMMAND']) & 0x03 == 0x03: |
| 252 | + raise EnvironmentError, 'TSL2561 did not power on correctly.'.format(address) |
| 253 | + |
| 254 | + def _poweroff(self): |
| 255 | + self._i2c.write8( |
| 256 | + TSL2561._REGISTER['CONTROL'] | TSL2561._REG_MOD['COMMAND'], |
| 257 | + TSL2561._CONTROL['OFF']) |
| 258 | + |
| 259 | + def _setTiming(self): |
| 260 | + if self.mode == TSL2561.MODE_SINGLE: |
| 261 | + self._poweron() |
| 262 | + self._i2c.write8( |
| 263 | + TSL2561._REGISTER['TIMING'] | TSL2561._REG_MOD['COMMAND'], |
| 264 | + TSL2561._GAIN[self.gain] | self._integrationTime.bits) |
| 265 | + if self.mode == TSL2561.MODE_SINGLE: |
| 266 | + self._poweroff() |
| 267 | + |
| 268 | + def _getData(self): |
| 269 | + if self.mode == TSL2561.MODE_SINGLE: |
| 270 | + self._poweron() |
| 271 | + _sleepms(self._integrationTime.time) |
| 272 | + chan0 = self._i2c.reverseByteOrder(self._i2c.readU16(TSL2561._REGISTER['CHAN0'] | TSL2561._REG_MOD['COMMAND'] | TSL2561._REG_MOD['WORD'])) |
| 273 | + chan1 = self._i2c.reverseByteOrder(self._i2c.readU16(TSL2561._REGISTER['CHAN1'] | TSL2561._REG_MOD['COMMAND'] | TSL2561._REG_MOD['WORD'])) |
| 274 | + if self.mode == TSL2561.MODE_SINGLE: |
| 275 | + self._poweroff() |
| 276 | + |
| 277 | + if self.debug: |
| 278 | + print 'chan0: 0x{0:x}, chan1: 0x{1:x}'.format(chan0, chan1) |
| 279 | + |
| 280 | + if chan0 >= self._integrationTime.max and chan1 >= self._integrationTime.max: |
| 281 | + raise SensorSaturatedException('both') |
| 282 | + elif chan0 >= self._integrationTime.max: |
| 283 | + raise SensorSaturatedException(0) |
| 284 | + elif chan1 >= self._integrationTime.max: |
| 285 | + raise SensorSaturatedException(1) |
| 286 | + |
| 287 | + return (chan0, chan1) |
| 288 | + |
| 289 | + def _getRatioIdx(self, ratio): |
| 290 | + for i in xrange(len(self._K)): |
| 291 | + if ratio < self._K[i]: |
| 292 | + return i |
| 293 | + return len(self._K) |
| 294 | + |
0 commit comments