Reading an I2C Color Sensor

From Phidgets Support


Introduction

The I2C Adapter Phidget allows you to interface with a wide range of I2C-compatible devices, including sensors, displays, and more.

In this project, we will use it to read data from an RGB Color Sensor.

Steps

To complete this project, we will work through the following steps:

  1. Determine the device's basic configuration parameters
  2. Identify the sequence of commands expected by the device
  3. Connect the device to the I2C Adapter Phidget
  4. Test the device using the Phidget Control Panel
  5. Create a custom program.

Resources

Information from the following resource will be referenced throughout this project.

Demonstration

Configuration Parameters

Excerpts from Datasheet

During this step, we are looking for the following information:

  • Supply Voltage
  • Communication Frequency
  • Device Address

This information is typically found using the device's product page and/or datasheet. In this case, it was all found on pages 3 to 5 of the datasheet:

  • Supply Voltage: 3.3V
  • Communication Frequency: 400kHz
  • I2C Address: 0x29

Command Sequence

Next, we need to determine the sequence of commands the device is expecting. The TCS34725 datasheet provides a state diagram that we can use to create a list of actions:

From datasheet (page 7)


Based on the state diagram, our list of actions should look like this:

Power On Steps:

  1. Enable PON (power on) to go from sleep to idle state
  2. Enable AEN (RGB enabled) to go from idle to RGBC state

After these steps, the device should be able to provide data and respond to commands. We're not using WEN (wait enabled) since we don't need this power-saving feature at this time.

Writing to TCS34725 Registers

I2C devices typically expect the following format:

Adp0002 an typicalcomms3.png

This protocol diagram from page 12 of the datasheet confirms that the TCS34725 behaves as expected:

Adp0002 rgb protocol.png

The Start (S), Stop (P), Acknowledge (A), and W/R (Write/Read) is all handled internally by the I2C Phidget, so we just need to focus on the command code and data.

Command Register

The command register lets us tell the sensor which register we want to read or write on our next communication.

Adp0002 rgb cmdreg.png
  • The first bit is always '1'
  • The next two bits control the how the device behaves when requesting multiple bytes
  • The last five bits hold the register address (we can ignore the special functions for this project)

Here's an abridged list of registers we'll be using:

Adp0002 rgb registers.png

Power On Steps

Enable the Sensor

Enable is managed by register 0x00, so our command register will look like this:

Adp0002 rgb cmdenable.png

So we'll send 0x80 to select the enable register.

The enable register is defined in the datasheet as follows:

Adp0002 rgb enablereg.png
  • Bits 5, 6, and 7 are 000, as they're reserved
  • Bit 4 will be 0, since we're not using interrupts
  • Bit 3 will be 0, since we're not using the wait feature
  • Bit 2 is 0, as it's reserved
  • Bit 1 will be 0, since we're not enabling RGBC yet
  • Bit 0 will be 1, to enable power

That gives us 0x01. Together with our command register, our first communication will be 0x8001. The reason we don't enable RGBC yet is because the device requires a 2.4ms delay between PON and RGBC for reliable startup.

Enable RGBC

To enable RGBC we will use the same command we used for power on, but both the AEN bit and the PON bit will be 1. This gives us 0x8003.

Reading Data

Now that the sensor is initialized, we can start getting data from it. We want to read all eight color registers, so we need to set auto-increment in the command register and point it to the first color register which is at 0x14:

Adp0002 rgb cmdreadauto.png

This means we need to send B4. After that, we can listen for 8 bytes at a time to get all of our color data.

Connecting to the ADP0001

The pinout for this sensor is printed on the PCB:

I2C rgb pinout2.png
  • VIN - Power
  • GND - Ground
  • 3V3 - 3.3V Output
  • SCL - Serial Clock
  • SDA - Serial Data
  • INT - Interrupt
  • LED - LED toggle


We don't need the 3.3V output or the interrupt pin for this project. The other pins can be connected to the ADP0001 as follows:

I2C rgb connection.png

The LED toggle pin is connected to one of the ADP0001's GPIO pins so we can use it to turn off the LED when not in use.

Testing in the Phidget Control Panel

Now that we have the command sequence and our sensor connected, we can use the Phidget Control Panel to confirm our device is working as expected.

