# ESP32 - Drivers for starter kits like KeyeStudio FKS0001 and RW2050
# Copyright by Anthony Vogelaar, Perth Scotland and brainelectronics (see below)
# rev 0.10  2025-05-15  initial release

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

# Driver for 2 line, 16 character I2C LCD using HD44780 and PCF8574
#    implementing lprint where default i2c scl=22 and sda21
# Copyright LCD  : brainelectronics, contributers and Anthony Vogelaar
# Copyright other: Anthony Vogelaar, Perth Scotland
# see also: https://github.com/brainelectronics/micropython-i2c-lcd
# rev 0.10  2025-05-15  initial release

# ===================================== Copyright ==============================
'''
MIT License

Copyright (c) 2023 brainelectronics and contributors
Copyright (c) 2025 Anthony Vogelaar

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.
'''
# ================================ end copyright ===============================

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

# Implemented:
# Lcd(scl=22, sda=21, 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)

# ============================= LCD =====================================

# 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, pin_scl=22, pin_sda=21, addr=0x27, cols=16, rows=2, charsize=0):
        self._i2c              = I2C(0, scl=Pin(pin_scl), sda=Pin(pin_sda), freq=400000)
        self._addr             = addr
        self._cols             = cols
        self._rows             = rows
        self._charsize         = charsize
        self._backlightval     = LCD_BACKLIGHT
        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]))

# ================================== Test ====================================

if __name__ == '__main__':
    
    def test():
        lcd = Lcd()
        lcd.print('Hello')
        lcd.print('    world!')


    test()
    
# ================================== End =====================================
