# Drivers for KeyeStudio starter it FKS0001
# Copyright by Anthony Vogelaar, Perth Scotland
# rev 0.10  2025-04-28  initial release

# ======================== imported mve_hd4478_i2c.py ==========================

# Driver for 2 line, 16 character I2C LCD using HD44780 and PCF8574
#    implementing lprint where i2c scl=22 and sda21
# Copyright by brainelectronics, contributers and Anthony Vogelaar
# see also: https://github.com/brainelectronics/micropython-i2c-lcd
# rev 0.10  2025-04-28  initial release
# rev 0.11  2025-05-05  separator between items ',' replaced by ' '

from	micropython		import	const
import	time

# Implemented:
# LCD(i2c, addr=0x27, cols=16, rows=0, charsize=0)
#    LCD.clear()
#    LCD.home()
#    LCD.no_display()
#    LCD.display()
#    LCD.no_blink()
#    LCD.blink()
#    LCD.blink_on()
#    LCD.blink_off()
#    LCD.no_cursor()
#    LCD.cursor()
#    LCD.cursor_on()
#    LCD.cursor_off()
#    LCD.set_cursor(col, row)
#    LCD.scroll_display_left()
#    LCD.scroll_display_right()
#    LCD.left_to_right()
#    LCD.right_to_left()
#    LCD.no_backlight()
#    LCD.backlight()
#    LCD.set_backlight(new_val)
#    LCD.get_backlight()
#    LCD.autoscroll()
#    LCD.no_autoscroll()
#    LCD.create_char(location, charmap)
#    LCD.printAbs(text)
#    LCD.print(*items)

# ======================== Constants for HD44780 ==============================

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)

LCD_ENTRYRIGHT 			= const(0x00)
LCD_ENTRYLEFT 			= const(0x02)
LCD_ENTRYSHIFTINCREMENT = const(0x01)
LCD_ENTRYSHIFTDECREMENT = const(0x00)

LCD_DISPLAYON 			= const(0x04)
LCD_DISPLAYOFF 			= const(0x00)
LCD_CURSORON 			= const(0x02)
LCD_CURSOROFF 			= const(0x00)
LCD_BLINKON 			= const(0x01)
LCD_BLINKOFF 			= const(0x00)

LCD_DISPLAYMOVE 		= const(0x08)
LCD_CURSORMOVE 			= const(0x00)
LCD_MOVERIGHT 			= const(0x04)
LCD_MOVELEFT 			= const(0x00)

LCD_8BITMODE 			= const(0x10)
LCD_4BITMODE 			= const(0x00)
LCD_2LINE 				= const(0x08)
LCD_1LINE 				= const(0x00)
LCD_5x10DOTS 			= const(0x04)
LCD_5x8DOTS 			= const(0x00)

LCD_BACKLIGHT 			= const(0x08)
LCD_NOBACKLIGHT 		= const(0x00)

LCD_EN 					= const(0b00000100)
LCD_RW 					= const(0b00000010)
LCD_RS 					= const(0b00000001)

# ============================== 2*16 I2C LCD =================================