Power On Steps

We can set the voltage to 3.3V, the frequency to 400kHz, and the address to 0x29.

I2C RGB ControlPanel.png

Now we can enable the sensor with our command sequence:

  • 0x8001 (Power on)
  • 0x8003 (RGBC on)
  • 0xB4 (auto increment and start from register 0x14)
I2C RGB ControlPanel2.png

Reading Data

After sending the command register B4, we can read all color registers by reading 8 bytes at a time.

I2C RGB ControlPanel3.png
Sensor response characteristic (pg.6)


This sensor ultimately provides us with four 16-bit values: Clear, Red, Green, and Blue. The clear light value is important for providing brightness calibration. Since the responses of the red, green and blue sensors are normalized to match the response of the clear sensor, we can use it as a baseline to determine color in both bright and dim environments.

In the above image, we get roughly the same value when polling the sensor three times, so the value seems stable. We'll use the bottom row to see if the color makes sense. The data bytes received are sent in register order, so our values are:

  • Clear: 0x0FFD
  • Red: 0x0A96
  • Green: 0x0331
  • Blue: 0x0271

Converting to decimal, that's 4093, 2710, 817, 625. We can normalize the last three values using the clear value, and then multiply by 255 and round to get a standard RGB value:

  • Red: (2710/4093)*255 = 169
  • Green: (817/4093)*255 = 51
  • Blue: (625/4093)*255 = 39

