From 3323ef49114f0dedbe411fff5a9f99f17bf48ab8 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Thu, 16 May 2024 23:10:38 +0100 Subject: [PATCH 1/4] Basic driver for and implementation of states on display --- smibhid/lib/LCD1602.py | 120 +++++++++++++++++++++++++++++++++++++++++ smibhid/lib/display.py | 36 +++++++++++++ smibhid/lib/hid.py | 9 ++++ 3 files changed, 165 insertions(+) create mode 100644 smibhid/lib/LCD1602.py create mode 100644 smibhid/lib/display.py diff --git a/smibhid/lib/LCD1602.py b/smibhid/lib/LCD1602.py new file mode 100644 index 0000000..bd26b24 --- /dev/null +++ b/smibhid/lib/LCD1602.py @@ -0,0 +1,120 @@ +## Originally copied from https://files.waveshare.com/upload/d/db/LCD1602_I2C_Module_code.zip + +# -*- coding: utf-8 -*- +import time +from machine import Pin,I2C + +LCD1602_SDA = Pin(8) +LCD1602_SCL = Pin(9) + +LCD1602_I2C = I2C(0,sda = LCD1602_SDA,scl = LCD1602_SCL ,freq = 400000) + +#Device I2C Arress +LCD_ADDRESS = (0x7c>>1) + +LCD_CLEARDISPLAY = 0x01 +LCD_RETURNHOME = 0x02 +LCD_ENTRYMODESET = 0x04 +LCD_DISPLAYCONTROL = 0x08 +LCD_CURSORSHIFT = 0x10 +LCD_FUNCTIONSET = 0x20 +LCD_SETCGRAMADDR = 0x40 +LCD_SETDDRAMADDR = 0x80 + +#flags for display entry mode +LCD_ENTRYRIGHT = 0x00 +LCD_ENTRYLEFT = 0x02 +LCD_ENTRYSHIFTINCREMENT = 0x01 +LCD_ENTRYSHIFTDECREMENT = 0x00 + +#flags for display on/off control +LCD_DISPLAYON = 0x04 +LCD_DISPLAYOFF = 0x00 +LCD_CURSORON = 0x02 +LCD_CURSOROFF = 0x00 +LCD_BLINKON = 0x01 +LCD_BLINKOFF = 0x00 + +#flags for display/cursor shift +LCD_DISPLAYMOVE = 0x08 +LCD_CURSORMOVE = 0x00 +LCD_MOVERIGHT = 0x04 +LCD_MOVELEFT = 0x00 + +#flags for function set +LCD_8BITMODE = 0x10 +LCD_4BITMODE = 0x00 +LCD_2LINE = 0x08 +LCD_1LINE = 0x00 +LCD_5x8DOTS = 0x00 + + +class LCD1602: + def __init__(self, col, row): # TODO move I2C pins and number here + self._row = row + self._col = col + + self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS + self.begin(self._row,self._col) + + + def command(self,cmd): + LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x80, chr(cmd)) + + def write(self,data): + LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x40, chr(data)) + + def setCursor(self,col,row): + if(row == 0): + col|=0x80 + else: + col|=0xc0 + LCD1602_I2C.writeto(LCD_ADDRESS, bytearray([0x80,col])) + + def clear(self): + self.command(LCD_CLEARDISPLAY) + time.sleep(0.002) + def printout(self,arg): + if(isinstance(arg,int)): + arg=str(arg) + + for x in bytearray(arg,'utf-8'): + self.write(x) + + + def display(self): + self._showcontrol |= LCD_DISPLAYON + self.command(LCD_DISPLAYCONTROL | self._showcontrol) + + + def begin(self,cols,lines): + if (lines > 1): + self._showfunction |= LCD_2LINE + + self._numlines = lines + self._currline = 0 + + time.sleep(0.05) + + # Send function set command sequence + self.command(LCD_FUNCTIONSET | self._showfunction) + #delayMicroseconds(4500); # wait more than 4.1ms + time.sleep(0.005) + # second try + self.command(LCD_FUNCTIONSET | self._showfunction) + #delayMicroseconds(150); + time.sleep(0.005) + # third go + self.command(LCD_FUNCTIONSET | self._showfunction) + # finally, set # lines, font size, etc. + self.command(LCD_FUNCTIONSET | self._showfunction) + # turn the display on with no cursor or blinking default + self._showcontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF + self.display() + # clear it off + self.clear() + # Initialize to default text direction (for romance languages) + self._showmode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT + # set the entry mode + self.command(LCD_ENTRYMODESET | self._showmode) + # backlight init diff --git a/smibhid/lib/display.py b/smibhid/lib/display.py new file mode 100644 index 0000000..1810ed7 --- /dev/null +++ b/smibhid/lib/display.py @@ -0,0 +1,36 @@ +from LCD1602 import LCD1602 + +def check_enabled(method): + def wrapper(self, *args, **kwargs): + if self.enabled: + return method(self, *args, **kwargs) + return None + return wrapper + +class Display: + def __init__(self) -> None: + self.lcd = LCD1602(16, 2) + self.enabled = True + + @check_enabled + def clear(self) -> None: + self.lcd.clear() + + def _text_to_line(self, text: str) -> str: + text = text[:16] + text = "{:<16}".format(text) + return text + + @check_enabled + def print_top_line(self, text: str) -> None: + self.lcd.setCursor(0, 0) + self.lcd.printout(self._text_to_line(text)) + + @check_enabled + def print_bottom_line(self, text: str) -> None: + self.lcd.setCursor(0, 1) + self.lcd.printout(self._text_to_line(text)) + + @check_enabled + def print_space_state(self, state: str) -> None: + self.print_bottom_line(f"Space: {state}") \ No newline at end of file diff --git a/smibhid/lib/hid.py b/smibhid/lib/hid.py index 2c2e424..a577560 100644 --- a/smibhid/lib/hid.py +++ b/smibhid/lib/hid.py @@ -6,6 +6,7 @@ from slack_api import Wrapper from lib.networking import WirelessNetwork from constants import OPEN, CLOSED +from display import Display class HID: @@ -31,6 +32,7 @@ def __init__(self, loglevel: int) -> None: self.space_state_check_in_error_state = False self.checking_space_state = False self.checking_space_state_timeout_s = 30 + self.display = Display() self.space_state_poll_frequency = config.space_state_poll_frequency_s if self.space_state_poll_frequency != 0 and self.space_state_poll_frequency < 5: @@ -41,6 +43,9 @@ def startup(self) -> None: Initialise all aysnc services for the HID. """ self.log.info("Starting HID") + self.display.clear() + self.display.print_top_line("S.M.I.B.H.I.D.") + self.display.print_bottom_line("Starting up...") self.log.info(f"Starting {self.open_button.get_name()} button watcher") create_task(self.open_button.wait_for_press()) self.log.info(f"Starting {self.closed_button.get_name()} button watcher") @@ -67,6 +72,7 @@ def set_output_space_open(self) -> None: self.space_state = True self.space_open_led.on() self.space_closed_led.off() + self.display.print_space_state("Open") self.log.info("Space state is open.") def set_output_space_closed(self) -> None: @@ -74,6 +80,7 @@ def set_output_space_closed(self) -> None: self.space_state = False self.space_open_led.off() self.space_closed_led.on() + self.display.print_space_state("Closed") self.log.info("Space state is closed.") def set_output_space_none(self) -> None: @@ -81,6 +88,7 @@ def set_output_space_none(self) -> None: self.space_state = None self.space_open_led.off() self.space_closed_led.off() + self.display.print_space_state("None") self.log.info("Space state is none.") def _set_space_state_check_to_error(self) -> None: @@ -89,6 +97,7 @@ def _set_space_state_check_to_error(self) -> None: self.space_state_check_in_error_state = True self.state_check_error_open_led_flash_task = create_task(self.space_open_led.async_constant_flash(2)) self.state_check_error_closed_led_flash_task = create_task(self.space_closed_led.async_constant_flash(2)) + self.display.print_space_state("Error") def _set_space_state_check_to_ok(self) -> None: """Activities relating to space_state check moving to ok state""" From 4fe9bbaa93bddde01bc76537f1ab51f5f347cf55 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Fri, 17 May 2024 00:18:51 +0100 Subject: [PATCH 2/4] Add config for I2C pins and error checking for display present. Disable display if error initialising. --- smibhid/config.py | 7 ++++++- smibhid/lib/LCD1602.py | 26 ++++++++++++++------------ smibhid/lib/display.py | 12 ++++++++++-- smibhid/lib/hid.py | 2 +- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/smibhid/config.py b/smibhid/config.py index 366b8eb..6ac7e9d 100644 --- a/smibhid/config.py +++ b/smibhid/config.py @@ -18,4 +18,9 @@ ## Space state # Set the space state poll frequency in seconds (>= 5), set to 0 to disable the state poll -space_state_poll_frequency_s = 5 \ No newline at end of file +space_state_poll_frequency_s = 5 + +## I2C +SDA_PIN = 8 +SCL_PIN = 9 +I2C_ID = 0 \ No newline at end of file diff --git a/smibhid/lib/LCD1602.py b/smibhid/lib/LCD1602.py index bd26b24..0433559 100644 --- a/smibhid/lib/LCD1602.py +++ b/smibhid/lib/LCD1602.py @@ -3,11 +3,7 @@ # -*- coding: utf-8 -*- import time from machine import Pin,I2C - -LCD1602_SDA = Pin(8) -LCD1602_SCL = Pin(9) - -LCD1602_I2C = I2C(0,sda = LCD1602_SDA,scl = LCD1602_SCL ,freq = 400000) +from ulogging import uLogger #Device I2C Arress LCD_ADDRESS = (0x7c>>1) @@ -50,26 +46,32 @@ class LCD1602: - def __init__(self, col, row): # TODO move I2C pins and number here + def __init__(self, log_level: int, i2c_id: int, i2c_sda: int, i2c_scl: int, col: int, row: int): + self.log = uLogger("LCD1602", log_level) + self.log.info("Init LCD1602 display driver") self._row = row self._col = col - self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS - self.begin(self._row,self._col) - + try: + self.LCD1602_I2C = I2C(i2c_id, sda = i2c_sda, scl = i2c_scl ,freq = 400000) + self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS + self.begin(self._row,self._col) + except BaseException: + self.log.error("Error connecting to LCD display on I2C bus. Check I2C pins and ID and that correct module (I2C address) is connected.") + raise def command(self,cmd): - LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x80, chr(cmd)) + self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x80, chr(cmd)) def write(self,data): - LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x40, chr(data)) + self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x40, chr(data)) def setCursor(self,col,row): if(row == 0): col|=0x80 else: col|=0xc0 - LCD1602_I2C.writeto(LCD_ADDRESS, bytearray([0x80,col])) + self.LCD1602_I2C.writeto(LCD_ADDRESS, bytearray([0x80,col])) def clear(self): self.command(LCD_CLEARDISPLAY) diff --git a/smibhid/lib/display.py b/smibhid/lib/display.py index 1810ed7..6e7fbaa 100644 --- a/smibhid/lib/display.py +++ b/smibhid/lib/display.py @@ -1,4 +1,6 @@ from LCD1602 import LCD1602 +from config import I2C_ID, SCL_PIN, SDA_PIN +from ulogging import uLogger def check_enabled(method): def wrapper(self, *args, **kwargs): @@ -8,9 +10,15 @@ def wrapper(self, *args, **kwargs): return wrapper class Display: - def __init__(self) -> None: - self.lcd = LCD1602(16, 2) + def __init__(self, log_level: int) -> None: + self.log = uLogger("Display", log_level) + self.log.info("Init display") self.enabled = True + try: + self.lcd = LCD1602(log_level, I2C_ID, SDA_PIN, SCL_PIN, 16, 2) + except Exception: + self.log.error("Error initialising display on I2C bus. Disabling display functionality.") + self.enabled = False @check_enabled def clear(self) -> None: diff --git a/smibhid/lib/hid.py b/smibhid/lib/hid.py index a577560..9fc00f8 100644 --- a/smibhid/lib/hid.py +++ b/smibhid/lib/hid.py @@ -32,7 +32,7 @@ def __init__(self, loglevel: int) -> None: self.space_state_check_in_error_state = False self.checking_space_state = False self.checking_space_state_timeout_s = 30 - self.display = Display() + self.display = Display(loglevel) self.space_state_poll_frequency = config.space_state_poll_frequency_s if self.space_state_poll_frequency != 0 and self.space_state_poll_frequency < 5: From 7d5c5bfb2ddadadc798db78d022f92bca81ec307 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Fri, 17 May 2024 18:44:02 +0100 Subject: [PATCH 3/4] Docstrings and tidying --- smibhid/lib/LCD1602.py | 160 +++++++++++++++++++++-------------------- smibhid/lib/display.py | 11 +++ 2 files changed, 94 insertions(+), 77 deletions(-) diff --git a/smibhid/lib/LCD1602.py b/smibhid/lib/LCD1602.py index 0433559..92f51d7 100644 --- a/smibhid/lib/LCD1602.py +++ b/smibhid/lib/LCD1602.py @@ -1,11 +1,11 @@ ## Originally copied from https://files.waveshare.com/upload/d/db/LCD1602_I2C_Module_code.zip # -*- coding: utf-8 -*- -import time -from machine import Pin,I2C +from time import sleep +from machine import I2C from ulogging import uLogger -#Device I2C Arress +#Device I2C address LCD_ADDRESS = (0x7c>>1) LCD_CLEARDISPLAY = 0x01 @@ -44,79 +44,85 @@ LCD_1LINE = 0x00 LCD_5x8DOTS = 0x00 - class LCD1602: - def __init__(self, log_level: int, i2c_id: int, i2c_sda: int, i2c_scl: int, col: int, row: int): - self.log = uLogger("LCD1602", log_level) - self.log.info("Init LCD1602 display driver") - self._row = row - self._col = col - - try: - self.LCD1602_I2C = I2C(i2c_id, sda = i2c_sda, scl = i2c_scl ,freq = 400000) - self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS - self.begin(self._row,self._col) - except BaseException: - self.log.error("Error connecting to LCD display on I2C bus. Check I2C pins and ID and that correct module (I2C address) is connected.") - raise - - def command(self,cmd): - self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x80, chr(cmd)) - - def write(self,data): - self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x40, chr(data)) - - def setCursor(self,col,row): - if(row == 0): - col|=0x80 - else: - col|=0xc0 - self.LCD1602_I2C.writeto(LCD_ADDRESS, bytearray([0x80,col])) - - def clear(self): - self.command(LCD_CLEARDISPLAY) - time.sleep(0.002) - def printout(self,arg): - if(isinstance(arg,int)): - arg=str(arg) - - for x in bytearray(arg,'utf-8'): - self.write(x) - - - def display(self): - self._showcontrol |= LCD_DISPLAYON - self.command(LCD_DISPLAYCONTROL | self._showcontrol) - + """Drive for the LCD1602 16x2 character LED display""" + def __init__(self, log_level: int, i2c_id: int, i2c_sda: int, i2c_scl: int, col: int, row: int) -> None: + """Configure and connect to display via I2C, throw error on connection issue.""" + self.log = uLogger("LCD1602", log_level) + self.log.info("Init LCD1602 display driver") + self._row = row + self._col = col + + try: + self.LCD1602_I2C = I2C(i2c_id, sda = i2c_sda, scl = i2c_scl ,freq = 400000) + self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS + self._begin(self._row) + except BaseException: + self.log.error("Error connecting to LCD display on I2C bus. Check I2C pins and ID and that correct module (I2C address) is connected.") + raise + + def _command(self, cmd: int) -> None: + """Execute a command against the display driver. Refer to command constants.""" + self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x80, chr(cmd)) + + def _write(self, data: int) -> None: + self.LCD1602_I2C.writeto_mem(LCD_ADDRESS, 0x40, chr(data)) + + def setCursor(self, col: int, row: int) -> None: + """Position the cursor ahead of writing a character or string.""" + if(row == 0): + col|=0x80 + else: + col|=0xc0 + self.LCD1602_I2C.writeto(LCD_ADDRESS, bytearray([0x80, col])) + + def clear(self) -> None: + """Clear the entire screen.""" + self._command(LCD_CLEARDISPLAY) + sleep(0.002) + + def printout(self, arg: str) -> None: + """Print a string to the cursor position.""" + if(isinstance(arg, int)): + arg=str(arg) + + for x in bytearray(arg, 'utf-8'): + self._write(x) + + def _display(self) -> None: + """Turn on display""" + self._showcontrol |= LCD_DISPLAYON + self._command(LCD_DISPLAYCONTROL | self._showcontrol) - def begin(self,cols,lines): - if (lines > 1): - self._showfunction |= LCD_2LINE - - self._numlines = lines - self._currline = 0 - - time.sleep(0.05) - - # Send function set command sequence - self.command(LCD_FUNCTIONSET | self._showfunction) - #delayMicroseconds(4500); # wait more than 4.1ms - time.sleep(0.005) - # second try - self.command(LCD_FUNCTIONSET | self._showfunction) - #delayMicroseconds(150); - time.sleep(0.005) - # third go - self.command(LCD_FUNCTIONSET | self._showfunction) - # finally, set # lines, font size, etc. - self.command(LCD_FUNCTIONSET | self._showfunction) - # turn the display on with no cursor or blinking default - self._showcontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF - self.display() - # clear it off - self.clear() - # Initialize to default text direction (for romance languages) - self._showmode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT - # set the entry mode - self.command(LCD_ENTRYMODESET | self._showmode) - # backlight init + def _begin(self, lines: int) -> None: + """Configure and set initial display output""" + if (lines > 1): + self._showfunction |= LCD_2LINE + + self._numlines = lines + self._currline = 0 + + sleep(0.05) + + # Send function set command sequence + self._command(LCD_FUNCTIONSET | self._showfunction) + #delayMicroseconds(4500); # wait more than 4.1ms + sleep(0.005) + # second try + self._command(LCD_FUNCTIONSET | self._showfunction) + #delayMicroseconds(150); + sleep(0.005) + # third go + self._command(LCD_FUNCTIONSET | self._showfunction) + # finally, set # lines, font size, etc. + self._command(LCD_FUNCTIONSET | self._showfunction) + # turn the display on with no cursor or blinking default + self._showcontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF + self._display() + # clear it off + self.clear() + # Initialize to default text direction (for romance languages) + self._showmode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT + # set the entry mode + self._command(LCD_ENTRYMODESET | self._showmode) + # backlight init diff --git a/smibhid/lib/display.py b/smibhid/lib/display.py index 6e7fbaa..ace7d5c 100644 --- a/smibhid/lib/display.py +++ b/smibhid/lib/display.py @@ -10,7 +10,13 @@ def wrapper(self, *args, **kwargs): return wrapper class Display: + """ + Display management for SMIBHID to drive displays via driver classes abstracting display constraints from the messaging required. + Provides functions to clear display and print top or bottom line text. + Currently supports 2x16 character LCD display. + """ def __init__(self, log_level: int) -> None: + """Connect to display using configu file values for I2C""" self.log = uLogger("Display", log_level) self.log.info("Init display") self.enabled = True @@ -22,23 +28,28 @@ def __init__(self, log_level: int) -> None: @check_enabled def clear(self) -> None: + """Clear entire screen""" self.lcd.clear() def _text_to_line(self, text: str) -> str: + """Internal function to ensure line fits the screen and no previous line text is present for short strings.""" text = text[:16] text = "{:<16}".format(text) return text @check_enabled def print_top_line(self, text: str) -> None: + """Print up to 16 characters on the top line.""" self.lcd.setCursor(0, 0) self.lcd.printout(self._text_to_line(text)) @check_enabled def print_bottom_line(self, text: str) -> None: + """Print up to 16 characters on the bottom line.""" self.lcd.setCursor(0, 1) self.lcd.printout(self._text_to_line(text)) @check_enabled def print_space_state(self, state: str) -> None: + """Abstraction for space state formatting and placement.""" self.print_bottom_line(f"Space: {state}") \ No newline at end of file From 58c6ea496bc23b89e00acd8968be9661564454cf Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Fri, 17 May 2024 19:26:39 +0100 Subject: [PATCH 4/4] Fix state output not set on space state check working after failure. --- smibhid/lib/hid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smibhid/lib/hid.py b/smibhid/lib/hid.py index 9fc00f8..4640eed 100644 --- a/smibhid/lib/hid.py +++ b/smibhid/lib/hid.py @@ -107,6 +107,7 @@ def _set_space_state_check_to_ok(self) -> None: self.state_check_error_closed_led_flash_task.cancel() self.space_open_led.off() self.space_closed_led.off() + self._set_space_output(self.space_state) def _free_to_check_space_state(self) -> bool: """Check that we're not already checking for space state"""