From 939e99957aa7ecdfa419c0bdb5b96697afc5e096 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 24 Dec 2022 22:01:19 -0700 Subject: [PATCH] ports/esp32/boards/PYBOX: Add support SparkFun Thing Plus board. Signed-off-by: Wolf --- ports/esp32/boards/PYBOX/board.json | 23 ++ ports/esp32/boards/PYBOX/board.md | 2 + ports/esp32/boards/PYBOX/manifest.py | 2 + ports/esp32/boards/PYBOX/modules/pybox.py | 148 ++++++++ ports/esp32/boards/PYBOX/modules/pybox_btn.py | 70 ++++ ports/esp32/boards/PYBOX/modules/pybox_ct.py | 56 ++++ ports/esp32/boards/PYBOX/modules/pybox_fg.py | 32 ++ ports/esp32/boards/PYBOX/modules/pybox_i2c.py | 97 ++++++ ports/esp32/boards/PYBOX/modules/pybox_lcd.py | 315 ++++++++++++++++++ ports/esp32/boards/PYBOX/modules/pybox_log.py | 34 ++ ports/esp32/boards/PYBOX/modules/pybox_tz.py | 39 +++ ports/esp32/boards/PYBOX/mpconfigboard.cmake | 7 + ports/esp32/boards/PYBOX/mpconfigboard.h | 2 + ports/esp32/boards/PYBOX/sdkconfig.board | 17 + 14 files changed, 844 insertions(+) create mode 100644 ports/esp32/boards/PYBOX/board.json create mode 100644 ports/esp32/boards/PYBOX/board.md create mode 100644 ports/esp32/boards/PYBOX/manifest.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_btn.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_ct.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_fg.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_i2c.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_lcd.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_log.py create mode 100644 ports/esp32/boards/PYBOX/modules/pybox_tz.py create mode 100644 ports/esp32/boards/PYBOX/mpconfigboard.cmake create mode 100644 ports/esp32/boards/PYBOX/mpconfigboard.h create mode 100644 ports/esp32/boards/PYBOX/sdkconfig.board diff --git a/ports/esp32/boards/PYBOX/board.json b/ports/esp32/boards/PYBOX/board.json new file mode 100644 index 0000000000000..c56579c0e9b2b --- /dev/null +++ b/ports/esp32/boards/PYBOX/board.json @@ -0,0 +1,23 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "Battery Charging", + "Feather", + "RGB LED", + "STEMMA QT/QWIIC", + "BLE", + "WiFi" + ], + "id": "pyBox", + "images": [ + "pyBox.jpg" + ], + "mcu": "esp32", + "product": "pyBox", + "thumbnail": "https://wolfpaulus.com/content/pyBox.jpg", + "url": "https://wolfpaulus.com/pyBox", + "vendor": "Techcasita Productions" +} diff --git a/ports/esp32/boards/PYBOX/board.md b/ports/esp32/boards/PYBOX/board.md new file mode 100644 index 0000000000000..e41c8eaf76a03 --- /dev/null +++ b/ports/esp32/boards/PYBOX/board.md @@ -0,0 +1,2 @@ +The following files for the SparkFun Thing Plus – ESP32 WROOM (USB-C) paired with the SparkFun 16×2 SerLCD – RGB Text (Qwiic). +This firmware is compiled using ESP-IDF v4.4. diff --git a/ports/esp32/boards/PYBOX/manifest.py b/ports/esp32/boards/PYBOX/manifest.py new file mode 100644 index 0000000000000..7ae2ed15d9169 --- /dev/null +++ b/ports/esp32/boards/PYBOX/manifest.py @@ -0,0 +1,2 @@ +include("$(PORT_DIR)/boards/manifest.py") +freeze("modules") diff --git a/ports/esp32/boards/PYBOX/modules/pybox.py b/ports/esp32/boards/PYBOX/modules/pybox.py new file mode 100644 index 0000000000000..49b4197b2acfa --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox.py @@ -0,0 +1,148 @@ +""" + NeoPixel, BlueLED, SDDrive, WiFi for MicroPython on SparkFun Thing Plus + Author: wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +from machine import freq, Pin, SDCard, Signal, RTC +from utime import sleep +from network import WLAN, STA_IF +from neopixel import NeoPixel +from uos import mount +from pybox_btn import Button +from collections import OrderedDict + +_BTN_UP_PIN = const(27) +_BTN_DN_PIN = const(33) +_TURBO_SPEED = const(240000000) +_NORMAL_SPEED = const(160000000) +_BLUE_LED_PIN = const(13) +_NEO_PIN = const(2) + + +class Neo: + """ + NeoPixel + """ + + def __init__(self, pin: int = 2): + self.neo = NeoPixel(Pin(pin, Pin.OUT, value=False), 1) + + def color(self, r: int = 255, g: int = 255, b: int = 255): + self.neo[0] = (r, g, b) + self.neo.write() + + def get_color(self) -> (int, int, int): + return self.neo[0] + + def off(self) -> None: + self.neo[0] = (0, 0, 0) + self.neo.write() + + +def turbo(on: bool = True) -> None: + """ + Switch ESP's clock speed + :param on: 240MHz is True else, defaulting to 160MHz + :return: None + """ + freq(_TURBO_SPEED if on else _NORMAL_SPEED) + + +def mount_sd(directory: str = "/sd") -> None: + """ + Define removable storage media - secure digital memory card + """ + sd = SDCard(slot=2, width=1, cd=None, wp=None, sck=18, miso=19, mosi=23, cs=5, freq=40000000) + mount(sd, directory) + + +def connect(wifi_networks: list) -> bool: + """ + Connect to Wifi router or become an Access Point + :param wifi_networks: list of tuples [(ssid, password),..] + :return: True if connected + """ + global hostname, ip_addr + conn = False + while not conn: + for ssid, pw in wifi_networks: + print(ssid) + sta_if = WLAN(STA_IF) + if not sta_if.isconnected(): + sta_if.active(True) + sta_if.connect(ssid, pw) + # sta_if.config(dhcp_hostname=profile.HostName) + for i in range(2, 8): + sta_if = WLAN(STA_IF) + if sta_if.isconnected(): + conn = True + ip_addr = sta_if.ifconfig()[0] + hostname = sta_if.config("dhcp_hostname") + break + else: + sleep(i) + if conn: + break + return conn + + +def is_connected() -> bool: + """ + :return: True is connected to WiFi + """ + return WLAN(STA_IF).isconnected() + + +def set_system_time(d: dict) -> None: + """ + Set the Systemtime + :param d: dictionary + :return: + """ + RTC().datetime((d["year"], d["month"], d["day"], 0, d["hour"], d["minute"], d["seconds"], 0)) + + +def rgb_color() -> (int, int, int): + """ + RGB color generator (r,g,b in the range 0..255) + :return: (r,g,b all in the range 0..255) + """ + for i in range(1, 86): + yield 255 - i * 3, 0, i * 3 + for i in range(1, 86): + yield 0, i * 3, 255 - i * 3 + for i in range(1, 86): + yield i * 3, 255 - i * 3, 0 + + +palette = OrderedDict( + [ + # red + ("Auriga", (255, 0, 63)), + ("Antares", (255, 0, 0)), + # orange + ("Jupiter", (255, 32, 0)), + ("Proxima", (191, 128, 0)), + # gray + ("Sirius", (255, 255, 255)), + ("Vega", (127, 127, 191)), + # green + ("Nebula", (0, 255, 0)), + ("Kepler", (0, 127, 63)), + ("Uranus", (0, 191, 255)), + # blue + ("Rigel", (31, 31, 255)), + ("Medusa", (127, 0, 255)), + ("Orion", (255, 31, 255)), + ] +) + +blue_led = Signal(Pin(_BLUE_LED_PIN, mode=Pin.OUT, value=False), invert=False) +blue_led.off() +neo = Neo(_NEO_PIN) +neo.color(0, 0, 0) +btn_up = Button(_BTN_UP_PIN) +btn_dn = Button(_BTN_DN_PIN) +hostname = "" +ip_addr = "0.0.0.0" diff --git a/ports/esp32/boards/PYBOX/modules/pybox_btn.py b/ports/esp32/boards/PYBOX/modules/pybox_btn.py new file mode 100644 index 0000000000000..22136ceff91c0 --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_btn.py @@ -0,0 +1,70 @@ +""" + Capacitive Touch Buttons with Press and Long-Press detection + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from machine import Pin, TouchPad +from utime import ticks_ms, ticks_diff +import pybox_ct as ct + + +class Button(TouchPad): + """Capacitive Touch Button""" + + def __init__( + self, + pin: int, + on_press: callable = None, + on_long_press: callable = None, + threshold: int = 600, + hold_ms: int = 500, + freq_ms: int = 100, + ): + """ + :param pin: port pin this button is connected to + :param on_press: function to be called on touch + :param on_long_press: function to be called on long touch + :param threshold: capacitive touch value that qualifies as a touch + :param hold_ms: number of ms button needs to be touch to register a long touch + :param freq_ms: number of ms between checks + """ + super().__init__(Pin(pin)) + self.on_press = on_press + self.on_long_press = on_long_press + self._threshold = threshold + self._hold = hold_ms + self._freq = freq_ms + self._acted = False + self._started = 0 + self._active = True + + def set_actions(self, on_press: callable = None, on_long_press: callable = None): + self.on_press = on_press + self.on_long_press = on_long_press + + def check(self) -> None: + """timer triggered""" + if self._active and (self.on_press or self.on_long_press): + v = self.read() + if v < self._threshold: # button currently touched + if not self._started: + self._started = ticks_ms() + elif self._hold < ticks_diff(ticks_ms(), self._started): # long press detected + if not self._acted: + self._active = False + self.on_long_press() + self._active = self._acted = True + elif self._started: # button was released + if not self._acted: + self._active = False + self.on_press() + self._active = True + else: + self._acted = False + self._started = 0 + + def enable(self): + ct.register((self.check, self._freq)) + + def disable(self): + ct.unregister((self.check, self._freq)) diff --git a/ports/esp32/boards/PYBOX/modules/pybox_ct.py b/ports/esp32/boards/PYBOX/modules/pybox_ct.py new file mode 100644 index 0000000000000..394af7dedbd7f --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_ct.py @@ -0,0 +1,56 @@ +""" + ct - Central Timer + One timer will be used for all of pyBox system calls, leaving remaining three for custom code + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +from machine import Timer +import pybox_log as log + +_TIMER_ID = const(0) +_TICK_MS = const(1) +MIN_FREQ = const(1000 // _TICK_MS * 60 * 60) # about 1 hour +MAX_FREQ = const(_TICK_MS) + + +def process(_) -> None: + """ + this is called every ${frequency}-th of a second and will call every callback that is due. + :return: None + """ + global _counter + _counter = _counter + 1 if _counter < MIN_FREQ else 1 # 1..3_600_000 + for t in _tasks: + if _counter % t[1] == 0: + t[0]() + + +def register(task: ()) -> None: + """ + Registers a callback and call frequency. The callback should have no parameters + and the frequency should be in MAX_FREQ..MIN_FREQ range + :param task: callback,freq + :return: None + """ + _tasks.append(task) + + +def unregister(task: (callable, int)) -> None: + """ + Unregisters a previously registered task + :param task: callback,freq + :return: None + """ + for i in range(len(_tasks) - 1, -1, -1): + if _tasks[i][0].__name__ == task[0].__name__: + _tasks.pop(i) + break + else: + log.log(log.ERROR, f"Could not unregister {task[0].__name__}") + + +_counter = 0 +_tasks = [] +_timer = Timer(_TIMER_ID) +_timer.init(mode=Timer.PERIODIC, period=_TICK_MS, callback=process) diff --git a/ports/esp32/boards/PYBOX/modules/pybox_fg.py b/ports/esp32/boards/PYBOX/modules/pybox_fg.py new file mode 100644 index 0000000000000..f8de13b339277 --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_fg.py @@ -0,0 +1,32 @@ +""" + fg - Fuel Gauge + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +import pybox_i2c as i2c + +DEFAULT_ADDRESS = const(0x36) +_MAX17048_VCELL = const(0x02) +_MAX17048_SOC = const(0x04) + + +def is_connected(address=DEFAULT_ADDRESS): + """ + Determine if a device is connected to the system. + :return: True if the device is connected, otherwise False. + :rtype: bool + """ + return i2c.is_device_connected(address) + + +def voltage(address=DEFAULT_ADDRESS) -> float: + """Current Voltage""" + raw = i2c.read16(address, _MAX17048_VCELL) + return raw * 78.125 / 1_000_000 + + +def remaining(address=DEFAULT_ADDRESS) -> int: + """remaining capacity in percentage""" + raw = i2c.read16(address, _MAX17048_SOC) + return raw // 256 diff --git a/ports/esp32/boards/PYBOX/modules/pybox_i2c.py b/ports/esp32/boards/PYBOX/modules/pybox_i2c.py new file mode 100644 index 0000000000000..1038ef8458441 --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_i2c.py @@ -0,0 +1,97 @@ +""" + Communicate with devices on the i2c bus (i2c / Qwiic connector) + 0x72 Sparkfun 16x2 LCD https://www.sparkfun.com/products/16397 + 0x36 MAX17048 fuel gauge https://cdn.sparkfun.com/assets/b/b/2/c/b/MAX17048.pdf + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +from machine import Pin, I2C + +_I2C_SDA = const(21) +_I2C_SCL = const(22) +_FREQ = const(400000) + + +def write_bytes(address: int, ba: bytearray) -> None: + """Sends a bytearray to the given device address + :param address: I2C address of the device to write to + :param ba: bytes to write + :return: None + """ + _i2c.writeto(address, ba) + + +def write_byte(address: int, data: int) -> None: + """Sends a single byte to the device + :param address: I2C address of the device to write to + :param data: the data will be cast to a byte and then send. + :return: None + """ + ba = bytearray(1) + ba[0] = data + _i2c.writeto(address, ba) + + +def write_cmd(address: int, command: int, value: int) -> None: + """Sends two bytes, e.g. a command and a parameter + :param address: I2C address of the device to write to + :param command: The "command" or register + :param value: The byte to write to the I2C bus + :return: None + """ + ba = bytearray(2) + ba[0] = command + ba[1] = value + write_bytes(address, ba) + + +def write_block(address: int, command: int, values: [int]) -> None: + """Sends a command byte and a data list + :param address: I2C address of the device to write to + :param command: The "command" or register + :param values: a list of ints (cast to bytes) to write on the I2C bus. + :return: None + """ + ba = bytearray(len(values) + 1) + ba[0] = command + for i in range(len(values)): + ba[i + 1] = values[i] + write_bytes(address, ba) + + +def scan() -> []: + """ + Scan the i2c bus for devices + :return: list of device ids + """ + return _i2c.scan() + + +def is_device_connected(address: int) -> bool: + """ + :param address: The I2C address of the device to to look for + :return: True, if the device is on the bus + """ + devices = scan() + if address in devices: + try: + write_byte(address, 0x0) + return True + except Exception as ee: + print("Error connecting to Device: %X, %s" % (address, ee)) + pass + return False + + +def read16(address: int, register: int) -> int: + """ + :param address: The I2C address of the device to to look for + :param register: int + :return: register value + """ + buffer = _i2c.readfrom_mem(address, register, 2) + return buffer[0] << 8 | buffer[1] + + +_i2c = I2C(0, sda=Pin(_I2C_SDA), scl=Pin(_I2C_SCL), freq=_FREQ) diff --git a/ports/esp32/boards/PYBOX/modules/pybox_lcd.py b/ports/esp32/boards/PYBOX/modules/pybox_lcd.py new file mode 100644 index 0000000000000..0664168fd3f08 --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_lcd.py @@ -0,0 +1,315 @@ +""" + Accessing SparkFun's 16x2 SerLCD - RGB Text via i2c + https://www.sparkfun.com/products/16397 + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +from utime import sleep_ms +import pybox_i2c as i2c +import pybox_ct as ct + +_DEFAULT_ADDRESS = const(0x72) +_LCD_DISPLAYCONTROL = const(0x08) +_LCD_ENTRYMODESET = const(0x04) +_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_ENTRYRIGHT = const(0x00) +_LCD_ENTRYLEFT = const(0x02) +_LCD_ENTRYSHIFTINCREMENT = const(0x01) +_LCD_ENTRYSHIFTDECREMENT = const(0x00) +_LCD_SETDDRAMADDR = const(0x80) +_LCD_RETURNHOME = const(0x02) +_SPECIAL_COMMAND = const(0xFE) # Magic number for sending a special command +_SETTING_COMMAND = const(0x7C) # Command to change settings: baud, lines, width,.. +_SET_RGB_COMMAND = const(0x2B) # Command to set backlight RGB value +_ENABLE_SYSTEM_MESSAGE_DISPLAY = const(0x2E) # Command to enable system messages being displayed +_DISABLE_SYSTEM_MESSAGE_DISPLAY = const(0x2F) # Command to disable system messages being displayed +_ENABLE_SPLASH_DISPLAY = const(0x30) # Command to enable splash screen at power on +_DISABLE_SPLASH_DISPLAY = const(0x31) # Command to disable splash screen at power on +_SAVE_CURRENT_DISPLAY_AS_SPLASH = const(0x0A) # Command to save current text on display as splash +_CLEAR_COMMAND = const(0x2D) # Command to clear and home the display +_CONTRAST_COMMAND = const(0x18) # Command to change the contrast setting + +_MAX_ROWS = const(2) +_MAX_COLUMNS = const(16) + + +def special_command(command, count=1) -> None: + """ + Send one (or multiple) special commands to the display. Used by other functions. + :param command: Command to send (a single byte) + :param count: Number of times to send the command (if ommited, then default is once) + :return: Returns true if the I2C write was successful, otherwise False. + :rtype: bool + """ + for i in range(count): + i2c.write_cmd(address, _SPECIAL_COMMAND, command) + sleep_ms(50) + + +def command(command) -> None: + """ + Send one setting command to the display. Used by other functions. + :param command: Command to send (a single byte) + :return: Returns true if the I2C write was successful, otherwise False. + :rtype: bool + """ + i2c.write_cmd(address, _SETTING_COMMAND, command) + + +def is_connected() -> bool: + """ + Determine if a device is connected to the system. + :return: True if the device is connected, otherwise False. + :rtype: bool + """ + return i2c.is_device_connected(address) + + +def enable_system_messages() -> None: + """ + Enable system messages + :return: Returns true if the I2C write was successful, otherwise False. + """ + command(_ENABLE_SYSTEM_MESSAGE_DISPLAY) + sleep_ms(10) + + +def disable_system_messages() -> None: + """ + Disable system messages + :return: Returns true if the I2C write was successful, otherwise False. + """ + command(_DISABLE_SYSTEM_MESSAGE_DISPLAY) + sleep_ms(10) + + +def enable_splash() -> None: + """ + Enable splash screen at power on + :return: Returns true if the I2C write was successful, otherwise False. + """ + command(_ENABLE_SPLASH_DISPLAY) + sleep_ms(10) + + +def disable_splash() -> None: + """ + Disable splash screen at power on + :return: Returns true if the I2C write was successful, otherwise False. + """ + command(_DISABLE_SPLASH_DISPLAY) + sleep_ms(10) + + +def save_splash() -> None: + """ + Save the current display as the splash. Saves whatever is currently being displayed into EEPROM + This will be displayed at next power on as the splash screen + :return: Returns true if the I2C write was successful, otherwise False. + """ + command(_SAVE_CURRENT_DISPLAY_AS_SPLASH) + sleep_ms(10) + + +def set_backlight(r: int, g: int, b: int) -> None: + """ + Set backlight with no LCD messages or delays + :param r: red backlight value 0-255 + :param g: green backlight value 0-255 + :param b: blue backlight value 0-255 + :return: Returns true if the I2C write was successful, otherwise False. + """ + # create a block of data bytes to send to the screen + # This will include the SET_RGB_COMMAND, and three bytes of backlight values + block = [0, 1, 2, 3] + block[0] = _SET_RGB_COMMAND # command + block[1] = r + block[2] = g + block[3] = b + # send the complete bytes (address, settings command , rgb command , red byte, green byte, blue byte) + i2c.write_block(_DEFAULT_ADDRESS, _SETTING_COMMAND, block) + sleep_ms(10) + + +def display(): + """ + Turn the display on quickly. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + + displayControl |= _LCD_DISPLAYON + return special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def no_display() -> None: + """ + Turn the display off quickly. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + displayControl &= _LCD_DISPLAYON + special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def blink() -> None: + """ + Turn the blink cursor on. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + displayControl |= _LCD_BLINKON + special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def no_blink() -> None: + """ + Turn the blink cursor off. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + displayControl &= ~_LCD_BLINKON + special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def cursor() -> None: + """ + Turn the underline cursor on. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + + displayControl |= _LCD_CURSORON + special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def no_cursor() -> None: + """ + Turn the underline cursor off. + :return: Returns true if the I2C write was successful, otherwise False. + """ + global displayControl + displayControl &= ~_LCD_CURSORON + special_command(_LCD_DISPLAYCONTROL | displayControl) + + +def set_cursor(col, row) -> None: + """ + Set the cursor position to a particular column and row. + :param col: The column postion (0-19) + :param row: The row postion (0-3) + :return: Returns true if the I2C write was successful, otherwise False. + """ + row_offsets = [0x00, 0x40, 0x14, 0x54] + + # keep variables in bounds + row = max(0, row) # row cannot be less than 0 + row = min(row, (_MAX_ROWS - 1)) # row cannot be greater than max rows + # construct the cursor "command" + command = _LCD_SETDDRAMADDR | (col + row_offsets[row]) + # send the complete bytes (special command + command) + special_command(command) + + +def set_contrast(contrast) -> None: + """ + Set the contrast of the LCD screen (0-255) + :param contrast: The new contrast value (0-255) + :return: Returns true if the I2C write was successful, otherwise False. + """ + # To set the contrast we need to send 3 bytes: + # (1) SETTINGS_COMMAND + # (2) CONTRAST_COMMAND + # (3) contrast value + # To do this, we are going to use writeBlock(), + # so we need our "block of bytes" to include + # CONTRAST_COMMAND and contrast value + + block = [_CONTRAST_COMMAND, contrast] + # send the complete bytes (address, settings command , contrast command, contrast value) + i2c.write_block(address, _SETTING_COMMAND, block) + sleep_ms(10) + + +def home() -> None: + """ + Send the home command to the display. + This returns the cursor to return to the beginning of the display, + without clearing the display. + :return: Returns true if the I2C write was successful, otherwise False. + """ + special_command(_LCD_RETURNHOME) + sleep_ms(10) + + +def clear_screen() -> None: + """ + Sends the command to clear the screen + :return: Returns true if the I2C write was successful, otherwise False. + """ + print("", "") # this should unregister potentially running scroll tasks + set_backlight(255, 255, 255) + set_contrast(0) + command(_CLEAR_COMMAND) + sleep_ms(10) + + +def print(row0: str = None, row1: str = None) -> None: + """ + Outputs the given rows. + If a provided row in None, its current content will remain, i.e. use "" to clear that row's content + If a provided row's content is longer than the display, the content will scroll + Only a not scrolling row will be immediately updated + :param row0 + :param row1 + :return: None + """ + global scrolling + update = row0 is not None or row1 is not None + if update: + content = [row0, row1] + for r in range(_MAX_ROWS): + if content[r] is not None: + if len(content[r]) > _MAX_COLUMNS: # add a gap for scrolling + content_buffer[r] = content[r] + 3 * " " + else: # fill the row + content_buffer[r] = content[r] + (_MAX_COLUMNS - len(content[r])) * " " + longest = max([len(content_buffer[0]), len(content_buffer[1])]) + if longest > _MAX_COLUMNS: # need_to_scroll + if not scrolling: + scrolling = True + ct.register((print, scroll_speed)) + elif scrolling: + ct.unregister((print, scroll_speed)) + scrolling = False + + s = "" + for r in range(_MAX_ROWS): + if len(content_buffer[r]) > _MAX_COLUMNS and not update: # scroll content + if chr(126) not in content_buffer[r]: # move 1st char to the end + content_buffer[r] = content_buffer[r][1:] + content_buffer[r][0] + else: # (->) in str .. move last char to the start + content_buffer[r] = content_buffer[r][-1] + content_buffer[r][0:-1] + s += content_buffer[r][:_MAX_COLUMNS] + i2c.write_bytes(address, bytes(s, "utf-8")) + if update: + sleep_ms(50) + + +displayControl = _LCD_DISPLAYON | _LCD_CURSOROFF | _LCD_BLINKOFF +displayMode = _LCD_ENTRYLEFT | _LCD_ENTRYSHIFTDECREMENT +content_buffer = [" " * _MAX_COLUMNS, " " * _MAX_COLUMNS] +scrolling = False +address = _DEFAULT_ADDRESS +scroll_speed = 350 # 250 .. 450 looks OK + +special_command(_LCD_DISPLAYCONTROL | displayControl) +special_command(_LCD_ENTRYMODESET | displayMode) +disable_system_messages() +clear_screen() diff --git a/ports/esp32/boards/PYBOX/modules/pybox_log.py b/ports/esp32/boards/PYBOX/modules/pybox_log.py new file mode 100644 index 0000000000000..64c08c02bcf33 --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_log.py @@ -0,0 +1,34 @@ +""" + Minimal Logger + Author: Wolf Paulus wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from micropython import const +from utime import localtime +from uos import remove + +CRITICAL = const(50) +ERROR = const(40) +WARNING = const(30) +INFO = const(20) +DEBUG = const(10) + +LOG_FILE = "/log.txt" + +log_level = WARNING + + +def reset() -> None: + try: + remove(LOG_FILE) + except: + pass + + +def log(level: int, msg: str): + try: + if level >= log_level: + with open(LOG_FILE, "a") as file: + file.write(f"{level} : {localtime()} : {msg}\n") + except: + pass diff --git a/ports/esp32/boards/PYBOX/modules/pybox_tz.py b/ports/esp32/boards/PYBOX/modules/pybox_tz.py new file mode 100644 index 0000000000000..e506447a3802e --- /dev/null +++ b/ports/esp32/boards/PYBOX/modules/pybox_tz.py @@ -0,0 +1,39 @@ +""" + Fetch Time Zone info from timeapi.io + Author: wolf@paulus.com + MIT license; Copyright (c) 2022 wolfpaulus.com +""" +from urequests import request +from utime import sleep +import pybox_log as log + + +def get_time_stamp(tz: str) -> dict: + """ + Fetches the current time stamp at a given time zone: + :param tz: str + :return: { + "year": 2022, + "month": 12, + "day": 18, + "hour": 11, + "minute": 4, + "seconds": 2, + "milliSeconds": 692, + "dateTime": "2022-12-18T11:04:02.6927298", + "date": "12/18/2022", + "time": "11:04", + "timeZone": "America/Phoenix", + "dayOfWeek": "Sunday", + "dstActive": false + } + """ + url = f"https://timeapi.io/api/Time/current/zone?timeZone={tz}" + headers = {"accept": "application/json"} + for i in range(1, 4): + try: + return request("GET", url, headers=headers).json() + except Exception as e: + log.log(log.ERROR, str(e)) + sleep(i) + return {} diff --git a/ports/esp32/boards/PYBOX/mpconfigboard.cmake b/ports/esp32/boards/PYBOX/mpconfigboard.cmake new file mode 100644 index 0000000000000..2a7a06a1f546c --- /dev/null +++ b/ports/esp32/boards/PYBOX/mpconfigboard.cmake @@ -0,0 +1,7 @@ +set(IDF_TARGET esp32) +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.ble + boards/PYBOX/sdkconfig.board +) +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) diff --git a/ports/esp32/boards/PYBOX/mpconfigboard.h b/ports/esp32/boards/PYBOX/mpconfigboard.h new file mode 100644 index 0000000000000..cb8662a191005 --- /dev/null +++ b/ports/esp32/boards/PYBOX/mpconfigboard.h @@ -0,0 +1,2 @@ +#define MICROPY_HW_BOARD_NAME "pyBox" +#define MICROPY_HW_MCU_NAME "ESP32" diff --git a/ports/esp32/boards/PYBOX/sdkconfig.board b/ports/esp32/boards/PYBOX/sdkconfig.board new file mode 100644 index 0000000000000..3d731c8bc1378 --- /dev/null +++ b/ports/esp32/boards/PYBOX/sdkconfig.board @@ -0,0 +1,17 @@ +CONFIG_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y +CONFIG_ESPTOOLPY_AFTER_NORESET=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB= +CONFIG_ESPTOOLPY_FLASHSIZE_8MB= +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-16MiB.csv" + +# Network name + +CONFIG_LWIP_LOCAL_HOSTNAME="pyBox" + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy