|
| 1 | +// Package bh1745 implements a driver for the bh1745 color sensor |
| 2 | +// It detects 16-bit RGBC (Red, Green, Blue, Clear) color over |
| 3 | +// I2C interface. |
| 4 | +// |
| 5 | +// Datasheet: https://www.mouser.co.uk/datasheet/2/348/bh1745nuc-e-519994.pdf |
| 6 | +// |
| 7 | +// Pimoroni bh1745-python driver |
| 8 | +// https://github.com/pimoroni/bh1745-python/blob/main/bh1745/__init__.py |
| 9 | + |
| 10 | +package bh1745 // import "tinygo.org/x/drivers/bh1745" |
| 11 | + |
| 12 | +import ( |
| 13 | + "math" |
| 14 | + "time" |
| 15 | + |
| 16 | + "tinygo.org/x/drivers" |
| 17 | + "tinygo.org/x/drivers/internal/legacy" |
| 18 | +) |
| 19 | + |
| 20 | +// RGBCData holds one set of raw channel readings. |
| 21 | +type RGBCData struct { |
| 22 | + R, G, B, C uint16 |
| 23 | +} |
| 24 | + |
| 25 | +// Device is the BH1745 RGBC colour sensor driver. |
| 26 | +type Device struct { |
| 27 | + bus drivers.I2C |
| 28 | + Address uint16 |
| 29 | + |
| 30 | + r, g, b, c uint16 // cached raw RGBC readings from the last Update call |
| 31 | +} |
| 32 | + |
| 33 | +// New creates a new BH1745 driver. The I2C bus must already be configured. |
| 34 | +// Call Configure() before calling Read(). |
| 35 | +func New(bus drivers.I2C) Device { |
| 36 | + return Device{ |
| 37 | + bus: bus, |
| 38 | + Address: Address, |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +// Configure resets the sensor and sets it up with default settings: |
| 43 | +// 160 ms measurement time, 1× ADC gain, RGBC measurement enabled. |
| 44 | +// |
| 45 | +// The undocumented write of 0x02 to MODE_CONTROL3 (register 0x44) is included |
| 46 | +// here; without it the sensor reports incorrect RGBC values. This matches the |
| 47 | +// workaround applied in Pimoroni's Enviro Indoor firmware. |
| 48 | +func (d *Device) Configure() error { |
| 49 | + // Software reset. |
| 50 | + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{SW_RESET}); err != nil { |
| 51 | + return err |
| 52 | + } |
| 53 | + |
| 54 | + // Wait for sw_reset to self-clear (typically < 1 ms; allow up to 200 ms). |
| 55 | + for i := 0; i < 200; i++ { |
| 56 | + var buf [1]byte |
| 57 | + if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:]); err != nil { |
| 58 | + return err |
| 59 | + } |
| 60 | + if buf[0]&SW_RESET == 0 { |
| 61 | + break |
| 62 | + } |
| 63 | + time.Sleep(1 * time.Millisecond) |
| 64 | + } |
| 65 | + |
| 66 | + // Clear int_reset flag. |
| 67 | + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{0x00}); err != nil { |
| 68 | + return err |
| 69 | + } |
| 70 | + |
| 71 | + // 160 ms measurement time. |
| 72 | + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(MeasTime160ms)}); err != nil { |
| 73 | + return err |
| 74 | + } |
| 75 | + |
| 76 | + // Enable RGBC with 1× gain. |
| 77 | + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, []byte{RGBC_EN | byte(Gain1X)}); err != nil { |
| 78 | + return err |
| 79 | + } |
| 80 | + |
| 81 | + // Undocumented workaround: writing 0x02 to MODE_CONTROL3 is required for |
| 82 | + // the sensor to report correct colour data. Must be applied after reset. |
| 83 | + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL3, []byte{0x02}); err != nil { |
| 84 | + return err |
| 85 | + } |
| 86 | + |
| 87 | + // Wait for one full 160 ms measurement cycle before the first read. |
| 88 | + time.Sleep(160 * time.Millisecond) |
| 89 | + return nil |
| 90 | +} |
| 91 | + |
| 92 | +// Connected returns true when the sensor is present and the part ID and |
| 93 | +// manufacturer ID both match expected values. |
| 94 | +func (d *Device) Connected() bool { |
| 95 | + var buf [1]byte |
| 96 | + legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:]) |
| 97 | + if buf[0]&PART_ID_MASK != PART_ID { |
| 98 | + return false |
| 99 | + } |
| 100 | + legacy.ReadRegister(d.bus, uint8(d.Address), REG_MANUFACTURER, buf[:]) |
| 101 | + return buf[0] == MANUFACTURER_ID |
| 102 | +} |
| 103 | + |
| 104 | +// SetMeasurementTime changes the RGBC integration time. Wait at least one full |
| 105 | +// cycle at the new time before calling Read() again. |
| 106 | +func (d *Device) SetMeasurementTime(t MeasurementTime) { |
| 107 | + legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(t) & 0x07}) |
| 108 | +} |
| 109 | + |
| 110 | +// SetADCGain changes the ADC gain multiplier. |
| 111 | +func (d *Device) SetADCGain(g ADCGain) { |
| 112 | + var buf [1]byte |
| 113 | + legacy.ReadRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:]) |
| 114 | + buf[0] = (buf[0] &^ 0x03) | byte(g) |
| 115 | + legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:]) |
| 116 | +} |
| 117 | + |
| 118 | +// Update reads a fresh set of RGBC values from the sensor and caches them. |
| 119 | +// Pass drivers.Luminosity (or drivers.AllMeasurements) to trigger a read. |
| 120 | +// The cached values are then available via R(), G(), B(), C(), Luminosity(), |
| 121 | +// and ColorTemperature(). |
| 122 | +func (d *Device) Update(which drivers.Measurement) error { |
| 123 | + if which&drivers.Luminosity == 0 { |
| 124 | + return nil |
| 125 | + } |
| 126 | + var buf [8]byte |
| 127 | + if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_RED_DATA_LSB, buf[:]); err != nil { |
| 128 | + return err |
| 129 | + } |
| 130 | + d.r = uint16(buf[1])<<8 | uint16(buf[0]) |
| 131 | + d.g = uint16(buf[3])<<8 | uint16(buf[2]) |
| 132 | + d.b = uint16(buf[5])<<8 | uint16(buf[4]) |
| 133 | + d.c = uint16(buf[7])<<8 | uint16(buf[6]) |
| 134 | + return nil |
| 135 | +} |
| 136 | + |
| 137 | +// R returns the cached raw red channel value from the last Update call. |
| 138 | +func (d *Device) R() uint16 { return d.r } |
| 139 | + |
| 140 | +// G returns the cached raw green channel value from the last Update call. |
| 141 | +func (d *Device) G() uint16 { return d.g } |
| 142 | + |
| 143 | +// B returns the cached raw blue channel value from the last Update call. |
| 144 | +func (d *Device) B() uint16 { return d.b } |
| 145 | + |
| 146 | +// C returns the cached raw clear channel value from the last Update call. |
| 147 | +func (d *Device) C() uint16 { return d.c } |
| 148 | + |
| 149 | +// Luminosity returns the illuminance in lux calculated from the cached RGBC |
| 150 | +// values. Call Update first. |
| 151 | +func (d *Device) Luminosity() uint32 { |
| 152 | + return CalcLux(d.r, d.g, d.b, d.c) |
| 153 | +} |
| 154 | + |
| 155 | +// ColorTemperature returns the correlated colour temperature in Kelvin |
| 156 | +// calculated from the cached RGBC values. Call Update first. |
| 157 | +func (d *Device) ColorTemperature() uint32 { |
| 158 | + return CalcColorTemperature(d.r, d.g, d.b, d.c) |
| 159 | +} |
| 160 | + |
| 161 | +// Read returns the last cached raw RGBC channel values without performing I2C. |
| 162 | +// Call Update first to refresh the values. |
| 163 | +// |
| 164 | +// Deprecated: call Update then use R(), G(), B(), C() instead. |
| 165 | +func (d *Device) Read() RGBCData { |
| 166 | + return RGBCData{R: d.r, G: d.g, B: d.b, C: d.c} |
| 167 | +} |
| 168 | + |
| 169 | +// CalcLux calculates the illuminance in lux from raw RGBC values. |
| 170 | +// |
| 171 | +// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py), tuned for |
| 172 | +// the BH1745 configured at 160 ms integration time and 1× gain. |
| 173 | +func CalcLux(r, g, b, c uint16) uint32 { |
| 174 | + if g < 1 { |
| 175 | + return 0 |
| 176 | + } |
| 177 | + rf := float32(r) |
| 178 | + gf := float32(g) |
| 179 | + cf := float32(c) |
| 180 | + var tmp float32 |
| 181 | + if cf/gf < 0.160 { |
| 182 | + tmp = 0.202*rf + 0.766*gf |
| 183 | + } else { |
| 184 | + tmp = 0.159*rf + 0.646*gf |
| 185 | + } |
| 186 | + if tmp < 0 { |
| 187 | + return 0 |
| 188 | + } |
| 189 | + return uint32(tmp) |
| 190 | +} |
| 191 | + |
| 192 | +// CalcColorTemperature calculates the correlated colour temperature (CCT) in Kelvin |
| 193 | +// from raw RGBC values. |
| 194 | +// |
| 195 | +// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py). |
| 196 | +// Returns 0 when the light level is too low for a reliable estimate. |
| 197 | +func CalcColorTemperature(r, g, b, c uint16) uint32 { |
| 198 | + if g < 1 { |
| 199 | + return 0 |
| 200 | + } |
| 201 | + rf := float64(r) |
| 202 | + gf := float64(g) |
| 203 | + bf := float64(b) |
| 204 | + cf := float64(c) |
| 205 | + sum := rf + gf + bf |
| 206 | + if sum < 1 { |
| 207 | + return 0 |
| 208 | + } |
| 209 | + |
| 210 | + rRatio := rf / sum |
| 211 | + bRatio := bf / sum |
| 212 | + |
| 213 | + var ct float64 |
| 214 | + if cf/gf < 0.160 { |
| 215 | + bEff := bRatio * 3.13 |
| 216 | + if bEff > 1 { |
| 217 | + bEff = 1 |
| 218 | + } |
| 219 | + ct = (1-bEff)*12746*math.Exp(-2.911*rRatio) + bEff*1637*math.Exp(4.865*bRatio) |
| 220 | + } else { |
| 221 | + bEff := bRatio * 10.67 |
| 222 | + if bEff > 1 { |
| 223 | + bEff = 1 |
| 224 | + } |
| 225 | + ct = (1-bEff)*16234*math.Exp(-2.781*rRatio) + bEff*1882*math.Exp(4.448*bRatio) |
| 226 | + } |
| 227 | + if ct > 10000 { |
| 228 | + ct = 10000 |
| 229 | + } |
| 230 | + return uint32(ct) |
| 231 | +} |
0 commit comments