diff --git a/ports/esp32/boards/PYBOX/board.json b/ports/esp32/boards/PYBOX/board.json new file mode 100644 index 0000000000..c56579c0e9 --- /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 0000000000..e41c8eaf76 --- /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 0000000000..7ae2ed15d9 --- /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 0000000000..49b4197b2a --- /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 0000000000..22136ceff9 --- /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 0000000000..394af7dedb --- /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 0000000000..f8de13b339 --- /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 0000000000..1038ef8458 --- /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 0000000000..0664168fd3 --- /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 0000000000..64c08c02bc --- /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 0000000000..e506447a38 --- /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 0000000000..2a7a06a1f5 --- /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 0000000000..cb8662a191 --- /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 0000000000..3d731c8bc1 --- /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" + + +