class LCD:

    def __init__(self, i2c, addr=0x27, cols=16, rows=2, charsize=0):
        self._addr             = addr
        self._cols             = cols
        self._rows             = rows
        self._charsize         = charsize
        self._backlightval     = LCD_BACKLIGHT
        self._i2c              = i2c
        self._display_control  = 0
        self._display_mode     = 0
        self._display_function = 0
        self._cursor_position  = (0, 0)  				# (x, y)
        self.last_line         = ''
        self._display_function = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS
        if self._rows > 1:
            self._display_function |= LCD_2LINE
        if (self._charsize != 0) and (self.rows == 1):
            self._display_function |= LCD_5x10DOTS
        time.sleep_ms(50)
        self._expander_write(value=self._backlightval)
        time.sleep(1)
        for _ in range(0, 3):
            self._write_4_bits(value=(0x03 << 4))
            time.sleep_us(4500)  # wait minimum 4.1ms
        self._write_4_bits(value=(0x02 << 4))
        self._command(value=(LCD_FUNCTIONSET | self._display_function))
        self._display_control = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF
        self.display()
        self.clear()
        self._display_mode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT
        self._command(value=(LCD_ENTRYMODESET | self._display_mode))
        self.home()

    def clear(self):
        self._command(value=LCD_CLEARDISPLAY)
        time.sleep_ms(2)     # this command takes a long time!
        self._cursor_position = (0, 0)   # (x, y)

    def home(self):
        self._command(value=LCD_RETURNHOME)
        time.sleep_ms(2)     # this command takes a long time!
        self._cursor_position = (0, 0)   # (x, y)

    def no_display(self):
        self._display_control &= ~LCD_DISPLAYON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def display(self):
        self._display_control |= LCD_DISPLAYON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def no_blink(self):
        self._display_control &= ~LCD_BLINKON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def blink(self):
        self._display_control |= LCD_BLINKON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def blink_on(self):
        self.blink()

    def blink_off(self):
        self.no_blink()

    def no_cursor(self):
        self._display_control &= ~LCD_CURSORON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def cursor(self):
        self._display_control |= LCD_CURSORON
        self._command(value=(LCD_DISPLAYCONTROL | self._display_control))

    def cursor_on(self):
        self.cursor()

    def cursor_off(self):
        self.no_cursor()

    def set_cursor(self, col, row):
        row_offsets = [0x00, 0x40, 0x14, 0x54]
        if row > (self._rows - 1):
            row = self._rows - 1
        self._command(value=(LCD_SETDDRAMADDR | (col + row_offsets[row])))
        self._cursor_position = (col, row)   # (x, y)

    def scroll_display_left(self):
        self._command(value=(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT))   # noqa: E501

    def scroll_display_right(self):
        self._command(value=(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT))  # noqa: E501

    def left_to_right(self):
        self._display_mode |= LCD_ENTRYLEFT
        self._command(value=(LCD_ENTRYMODESET | self._display_mode))

    def right_to_left(self):
        self._display_mode &= ~LCD_ENTRYLEFT
        self._command(value=(LCD_ENTRYMODESET | self._display_mode))

    def no_backlight(self):
        self._backlightval = LCD_NOBACKLIGHT
        self._expander_write(value=0)

    def backlight(self):
        self._backlightval = LCD_BACKLIGHT
        self._expander_write(value=0)

    def set_backlight(self, new_val):
        if new_val: self.backlight()        # turn backlight on
        else      : self.no_backlight()     # turn backlight off

    def get_backlight(self):
        return self._backlightval == LCD_BACKLIGHT

    def autoscroll(self):
        self._display_mode |= LCD_ENTRYSHIFTINCREMENT
        self._command(value=(LCD_ENTRYMODESET | self._display_mode))

    def no_autoscroll(self):
        self._display_mode &= ~LCD_ENTRYSHIFTINCREMENT
        self._command(value=(LCD_ENTRYMODESET | self._display_mode))

    def create_char(self, location: int, charmap: List[int]) -> None:
        """
        Fill the first 8 CGRAM locations with custom characters

        :param      location:  The location to store the custom character
        :type       location:  int
        :param      charmap:   The charmap aka custom character
        :type       charmap:   List[int]
        """
        location &= 0x7     # we only have 8, locations 0-7
        self._command(value=(LCD_SETCGRAMADDR | location << 3))
        time.sleep_us(40)
        for x in range(0, 8):
            self._command(value=charmap[x], mode=LCD_RS)
            time.sleep_us(40)

    # print at position as set with set_cursor or continue from last position
    def printAbs(self, text):
        _cursor_x, _cursor_y = self._cursor_position
        for char in text:
            self._command(value=ord(char), mode=LCD_RS)
        self._cursor_position = (_cursor_x + len(text), _cursor_y)

    #print multiple items, comma separated, up-scrolling.  Duration 71 mSec @ 240 MHz
    def print(self, *items):
        self.clear()
        self.printAbs(self.last_line)
        self.set_cursor(0, 1)
        res = ''
        for item in items:
            if res: res += ' '
            res += str(item)
        res = res[:16]
        self.printAbs(res)
        self.last_line = res
        
    def _command(self, value, mode=0):
        high_nib = value & 0xF0
        low_nib = (value << 4) & 0xF0
        self._write_4_bits(value=(high_nib | mode))
        self._write_4_bits(value=(low_nib | mode))

    def _write_4_bits(self, value):
        self._expander_write(value=value)
        self._pulse_enable(value=value)

    def _pulse_enable(self, value):
        self._expander_write(value=(value | LCD_EN))
        time.sleep_us(1)
        self._expander_write(value=(value & ~LCD_EN))
        time.sleep_us(50)

    def _expander_write(self, value):
        self._i2c.writeto(self._addr, bytes([value | self._backlightval]))

# ====================== Hardware setup for KeyeStudio =======================

from 	machine 	import 	Pin, I2C, freq;		freq(240_000_000)

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
lcd = LCD(i2c)

# ================================ Drivers ===================================

# print items to a line on LCD like print to console
def lprint(*items):
    lcd.print(*items)
    
# ================================== Test ====================================

if __name__ == '__main__':
    
    n = 0
    while True:
        n += 1
        lcd.print(f'line #{n}', 'ok', 'Hello world')	# 71 mSec  @ 240 MHz
        time.sleep(1)

# ================================== End =====================================