This RGB value (#A93327) looks to be correct, as the sensor was pointed at a roll of red electrical tape.

Writing a Custom Program

To begin writing a custom program for your Phidget device, navigate to the product page and select the Code Samples tab. From there, you can customize the code for your own purposes. Here we have modified the Python code sample to match our steps from the Phidget Control Panel.

from Phidget22.Phidget import *
from Phidget22.Devices.DataAdapter import *
import time

def convert_rgbc(data):

	c = data[1] << 8 | data[0]

	if(c <= 0):
		return [0,0,0]

	# Combine high and low bytes for each color,
	# Then normalize and cap at 255
	r = min(255,round((data[3] << 8 | data[2]) / c * 255))
	g = min(255,round((data[5] << 8 | data[4]) / c * 255))
	b = min(255,round((data[7] << 8 | data[6]) / c * 255))
	return [r,g,b]

def main():

	adp = DataAdapter()
	adp.openWaitForAttachment(5000)

	# Set properties
	adp.setFrequency(DataAdapterFrequency.FREQUENCY_400kHz)
	adp.setDataAdapterVoltage(DataAdapterVoltage.DATAADAPTER_VOLTAGE_3_3V)
	addr = 0x29

	# Power On
	adp.i2cSendReceive(addr, [0x8001], 0)
	time.sleep(0.01)

	# RGBC On
	adp.i2cSendReceive(addr, [0x8003], 0)

	# Auto-increment starting with CDATAL
	adp.i2cSendReceive(addr, [0xB4], 0)

	try:
		while True:
			rgb = convert_rgbc(adp.i2cSendReceive(addr, [], 8))
			print("R: " + str(rgb[0]) + "   G: " + str(rgb[1]) + "   B: " + str(rgb[2]))
			time.sleep(0.5)
			pass
	except KeyboardInterrupt:
		print("Closing...")

	adp.close()

main()

That’s about it, we went from reading the datasheet to a working script. Not all datasheets are created equal, so if you’re struggling with getting your sensor set up, feel free to ask for help by sending us an email.

Python Graphical Example

Here is a more in-depth script that displays a graph of the raw values and shows the calculated color after gamma and saturation adjustment.

Expand All
Full Script (Click to expand)
import tkinter as tk
import math
from Phidget22.Phidget import *
from Phidget22.Devices.DataAdapter import *
import time

class MultiLineGraph(tk.Canvas):
    def __init__(self, parent, lines=4, max_points=100, **kwargs):
        super().__init__(parent, bg="black", **kwargs)
        self.lines = lines
        self.max_points = max_points
        self.data = [[] for _ in range(lines)]
        self.colors = ["white", "red", "green", "blue"]

    def add_points(self, *values):
        for i, v in enumerate(values):
            self.data[i].append(v)
            if len(self.data[i]) > self.max_points:
                self.data[i].pop(0)
        self.draw()

    def draw(self):
        self.delete("all")
        w = self.winfo_width()
        h = self.winfo_height()

        if w < 10 or h < 10:
            return

        # Flatten all visible data
        all_values = [v for line in self.data for v in line]

        if not all_values:
            return

        min_val = min(all_values)
        max_val = max(all_values)

        # Prevent divide-by-zero
        if max_val == min_val:
            max_val += 1

        padding = (max_val - min_val) * 0.1  # 10% padding
        min_val -= padding
        max_val += padding

        for i, line in enumerate(self.data):
            if len(line) < 2:
                continue

            points = []
            for j, val in enumerate(line):
                x = j / (self.max_points - 1) * w

                # Normalize to 0–1
                norm = (val - min_val) / (max_val - min_val)

                # Flip Y (Tkinter origin is top-left)
                y = h - (norm * h)

                points.extend([x, y])

            self.create_line(points, fill=self.colors[i], width=2)


class App:
    def __init__(self, root):
        self.root = root
        self.root.title("I2C RGB Viewer")
        self.adp = DataAdapter()
        self.addr = 0x29
        self.sat = 1.8
        self.gamma = 1.3

        # --- Define Gamma Table ---
        self.gamma_table = [0] * 256
        for i in range(256):
            self.gamma_table[i] = int(math.pow(i/255.0,self.gamma)*255)

        # --- UI ---
        self.graph = MultiLineGraph(root, height=300)
        self.graph.pack(fill="both", expand=True)

        self.rgb_entry = tk.Entry(root, font=("Consolas", 14))
        self.rgb_entry.pack(fill="x")

        self.color_panel = tk.Frame(root, height=50, bg="black")
        self.color_panel.pack(fill="x")
        
        self.adp.openWaitForAttachment(5000)

        # Set properties
        self.adp.setFrequency(DataAdapterFrequency.FREQUENCY_400kHz)
        self.adp.setDataAdapterVoltage(DataAdapterVoltage.DATAADAPTER_VOLTAGE_3_3V)

        # Power On
        self.adp.i2cSendReceive(self.addr, [0x8001], 0)
        time.sleep(0.01)

        # RGBC On
        self.adp.i2cSendReceive(self.addr, [0x8003], 0)

        # Auto-increment starting with CDATAL
        self.adp.i2cSendReceive(self.addr, [0xB4], 0)

        # --- Timer ---
        self.update_loop()

    def read_sensor(self):
        
        data = self.adp.i2cSendReceive(self.addr, [], 8)

        # Combine high and low bytes for clear register
        raw_c = data[1] << 8 | data[0]

        if(raw_c <= 0):
            return [0,0,0,0]

        # Combine high and low bytes for each colour
        raw_r = data[3] << 8 | data[2]
        raw_g = data[5] << 8 | data[4]
        raw_b = data[7] << 8 | data[6]

        return raw_c, raw_r, raw_g, raw_b

    def update_loop(self):
        raw_c, raw_r, raw_g, raw_b = self.read_sensor()

        if raw_c > 0:
            # Normalize + gamma
            norm_r = self.gamma_table[int(raw_r / raw_c * 255)]
            norm_g = self.gamma_table[int(raw_g / raw_c * 255)]
            norm_b = self.gamma_table[int(raw_b / raw_c * 255)]

            avg = (norm_r + norm_g + norm_b) / 3

            # Saturation adjust
            norm_r = max(0, min(255, avg + (norm_r - avg) * self.sat))
            norm_g = max(0, min(255, avg + (norm_g - avg) * self.sat))
            norm_b = max(0, min(255, avg + (norm_b - avg) * self.sat))

            hex_color = "#{:02X}{:02X}{:02X}".format(
                int(norm_r), int(norm_g), int(norm_b)
            )

            self.rgb_entry.delete(0, tk.END)
            self.rgb_entry.insert(0, hex_color)

            self.color_panel.config(bg=hex_color)

        # Update graph
        self.graph.add_points(raw_c, raw_r, raw_g, raw_b)

        # Repeat every 50 ms
        self.root.after(50, self.update_loop)


if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.geometry("600x400")
    root.mainloop()