Skip to content

Instantly share code, notes, and snippets.

@idriszmy
Created April 18, 2022 08:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save idriszmy/3bdb13d7d11239b6fecfb9eba068b6ec to your computer and use it in GitHub Desktop.
Save idriszmy/3bdb13d7d11239b6fecfb9eba068b6ec to your computer and use it in GitHub Desktop.
Color (RGB) detector using CircuitPython on Raspberry Pi Pico
"""
Color (RGB) detector using CircuitPython on Raspberry Pi Pico.
Items:
- Raspberry Pi Pico
https://my.cytron.io/p-raspberry-pi-pico-board
- Colour Sensor Module
https://my.cytron.io/p-colour-sensor-module
- I2C 1602 Serial LCD
https://my.cytron.io/p-i2c-1602-serial-lcd-for-arduino-and-rpi
- Strip Board
https://my.cytron.io/p-strip-board-big-10x23cm
- Soldering Iron Kit
https://my.cytron.io/Plusivo/p-soldering-iron-kit-with-digital-multimeter-uk-plug
- USB Micro B Cable
https://my.cytron.io/p-usb-micro-b-cable
- Straight Female Header 1x40 Ways
https://my.cytron.io/p-straight-female-header-1x40-ways
Libraries required from bundle (https://circuitpython.org/libraries):
- simpleio.mpy
References:
- https://randomnerdtutorials.com/arduino-color-sensor-tcs230-tcs3200/
- https://docs.circuitpython.org/en/latest/shared-bindings/pulseio/index.html
Last update: 18 Apr 2022
"""
import time
import board
import digitalio
import pulseio
import simpleio
import busio
from lcd.lcd import LCD
from lcd.i2c_pcf8574_interface import I2CPCF8574Interface
LCD_SCL = board.GP9
LCD_SDA = board.GP8
LCD_ADDR = 0x27
i2c_lcd = busio.I2C(LCD_SCL, LCD_SDA)
lcd = LCD(I2CPCF8574Interface(i2c_lcd, LCD_ADDR), num_rows=2, num_cols=16)
sen_S0 = digitalio.DigitalInOut(board.GP14)
sen_S0.direction = digitalio.Direction.OUTPUT
sen_S1 = digitalio.DigitalInOut(board.GP15)
sen_S1.direction = digitalio.Direction.OUTPUT
sen_S2 = digitalio.DigitalInOut(board.GP17)
sen_S2.direction = digitalio.Direction.OUTPUT
sen_S3 = digitalio.DigitalInOut(board.GP16)
sen_S3.direction = digitalio.Direction.OUTPUT
sen_OUT = pulseio.PulseIn(board.GP18, idle_state=True)
lcd.print("RGB Detector\n>NONE")
# Set output frequency scaling to 20%
sen_S0.value = True
sen_S1.value = False
color = "NONE"
prev_color = ""
while True:
sen_S2.value = False
sen_S3.value = False
time.sleep(0.01)
# Wait for an active pulse
while len(sen_OUT) == 0:
pass
sen_OUT.pause() # Pause while we do something with the pulses
red_freq = sen_OUT[0]
red_color = simpleio.map_range(red_freq, 100, 530, 100, 0)
sen_OUT.clear()
sen_OUT.resume()
sen_S2.value = True
sen_S3.value = True
time.sleep(0.01)
# Wait for an active pulse
while len(sen_OUT) == 0:
pass
# Pause while we do something with the pulses
sen_OUT.pause()
green_freq = sen_OUT[0]
green_color = simpleio.map_range(green_freq, 150, 700, 100, 0)
sen_OUT.clear()
sen_OUT.resume()
sen_S2.value = False
sen_S3.value = True
time.sleep(0.01)
# Wait for an active pulse
while len(sen_OUT) == 0:
pass
# Pause while we do something with the pulses
sen_OUT.pause()
blue_freq = sen_OUT[0]
blue_color = simpleio.map_range(blue_freq, 70, 600, 100, 0)
#print("{}\t{}\t{}".format(red_freq, green_freq, blue_freq))
#print("{}\t{}\t{}".format(int(red_color), int(green_color), int(blue_color)))
if red_color < 50 and green_color < 50 and blue_color < 50:
color = "NONE "
elif red_color > green_color and \
red_color > blue_color:
color = "RED "
elif green_color > red_color and \
green_color > blue_color:
color = "GREEN"
elif blue_color > green_color and \
blue_color > red_color:
color = "BLUE "
if color != prev_color:
prev_color = color
print(color)
lcd.set_cursor_pos(1, 1)
lcd.print(color)
time.sleep(0.1)
sen_OUT.clear()
sen_OUT.resume()
# Copyright (C) 2017 Dan Halbert
# Adapted from https://github.com/dbrgn/RPLCD, Copyright (C) 2013-2016 Danilo Bargen
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Low-level interface to PCF8574."""
import busio
import board
import microcontroller
from adafruit_bus_device.i2c_device import I2CDevice
from .lcd import LCD_4BITMODE, LCD_BACKLIGHT, LCD_NOBACKLIGHT, PIN_ENABLE
class I2CPCF8574Interface:
# Bit values to turn backlight on/off. Indexed by a boolean.
_BACKLIGHT_VALUES = (LCD_NOBACKLIGHT, LCD_BACKLIGHT)
def __init__(self, i2c, address):
"""
CharLCD via PCF8574 I2C port expander.
Pin mapping::
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
D7 | D6 | D5 | D4 | BL | EN | RW | RS
:param address: The I2C address of your LCD.
"""
self.address = address
self._backlight_pin_state = LCD_BACKLIGHT
self.i2c = i2c
self.i2c_device = I2CDevice(self.i2c, self.address)
self.data_buffer = bytearray(1)
def deinit(self):
self.i2c.deinit()
@property
def data_bus_mode(self):
return LCD_4BITMODE
@property
def backlight(self):
return self._backlight_pin_state == LCD_BACKLIGHT
@backlight.setter
def backlight(self, value):
self._backlight_pin_state = self._BACKLIGHT_VALUES[value]
with self.i2c_device:
self._i2c_write(self._backlight_pin_state)
# Low level commands
def send(self, value, rs_mode):
"""Send the specified value to the display in 4-bit nibbles.
The rs_mode is either ``_RS_DATA`` or ``_RS_INSTRUCTION``."""
self._write4bits(rs_mode | (value & 0xF0) | self._backlight_pin_state)
self._write4bits(rs_mode | ((value << 4) & 0xF0) | self._backlight_pin_state)
def _write4bits(self, value):
"""Pulse the `enable` flag to process value."""
with self.i2c_device:
self._i2c_write(value & ~PIN_ENABLE)
# This 1us delay is probably unnecessary, given the time needed
# to execute the statements.
microcontroller.delay_us(1)
self._i2c_write(value | PIN_ENABLE)
microcontroller.delay_us(1)
self._i2c_write(value & ~PIN_ENABLE)
# Wait for command to complete.
microcontroller.delay_us(100)
def _i2c_write(self, value):
self.data_buffer[0] = value
self.i2c_device.write(self.data_buffer)
# Copyright (C) 2017 Dan Halbert
# Adapted from https://github.com/dbrgn/RPLCD, Copyright (C) 2013-2016 Danilo Bargen
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import time
from micropython import const
# Commands
_LCD_CLEARDISPLAY = const(0x01)
_LCD_RETURNHOME = const(0x02)
_LCD_ENTRYMODESET = const(0x04)
_LCD_DISPLAYCONTROL = const(0x08)
_LCD_CURSORSHIFT = const(0x10)
_LCD_FUNCTIONSET = const(0x20)
_LCD_SETCGRAMADDR = const(0x40)
_LCD_SETDDRAMADDR = const(0x80)
# Flags for display entry mode
_LCD_ENTRYRIGHT = const(0x00)
_LCD_ENTRYLEFT = const(0x02)
_LCD_ENTRYSHIFTINCREMENT = const(0x01)
_LCD_ENTRYSHIFTDECREMENT = const(0x00)
# Flags for display on/off control
_LCD_DISPLAYON = const(0x04)
_LCD_DISPLAYOFF = const(0x00)
_LCD_CURSORON = const(0x02)
_LCD_CURSOROFF = const(0x00)
_LCD_BLINKON = const(0x01)
_LCD_BLINKOFF = const(0x00)
# Flags for display/cursor shift
_LCD_DISPLAYMOVE = const(0x08)
_LCD_CURSORMOVE = const(0x00)
_LCD_MOVERIGHT = const(0x04)
_LCD_MOVELEFT = const(0x00)
# Flags for function set
_LCD_8BITMODE = const(0x10)
LCD_4BITMODE = const(0x00)
_LCD_2LINE = const(0x08)
_LCD_1LINE = const(0x00)
_LCD_5x10DOTS = const(0x04)
_LCD_5x8DOTS = const(0x00)
# Flags for backlight control
LCD_BACKLIGHT = const(0x08)
LCD_NOBACKLIGHT = const(0x00)
# Flags for RS pin modes
_RS_INSTRUCTION = const(0x00)
_RS_DATA = const(0x01)
# Pin bitmasks
PIN_ENABLE = const(0x4)
PIN_READ_WRITE = const(0x2)
PIN_REGISTER_SELECT = const(0x1)
class CursorMode:
HIDE = const(_LCD_CURSOROFF | _LCD_BLINKOFF)
LINE = const(_LCD_CURSORON | _LCD_BLINKOFF)
BLINK = const(_LCD_CURSOROFF | _LCD_BLINKON)
MICROSECOND = 1e-6
MILLISECOND = 1e-3
class LCD(object):
def __init__(self, interface, num_cols=20, num_rows=4, char_height=8):
"""
Character LCD controller.
:param interface: Communication interface, such as I2CInterface
:param num_rows: Number of display rows (usually 1, 2 or 4). Default: 4.
:param num_cols: Number of columns per row (usually 16 or 20). Default 20.
:param char_height: Some 1 line displays allow a font height of 10px.
Allowed: 8 or 10. Default: 8.
"""
self.interface = interface
if char_height not in (8, 10):
raise ValueError('The ``char_height`` argument should be either 8 or 10.')
self.char_height = char_height
self.num_rows = num_rows
self.num_cols = num_cols
# get row addresses (varies based on display size)
self._row_offsets = (0x00, 0x40, self.num_cols, 0x40 + self.num_cols)
# Setup initial display configuration
displayfunction = self.interface.data_bus_mode | _LCD_5x8DOTS
if self.num_rows == 1:
displayfunction |= _LCD_1LINE
elif self.num_rows in (2, 4):
# LCD only uses two lines on 4 row displays
displayfunction |= _LCD_2LINE
if self.char_height == 10:
# For some 1 line displays you can select a 10px font.
displayfunction |= _LCD_5x10DOTS
# Choose 4 or 8 bit mode
self.command(0x03)
time.sleep(4.5*MILLISECOND)
self.command(0x03)
time.sleep(4.5*MILLISECOND)
self.command(0x03)
if self.interface.data_bus_mode == LCD_4BITMODE:
# Hitachi manual page 46
time.sleep(100*MICROSECOND)
self.command(0x02)
elif self.interface.data_bus_mode == _LCD_8BITMODE:
# Hitachi manual page 45
self.command(0x30)
time.sleep(4.5*MILLISECOND)
self.command(0x30)
time.sleep(100*MICROSECOND)
self.command(0x30)
else:
raise ValueError('Invalid data bus mode: {}'.format(self.interface.data_bus_mode))
# Write configuration to display
self.command(_LCD_FUNCTIONSET | displayfunction)
time.sleep(50*MICROSECOND)
# Configure entry mode. Define internal fields.
self.command(_LCD_ENTRYMODESET | _LCD_ENTRYLEFT)
time.sleep(50*MICROSECOND)
# Configure display mode. Define internal fields.
self._display_mode = _LCD_DISPLAYON
self._cursor_mode = CursorMode.HIDE
self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode)
time.sleep(50*MICROSECOND)
self.clear()
def close(self):
self.interface.deinit()
def set_backlight(self, value):
self.interface.backlight = value
def set_display_enabled(self, value):
self._display_mode = _LCD_DISPLAYON if value else _LCD_DISPLAYOFF
self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode)
time.sleep(50*MICROSECOND)
def set_cursor_mode(self, value):
self._cursor_mode = value
self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode)
time.sleep(50*MICROSECOND)
def cursor_pos(self):
"""The cursor position as a 2-tuple (row, col)."""
return (self._row, self._col)
def set_cursor_pos(self, row, col):
if not (0 <= row < self.num_rows):
raise ValueError('row should be in range 0-{}'.format(self.num_rows - 1))
if not (0 <= col < self.num_cols):
raise ValueError('col should be in range 0-{}'.format(self.num_cols - 1))
self._row = row
self._col = col
self.command(_LCD_SETDDRAMADDR | self._row_offsets[row] + col)
time.sleep(50*MICROSECOND)
def print(self, string):
"""
Write the specified unicode string to the display.
A newline ('\n') will advance to the left side of the next row.
Lines that are too long automatically continue on next line.
Only characters with an ``ord()`` value between 0 and 255 are currently
supported.
"""
for char in string:
if char == '\n':
# Advance to next row, at left side. Wrap around to top row if at bottom.
self.set_cursor_pos((self._row + 1) % self.num_rows, 0)
else:
self.write(ord(char))
def clear(self):
"""Overwrite display with blank characters and reset cursor position."""
self.command(_LCD_CLEARDISPLAY)
time.sleep(2*MILLISECOND)
self.home()
def home(self):
"""Set cursor to initial position and reset any shifting."""
self.command(_LCD_RETURNHOME)
self._row = 0
self._col = 0
time.sleep(2*MILLISECOND)
def shift_display(self, amount):
"""Shift the display. Use negative amounts to shift left and positive
amounts to shift right."""
if amount == 0:
return
direction = _LCD_MOVERIGHT if amount > 0 else _LCD_MOVELEFT
for i in range(abs(amount)):
self.command(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | direction)
time.sleep(50*MICROSECOND)
def create_char(self, location, bitmap):
"""Create a new character.
The HD44780 supports up to 8 custom characters (location 0-7).
:param location: The place in memory where the character is stored.
Values need to be integers between 0 and 7.
:type location: int
:param bitmap: The bitmap containing the character. This should be a
bytearray of 8 numbers, each representing a 5 pixel row.
:type bitmap: bytearray
:raises AssertionError: Raised when an invalid location is passed in or
when bitmap has an incorrect size.
Example:
.. sourcecode:: python
>>> smiley = bytearray(
... 0b00000,
... 0b01010,
... 0b01010,
... 0b00000,
... 0b10001,
... 0b10001,
... 0b01110,
... 0b00000,
... )
>>> lcd.create_char(0, smiley)
"""
if not (0 <= location <= 7):
raise ValueError('Only locations 0-7 are valid.')
if len(bitmap) != 8:
raise ValueError('Bitmap should have exactly 8 rows.')
# Store previous position
save_row = self._row
save_col = self._col
# Write character to CGRAM
self.command(_LCD_SETCGRAMADDR | location << 3)
for row in bitmap:
self.interface.send(row, _RS_DATA)
# Restore cursor pos
self.set_cursor_pos(save_row, save_col)
def command(self, value):
"""Send a raw command to the LCD."""
self.interface.send(value, _RS_INSTRUCTION)
def write(self, value):
"""Write a raw character byte to the LCD."""
self.interface.send(value, _RS_DATA)
if self._col < self.num_cols - 1:
# Char was placed on current line. No need to reposition cursor.
self._col += 1
else:
# At end of line: go to left side next row. Wrap around to first row if on last row.
self._row = (self._row + 1) % self.num_rows
self._col = 0
self.set_cursor_pos(self._row, self._col)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment