diff --git a/.gitignore b/.gitignore index 8542927..b02eecf 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,4 @@ dmypy.json micropy.json pymakr.conf tinypico-20210418-v1.15.bin +pico-go.json diff --git a/README.md b/README.md index 7632421..57bf8ed 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This work is a lot based on: - [SparkFun MAX3010x Sensor Library](https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library "GitHub | SparkFun MAX3010x Sensor Library") - Written by **Peter Jansen and Nathan Seidle** (SparkFun) + Written by **Peter Jansen** and **Nathan Seidle** (SparkFun) This is a library written for the Maxim MAX30105 Optical Smoke Detector It should also work with the MAX30102. However, the MAX30102 does not have a Green LED. These sensors use I2C to communicate, as well as a single (optional) @@ -27,27 +27,38 @@ This work is not intended to be used in professional environments, and there are ## Repository organisation -- Driver: `src/lib/MAX30102.py` -- Example: `src/main.py` +- Driver: `./max30102` +- Example: `./example` ## Additional information -This driver works for sure with Maxim Integrated MAX30102 sensor. +This driver has been tested with Maxim Integrated MAX30102 sensor. However, it *should* work with MAX30105 sensor, too. -### Usage +### How to import the library and run the example -#### Including this library into your project +#### Including this library into your project (network-enabled MicroPython ports) +To include the library into a network-enabled MicroPython project, it's sufficient to install the package: -To include the library into a MicroPython project, it's sufficient to copy and include the `MAX30102.py` file. +```python +import upip +upip.install(micropython-max30102) +``` + +Make sure that your firmware runs these lines **after** an Internet connection has been established. -Example: create a *lib* directory inside your source code folder, and copy the `/src/lib/MAX30102.py` file inside it. -Then, it will be possible to include the sensor instance constructor by issuing: +To run the example in `./example` folder, upload `./example` content into your microcontroller, and run it. + +#### Including this library into your project (manual way) + +To directly include the library into a MicroPython project, it's sufficient to copy the `max30102` module next to your `main.py`, and then import it as follows: ```python -from lib.MAX30102 import MAX30102 +from max30102 import MAX30102 ``` +For instance, to run the example in `./example` folder, copy the `./max30102` directory and paste it in the `./example` folder. Then, upload `./example` content into your microcontroller and run it. + #### Setup and configuration At first, create a sensor instance. If you leave the arguments empty, the library will load the default TinyPico ESP32 board I2C configuration (SDA Pin 21, SCL Pin 22, 400kHz speed). diff --git a/example/boot.py b/example/boot.py new file mode 100644 index 0000000..8b8ba15 --- /dev/null +++ b/example/boot.py @@ -0,0 +1,29 @@ +# This file is executed on every boot (including wake-boot from deepsleep) + +def do_connect(ssid:str, password:str): + import network + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + if not wlan.isconnected(): + print('connecting to network...') + wlan.connect(ssid, password) + while not wlan.isconnected(): + pass + print('network config:', wlan.ifconfig()) + + +if __name__ == '__main__': + # Put yor WiFi credentials here + my_ssid = "my_ssid" + my_pass = "my_password" + + try: + import max30102 + except: + try: + import upip + do_connect(my_ssid, my_pass) + upip.install("micropython-max30102") + except: + print("Unable to get 'micropython-max30102' package!") + diff --git a/src/main.py b/example/main.py similarity index 94% rename from src/main.py rename to example/main.py index b356bed..59aaa5f 100644 --- a/src/main.py +++ b/example/main.py @@ -1,13 +1,9 @@ # main.py - -from lib.MAX30102 import MAX30102 from machine import sleep +from max30102 import MAX30102 from utime import ticks_diff, ticks_ms -import logging if __name__ == '__main__': - logging.basicConfig(level=logging.INFO) - # Sensor instance. If left empty, loads default ESP32 I2C configuration sensor = MAX30102() # Alternatively (for other boards): @@ -70,4 +66,4 @@ f_HZ = samples_n/1 samples_n = 0 t_start = ticks_ms() - print("acquisition frequency = ",f_HZ) \ No newline at end of file + print("acquisition frequency = ",f_HZ) diff --git a/src/lib/MAX30102.py b/max30102/__init__.py similarity index 96% rename from src/lib/MAX30102.py rename to max30102/__init__.py index 1311f7b..de4f56d 100644 --- a/src/lib/MAX30102.py +++ b/max30102/__init__.py @@ -18,15 +18,11 @@ n-elia ''' -from ustruct import unpack -from machine import SoftI2C, Pin -from utime import sleep_ms, ticks_ms, ticks_diff +from machine import Pin, SoftI2C from ucollections import deque +from ustruct import unpack +from utime import sleep_ms, ticks_diff, ticks_ms -import logging - -# Setup of logger -logger = logging.getLogger("MAX30102") # These I2C default settings work for TinyPico (ESP32-based board) MAX3010X_I2C_ADDRESS = 0x57 @@ -240,13 +236,9 @@ def __init__(self, try: self._i2c.readfrom(self._address, 1) except OSError as error: - logger.error("(%s) I2C Error. Unable to find a MAX3010x sensor.", TAG) raise SystemExit(error) - else: - logger.info("(%s) MAX3010x sensor found!", TAG) - + if not (self.checkPartID()): - logger.error("(%s) Wrong PartID. Unable to find a MAX3010x sensor.", TAG) raise SystemExit() # Sensor setup method @@ -286,7 +278,6 @@ def setup_sensor(self, LED_MODE=2, ADC_RANGE=16384, SAMPLE_RATE=400, def __del__(self): self.shutDown() - logger.info("(%s) Shutting down the sensor.", TAG) # Methods to read the two interrupt flags def getINT1(self): @@ -367,7 +358,6 @@ def softReset(self): # and data registers are reset to their power-on-state through # a power-on reset. The RESET bit is cleared automatically back to zero # after the reset sequence is completed. (datasheet pag. 19) - logger.debug("(%s) Resetting the sensor.", TAG) self.set_bitMask(MAX30105_MODECONFIG, MAX30105_RESET_MASK, MAX30105_RESET) @@ -475,8 +465,6 @@ def setSampleRate(self, sample_rate): MAX30105_SAMPLERATE_MASK, sr) - logger.debug("(%s) Setting sample rate to %d.", TAG, sample_rate) - # Store the sample rate and recompute the acq. freq. self._sampleRate = sample_rate self.updateAcquisitionFrequency() @@ -501,8 +489,6 @@ def setPulseWidth(self, pulse_width): MAX30105_PULSEWIDTH_MASK, pw) - logger.debug("(%s) Setting pulse width to %d.", TAG, pulse_width) - # Store the pulse width self._pulseWidth = pw @@ -555,9 +541,6 @@ def setFIFOAverage(self, number_of_samples): 'Wrong number of samples:{0}!'.format(number_of_samples)) self.set_bitMask(MAX30105_FIFOCONFIG, MAX30105_SAMPLEAVG_MASK, ns) - logger.debug("(%s) Setting FIFO avg samples to %d.", TAG, - number_of_samples) - # Store the number of averaged samples and recompute the acq. freq. self._sampleAvg = number_of_samples self.updateAcquisitionFrequency() @@ -565,16 +548,14 @@ def setFIFOAverage(self, number_of_samples): def updateAcquisitionFrequency(self): TAG = 'updateAcquisitionFrequency' if (None in [self._sampleRate, self._sampleAvg]): - logger.debug("(%s) Unable to compute acq freq..", TAG) return else: self._acqFrequency = self._sampleRate / self._sampleAvg from math import ceil + # Compute the time interval to wait before taking a good measure # (see note in setSampleRate() method) self._acqFrequencyInv = int(ceil(1000/self._acqFrequency)) - logger.info("(%s) Acq. frequency: %f Hz", TAG, self._acqFrequency) - logger.info("(%s) Acq. period: %f ms", TAG, self._acqFrequencyInv) def getAcquisitionFrequency(self): return self._acqFrequency diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a694f50..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -logging diff --git a/sdist_upip.py b/sdist_upip.py new file mode 100644 index 0000000..ce5877a --- /dev/null +++ b/sdist_upip.py @@ -0,0 +1,145 @@ +# +# This module overrides distutils (also compatible with setuptools) "sdist" +# command to perform pre- and post-processing as required for MicroPython's +# upip package manager. +# +# Preprocessing steps: +# * Creation of Python resource module (R.py) from each top-level package's +# resources. +# Postprocessing steps: +# * Removing metadata files not used by upip (this includes setup.py) +# * Recompressing gzip archive with 4K dictionary size so it can be +# installed even on low-heap targets. +# +import sys +import os +import zlib +from subprocess import Popen, PIPE +import glob +import tarfile +import re +import io + +from distutils.filelist import FileList +from setuptools.command.sdist import sdist as _sdist + + +def gzip_4k(inf, fname): + comp = zlib.compressobj(level=9, wbits=16 + 12) + with open(fname + ".out", "wb") as outf: + while 1: + data = inf.read(1024) + if not data: + break + outf.write(comp.compress(data)) + outf.write(comp.flush()) + os.rename(fname, fname + ".orig") + os.rename(fname + ".out", fname) + + +FILTERS = [ + # include, exclude, repeat + (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), + (r".+\.py$", r"[^/]+$"), + (None, r".+\.egg-info/.+"), +] + + +outbuf = io.BytesIO() + +def filter_tar(name): + fin = tarfile.open(name, "r:gz") + fout = tarfile.open(fileobj=outbuf, mode="w") + for info in fin: +# print(info) + if not "/" in info.name: + continue + fname = info.name.split("/", 1)[1] + include = None + + for inc_re, exc_re in FILTERS: + if include is None and inc_re: + if re.match(inc_re, fname): + include = True + + if include is None and exc_re: + if re.match(exc_re, fname): + include = False + + if include is None: + include = True + + if include: + print("including:", fname) + else: + print("excluding:", fname) + continue + + farch = fin.extractfile(info) + fout.addfile(info, farch) + fout.close() + fin.close() + + +def make_resource_module(manifest_files): + resources = [] + # Any non-python file included in manifest is resource + for fname in manifest_files: + ext = fname.rsplit(".", 1) + if len(ext) > 1: + ext = ext[1] + else: + ext = "" + if ext != "py": + resources.append(fname) + + if resources: + print("creating resource module R.py") + resources.sort() + last_pkg = None + r_file = None + for fname in resources: + try: + pkg, res_name = fname.split("/", 1) + except ValueError: + print("not treating %s as a resource" % fname) + continue + if last_pkg != pkg: + last_pkg = pkg + if r_file: + r_file.write("}\n") + r_file.close() + r_file = open(pkg + "/R.py", "w") + r_file.write("R = {\n") + + with open(fname, "rb") as f: + r_file.write("%r: %r,\n" % (res_name, f.read())) + + if r_file: + r_file.write("}\n") + r_file.close() + + +class sdist(_sdist): + + def run(self): + self.filelist = FileList() + self.get_file_list() + make_resource_module(self.filelist.files) + + r = super().run() + + assert len(self.archive_files) == 1 + print("filtering files and recompressing with 4K dictionary") + filter_tar(self.archive_files[0]) + outbuf.seek(0) + gzip_4k(outbuf, self.archive_files[0]) + + return r + + +# For testing only +if __name__ == "__main__": + filter_tar(sys.argv[1]) + outbuf.seek(0) + gzip_4k(outbuf, sys.argv[1]) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..39d36ff --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages +import sdist_upip + +setup( + name="micropython-max30102", + version="0.3.1", + description="MAX30102 driver for micropython.", + long_description=open("README.md").read(), + long_description_content_type='text/markdown', + + url="https://github.com/n-elia/MAX30102-MicroPython-driver", + license="MIT", + keywords="micropython", + + author="Nicola Elia", + maintainer="Nicola Elia", + + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: Implementation :: MicroPython", + ], + cmdclass={"sdist": sdist_upip.sdist}, + + packages=find_packages() +) \ No newline at end of file diff --git a/src/boot.py b/src/boot.py deleted file mode 100644 index fda0e79..0000000 --- a/src/boot.py +++ /dev/null @@ -1 +0,0 @@ -# This file is executed on every boot (including wake-boot from deepsleep) \ No newline at end of file diff --git a/src/lib/logging.py b/src/lib/logging.py deleted file mode 100644 index 537a666..0000000 --- a/src/lib/logging.py +++ /dev/null @@ -1,120 +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 LogRecord: - def __init__(self): - self.__dict__ = {} - - def __getattr__(self, key): - return self.__dict__[key] - -class Handler: - def __init__(self): - pass - - def setFormatter(self, fmtr): - pass - -class Logger: - - level = NOTSET - handlers = [] - record = LogRecord() - - 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 self.isEnabledFor(level): - levelname = self._level_str(level) - if args: - msg = msg % args - if self.handlers: - d = self.record.__dict__ - d["levelname"] = levelname - d["levelno"] = level - d["message"] = msg - d["name"] = self.name - for h in self.handlers: - h.emit(self.record) - else: - print(levelname, ":", self.name, ":", msg, sep="", 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) - - def addHandler(self, hndlr): - self.handlers.append(hndlr) - -_level = INFO -_loggers = {} - -def getLogger(name="root"): - if name in _loggers: - return _loggers[name] - l = Logger(name) - _loggers[name] = l - return l - -def info(msg, *args): - getLogger().info(msg, *args) - -def debug(msg, *args): - getLogger().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") \ No newline at end of file