diff --git a/.flake8 b/.flake8 index dc47994..6692937 100644 --- a/.flake8 +++ b/.flake8 @@ -43,10 +43,8 @@ exclude = .idea .bak # custom scripts, not being part of the distribution - libs_external sdist_upip.py setup.py - nextion/ulogging.py # Provide a comma-separated list of glob patterns to add to the list of excluded ones. # extend-exclude = diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24093d9..1129d6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,10 +68,11 @@ jobs: - name: Test build package run: | twine check dist/* - # - name: Validate mip package file - # run: | - # upy-package \ - # --setup_file setup.py \ - # --package_changelog_file changelog.md \ - # --package_file package.json \ - # --validate + - name: Validate mip package file + run: | + upy-package \ + --setup_file setup.py \ + --package_changelog_file changelog.md \ + --package_file package.json \ + --validate \ + --ignore-version diff --git a/QUICKSTART.md b/QUICKSTART.md index af14205..70d77c5 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -22,38 +22,46 @@ esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART erase_flash esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART --baud 921600 write_flash -z 0x1000 esp32spiram-20220117-v1.18.bin ``` -### Install package on board with pip +### Install package -```bash -rshell -p /dev/tty.SLAB_USBtoUART --editor nano +Connect the MicroPython device to a network (if possible) + +```python +import network +station = network.WLAN(network.STA_IF) +station.active(True) +station.connect('SSID', 'PASSWORD') +station.isconnected() ``` -Inside the rshell +Install the latest package version of this lib on the MicroPython device -```bash -cp examples/progressbar/main.py /pyboard -cp examples/boot.py /pyboard -repl +```python +import mip +mip.install("github:brainelectronics/micropython-nextion") ``` -Inside the REPL +For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` ```python -import machine -import network -import time import upip +upip.install('micropython-nextion') +``` -station = network.WLAN(network.STA_IF) -station.active(True) -station.connect('SSID', 'PASSWORD') -time.sleep(1) -print('Device connected to network: {}'.format(station.isconnected())) +### Copy example file -upip.install('micropython-nextion') +Copy one of the provided example `main.py` files to the MicroPython device. -print('Installation completed') -machine.soft_reset() +```bash +rshell --port /dev/tty.SLAB_USBtoUART --editor nano +``` + +Perform the following command inside the `rshell` to copy the Progressbar example to the MicroPython device. + +```bash +cp examples/progressbar/main.py /pyboard +cp examples/boot.py /pyboard +repl ``` diff --git a/README.md b/README.md index 760dbea..e914699 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ pip install -r requirements.txt ## Setup -### Install package with upip +### Install package Connect the MicroPython device to a network (if possible) diff --git a/changelog.md b/changelog.md index 928e0db..4432c12 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,21 @@ r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}$" --> ## Released +## [0.16.0] - 2023-06-12 +### Added +- Validate `package.json` file with every test workflow run but without version validation +- Basic setup instructions added to docs examples file + +### Changed +- Update `nextion/ulogging.py` to [version 0.5 of micropython-lib](https://github.com/micropython/micropython-lib/blob/7128d423c2e7c0309ac17a1e6ba873b909b24fcc/python-stdlib/logging/logging.py) + +### Removed +- Outdated and unused `libs_external` folder with `ulogging.py` +- `libs_external` and `nextion/ulogging.py` removed from `.flake8` exclude list + +### Fixed +- Installation instructions in Quickstart document are using `mip` + ## [0.15.3] - 2023-05-17 ### Fixed - Publish releases to PyPi again as `micropython-nextion`, see #35 @@ -229,8 +244,9 @@ r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}$" - [Example HMI file](examples/everything.HMI) to be used for all examples -[Unreleased]: https://github.com/brainelectronics/micropython-nextion/compare/0.15.3...develop +[Unreleased]: https://github.com/brainelectronics/micropython-nextion/compare/0.16.0...develop +[0.16.0]: https://github.com/brainelectronics/micropython-nextion/tree/0.16.0 [0.15.3]: https://github.com/brainelectronics/micropython-nextion/tree/0.15.3 [0.15.2]: https://github.com/brainelectronics/micropython-nextion/tree/0.15.2 [0.15.1]: https://github.com/brainelectronics/micropython-nextion/tree/0.15.1 diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 20c2751..8f8aa1b 100755 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -9,6 +9,26 @@ Usage examples of this `micropython-nextion` library An example of all implemented functionalities can be found at the [MicroPython Nextion examples folder][ref-micropython-nextion-examples] +## Setup Nextion + +```python +from nextion import NexHardware + +# define communication pins for Nextion display +tx_pin = 21 +rx_pin = 22 + +# create Nextion hardware interface +nh = NexHardware(rx_pin=rx_pin, tx_pin=tx_pin) + +# init nextion communication interface +nh.nexInit() + +# modify text field "t0" showing "newtxt" by default +cmd = 't0.txt="asdf"' +nh.sendCommand(cmd) +``` + ## Special hints ### Access object on a non active page diff --git a/examples/boot_wifi.py b/examples/boot_wifi.py index d34b787..c458ec6 100644 --- a/examples/boot_wifi.py +++ b/examples/boot_wifi.py @@ -10,7 +10,6 @@ """ # system packages -import esp import gc import machine import network @@ -20,15 +19,6 @@ # import machine # machine.freq(240000000) -# disable ESP os debug output -esp.osdebug(None) - -# set pin D4 as output (blue LED) -led_pin = machine.Pin(4, machine.Pin.OUT) - -# turn onboard LED on -led_pin.value(1) - station = network.WLAN(network.STA_IF) if station.active() and station.isconnected(): station.disconnect() @@ -75,9 +65,6 @@ print('Created Accesspoint: {}'.format(accesspoint_name)) -# turn onboard LED off -led_pin.value(0) - print('Restart cause: {}'.format(machine.reset_cause())) # run garbage collector at the end to clean up diff --git a/libs_external/ulogging.py b/libs_external/ulogging.py deleted file mode 100644 index cea2de0..0000000 --- a/libs_external/ulogging.py +++ /dev/null @@ -1,94 +0,0 @@ -import sys - -CRITICAL = 50 -ERROR = 40 -WARNING = 30 -INFO = 20 -DEBUG = 10 -NOTSET = 0 - -_level_dict = { - CRITICAL: "CRIT", - ERROR: "ERROR", - WARNING: "WARN", - INFO: "INFO", - DEBUG: "DEBUG", -} - -_stream = sys.stderr - -class Logger: - - level = NOTSET - - def __init__(self, name): - self.name = name - - def _level_str(self, level): - l = _level_dict.get(level) - if l is not None: - return l - return "LVL%s" % level - - def setLevel(self, level): - self.level = level - - def isEnabledFor(self, level): - return level >= (self.level or _level) - - def log(self, level, msg, *args): - if level >= (self.level or _level): - _stream.write("%s:%s:" % (self._level_str(level), self.name)) - if not args: - print(msg, file=_stream) - else: - print(msg % args, file=_stream) - - def debug(self, msg, *args): - self.log(DEBUG, msg, *args) - - def info(self, msg, *args): - self.log(INFO, msg, *args) - - def warning(self, msg, *args): - self.log(WARNING, msg, *args) - - def error(self, msg, *args): - self.log(ERROR, msg, *args) - - def critical(self, msg, *args): - self.log(CRITICAL, msg, *args) - - def exc(self, e, msg, *args): - self.log(ERROR, msg, *args) - sys.print_exception(e, _stream) - - def exception(self, msg, *args): - self.exc(sys.exc_info()[1], msg, *args) - - -_level = INFO -_loggers = {} - -def getLogger(name): - if name in _loggers: - return _loggers[name] - l = Logger(name) - _loggers[name] = l - return l - -def info(msg, *args): - getLogger(None).info(msg, *args) - -def debug(msg, *args): - getLogger(None).debug(msg, *args) - -def basicConfig(level=INFO, filename=None, stream=None, format=None): - global _level, _stream - _level = level - if stream: - _stream = stream - if filename is not None: - print("logging.basicConfig: filename arg is not supported") - if format is not None: - print("logging.basicConfig: format arg is not supported") diff --git a/nextion/ulogging.py b/nextion/ulogging.py index cea2de0..0d1c8d5 100644 --- a/nextion/ulogging.py +++ b/nextion/ulogging.py @@ -1,48 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +This file has been copied from micropython-lib + +https://github.com/micropython/micropython-lib/blob/7128d423c2e7c0309ac17a1e6ba873b909b24fcc/python-stdlib/logging/logging.py +""" + +try: + from micropython import const # noqa: F401 +except ImportError: + + def const(x): + return x + + import sys +import time + +CRITICAL = const(50) +ERROR = const(40) +WARNING = const(30) +INFO = const(20) +DEBUG = const(10) +NOTSET = const(0) -CRITICAL = 50 -ERROR = 40 -WARNING = 30 -INFO = 20 -DEBUG = 10 -NOTSET = 0 +_DEFAULT_LEVEL = const(WARNING) _level_dict = { - CRITICAL: "CRIT", + CRITICAL: "CRITICAL", ERROR: "ERROR", - WARNING: "WARN", + WARNING: "WARNING", INFO: "INFO", DEBUG: "DEBUG", + NOTSET: "NOTSET", } +_loggers = {} _stream = sys.stderr +_default_fmt = "%(levelname)s:%(name)s:%(message)s" +_default_datefmt = "%Y-%m-%d %H:%M:%S" -class Logger: - level = NOTSET - - def __init__(self, name): +class LogRecord: + def set(self, name, level, message): self.name = name + self.levelno = level + self.levelname = _level_dict[level] + self.message = message + self.ct = time.time() + self.msecs = int((self.ct - int(self.ct)) * 1000) + self.asctime = None + + +class Handler: + def __init__(self, level=NOTSET): + self.level = level + self.formatter = None + + def close(self): + pass + + def setLevel(self, level): + self.level = level - def _level_str(self, level): - l = _level_dict.get(level) - if l is not None: - return l - return "LVL%s" % level + def setFormatter(self, formatter): + self.formatter = formatter + + def format(self, record): + return self.formatter.format(record) + + +class StreamHandler(Handler): + def __init__(self, stream=None): + self.stream = _stream if stream is None else stream + self.terminator = "\n" + + def close(self): + if hasattr(self.stream, "flush"): + self.stream.flush() + + def emit(self, record): + if record.levelno >= self.level: + self.stream.write(self.format(record) + self.terminator) + + +class FileHandler(StreamHandler): + def __init__(self, filename, mode="a", encoding="UTF-8"): + super().__init__(stream=open(filename, mode=mode, encoding=encoding)) + + def close(self): + super().close() + self.stream.close() + + +class Formatter: + def __init__(self, fmt=None, datefmt=None): + self.fmt = _default_fmt if fmt is None else fmt + self.datefmt = _default_datefmt if datefmt is None else datefmt + + def usesTime(self): + return "asctime" in self.fmt + + def formatTime(self, datefmt, record): + if hasattr(time, "strftime"): + return time.strftime(datefmt, time.localtime(record.ct)) + return None + + def format(self, record): + if self.usesTime(): + record.asctime = self.formatTime(self.datefmt, record) + return self.fmt % { + "name": record.name, + "message": record.message, + "msecs": record.msecs, + "asctime": record.asctime, + "levelname": record.levelname, + } + + +class Logger: + def __init__(self, name, level=NOTSET): + self.name = name + self.level = level + self.handlers = [] + self.record = LogRecord() def setLevel(self, level): self.level = level def isEnabledFor(self, level): - return level >= (self.level or _level) + return level >= self.getEffectiveLevel() + + def getEffectiveLevel(self): + return self.level or getLogger().level or _DEFAULT_LEVEL def log(self, level, msg, *args): - if level >= (self.level or _level): - _stream.write("%s:%s:" % (self._level_str(level), self.name)) - if not args: - print(msg, file=_stream) - else: - print(msg % args, file=_stream) + if self.isEnabledFor(level): + if args: + if isinstance(args[0], dict): + args = args[0] + msg = msg % args + self.record.set(self.name, level, msg) + handlers = self.handlers + if not handlers: + handlers = getLogger().handlers + for h in handlers: + h.emit(self.record) def debug(self, msg, *args): self.log(DEBUG, msg, *args) @@ -59,36 +163,98 @@ def error(self, msg, *args): def critical(self, msg, *args): self.log(CRITICAL, msg, *args) - def exc(self, e, msg, *args): + def exception(self, msg, *args): self.log(ERROR, msg, *args) - sys.print_exception(e, _stream) + if hasattr(sys, "exc_info"): + sys.print_exception(sys.exc_info()[1], _stream) - def exception(self, msg, *args): - self.exc(sys.exc_info()[1], msg, *args) + def addHandler(self, handler): + self.handlers.append(handler) + def hasHandlers(self): + return len(self.handlers) > 0 -_level = INFO -_loggers = {} -def getLogger(name): - if name in _loggers: - return _loggers[name] - l = Logger(name) - _loggers[name] = l - return l +def getLogger(name=None): + if name is None: + name = "root" + if name not in _loggers: + _loggers[name] = Logger(name) + if name == "root": + basicConfig() + return _loggers[name] + + +def log(level, msg, *args): + getLogger().log(level, msg, *args) -def info(msg, *args): - getLogger(None).info(msg, *args) def debug(msg, *args): - getLogger(None).debug(msg, *args) - -def basicConfig(level=INFO, filename=None, stream=None, format=None): - global _level, _stream - _level = level - if stream: - _stream = stream - if filename is not None: - print("logging.basicConfig: filename arg is not supported") - if format is not None: - print("logging.basicConfig: format arg is not supported") + getLogger().debug(msg, *args) + + +def info(msg, *args): + getLogger().info(msg, *args) + + +def warning(msg, *args): + getLogger().warning(msg, *args) + + +def error(msg, *args): + getLogger().error(msg, *args) + + +def critical(msg, *args): + getLogger().critical(msg, *args) + + +def exception(msg, *args): + getLogger().exception(msg, *args) + + +def shutdown(): + for k, logger in _loggers.items(): + for h in logger.handlers: + h.close() + _loggers.pop(logger, None) + + +def addLevelName(level, name): + _level_dict[level] = name + + +def basicConfig( + filename=None, + filemode="a", + format=None, + datefmt=None, + level=WARNING, + stream=None, + encoding="UTF-8", + force=False, +): + if "root" not in _loggers: + _loggers["root"] = Logger("root") + + logger = _loggers["root"] + + if force or not logger.handlers: + for h in logger.handlers: + h.close() + logger.handlers = [] + + if filename is None: + handler = StreamHandler(stream) + else: + handler = FileHandler(filename, filemode, encoding) + + handler.setLevel(level) + handler.setFormatter(Formatter(format, datefmt)) + + logger.setLevel(level) + logger.addHandler(handler) + + +if hasattr(sys, "atexit"): + sys.atexit(shutdown) diff --git a/requirements-test.txt b/requirements-test.txt index ea33525..bb2d4d2 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,4 @@ # Avoid fixed versions flake8>=6.0.0,<7 yamllint>=1.29,<2 -setup2upypackage>=0.1.0,<1 +setup2upypackage>=0.2.0,<1 diff --git a/setup.py b/setup.py index 722465d..57ad580 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,10 @@ # -*- coding: UTF-8 -*- from setuptools import setup -import pathlib +from pathlib import Path import sdist_upip -here = pathlib.Path(__file__).parent.resolve() +here = Path(__file__).parent.resolve() # Get the long description from the README file long_description = (here / 'README.md').read_text(encoding='utf-8')