Skip to content

Instantly share code, notes, and snippets.

@idriszmy
Created April 11, 2022 09:20
Embed
What would you like to do?
Bidirectional Visitor Counter using CircuitPython on Maker Nano RP2040.
"""
Bidirectional Visitor Counter using CircuitPython on Maker Nano RP2040.
Items:
- Maker Nano RP2040
https://my.cytron.io/maker-nano-rp2040-simplifying-projects-with-raspberry-pi-rp2040
- IO Expansion Shield for Arduino Nano
https://my.cytron.io/p-io-expansion-shield-for-arduino-nano
- Adjustable Infrared Sensor Switch
https://my.cytron.io/c-sensor/c-optical-infrared-sensor/p-adjustable-infrared-sensor-switch
- I2C 1602 Serial LCD
https://my.cytron.io/p-i2c-1602-serial-lcd-for-arduino-and-rpi
- 12mm Momentary Push Button
https://my.cytron.io/p-12mm-momentary-push-button-red
- USB Micro B Cable
https://my.cytron.io/p-usb-micro-b-cable
- 3D Printing products
https://my.cytron.io/c-3d-modeling
Libraries required from bundle (https://circuitpython.org/libraries):
- simpleio.mpy
References:
- https://learn.adafruit.com/rotary-encoder/circuitpython
- https://github.com/dhalbert/CircuitPython_LCD
Last update: 11 Apr 2022
"""
import time
import board
import busio
import simpleio
import rotaryio
from lcd.lcd import LCD
from lcd.i2c_pcf8574_interface import I2CPCF8574Interface
buzzer = board.BUZZER
NOTE_C4 = 261
NOTE_G4 = 392
IR1 = board.GP2
IR2 = board.GP3
sensor = rotaryio.IncrementalEncoder(IR1, IR2)
LCD_SCL = board.GP5
LCD_SDA = board.GP4
LCD_ADDR = 0x27
i2c_lcd = busio.I2C(LCD_SCL, LCD_SDA)
lcd = LCD(I2CPCF8574Interface(i2c_lcd, LCD_ADDR), num_rows=2, num_cols=16)
lcd.print(" Bidirectional\nVisitor Counter")
simpleio.tone(buzzer, NOTE_C4, duration=0.1)
simpleio.tone(buzzer, NOTE_G4, duration=0.15)
time.sleep(2)
lcd.clear()
lcd.print("No of visitor\n0")
last_count = 0
while True:
count = sensor.position
if count != last_count:
print("No of visitor: {} ".format(count))
lcd.set_cursor_pos(1, 0)
lcd.print("{} ".format(count))
if count > last_count:
simpleio.tone(buzzer, NOTE_G4, duration=0.1)
else:
simpleio.tone(buzzer, NOTE_C4, duration=0.1)
last_count = count
# 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