Portierung des Terkin-Datenlogger auf Genuine MicroPython für ESP32

Es ist vermutlich doch einiges an Arbeit und ich sehe gerade andere Stellen, die nötiger beackert werden müssten.

Hi Markus,

der neue MicroPython Universal Bootloader umal.py bringt nun ein wenig Infrastruktur mit, um Plattformweichen zur Laufzeit zu realisieren. Bei hiveeyes-micropython-firmware/device.py at 0.6.0 · hiveeyes/hiveeyes-micropython-firmware · GitHub wird das nun beispielsweise bei der Ansteuerung der RGB LED eingesetzt.

Der weitere commit Start making Terkin platform-agnostic · hiveeyes/hiveeyes-micropython-firmware@ba9c72e · GitHub setzt ein paar der von Dir aufgezählten Dinge so um, dass es ab jetzt u.U. auch ohne weitere Anpassungen (auskommentieren) an einigen Stellen klappen könnte.

Manche der von Dir erwähnten Änderungen sind jedoch noch nicht umgesetzt, aber immerhin die meisten. Herzlichen Dank für Deine Eingaben!

Das WiFi-Modul zu portieren liegt noch vor uns.

Viele Grüße,
Andreas.

Folgend alle Änderungen, die an 0.6. gemacht werden müssen, damit es mit uPy 1.11 läuft.
Und um es vorweg zu nehmen: es läuft! :smile:

  • /, /lib & /dist-packages reichen:

umal.py, l.65ff:

if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
        # Extend by path containing frozen modules.
        sys.path[0:0] = ['/flash/lib-mpy']
        # Extend by all paths required for running the sandboxed firmware.
        sys.path.extend(['/flash/dist-packages', '/flash/terkin', '/flash/hiveeyes'])
    else:
        # Extend by all paths required for running the sandboxed firmware.
        sys.path.extend(['/dist-packages'])
  • Timer.Chrono() gibt es nicht. Braucht man zumindest für die uptime auch nicht. Wird aber glaub ich irgendwo noch zum Zeitnehmen verwendet.

Logging:

def getLogger(name=None, level=logging.INFO):
global _chrono

# Keep track of time since boot.
if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
    if _chrono is None:
        from machine import Timer
        _chrono = Timer.Chrono()
        _chrono.start()
  • s.oben utime.time() = Sekunden seit boot

    class TimedLogRecord(logging.LogRecord):
    def __init__(self, *args, **kwargs):
      super().__init__(*args, **kwargs)
      if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
          try:
              self.tdelta = _chrono.read()
          except:
              self.tdelta = None
      else:
          self.tdelta = utime.time()
    
  • sys.exc_info existiert nicht. Habe auch keinen Ersatz gefunden, also erstmal None

Dist-packages/logging/init.py:

def exception(self, msg, *args):
    if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
        self.exc(sys.exc_info()[1], msg, *args)
    else:
        self.exc(None, msg, *args)
  • wie gesagt, /flash existiert nicht. Außerdem gibt es /backup nicht. Ich vermute, das wird von make erzeugt, fehlt dann aber im VSC.

Terkin/configuration:

if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
    CONFIG_PATH = '/flash'
    BACKUP_PATH = '/flash/backup'
else:
    CONFIG_PATH = '/'
    BACKUP_PATH = '/backup'
  • das ganze LTE Zeug fehlt natürlich und pycom auch

Terkin/device:

    if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
        import pycom

  • syntax für mac ist unterschiedlich

    Terkin/network/wifi:
    def print_address_status(self):
      if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
          mac_address = self.humanize_mac_addresses(self.station.mac())
      else:
          mac_address = self.humanize_mac_addresses(self.config('mac'))
      ifconfig = self.station.ifconfig()
      log.info('WiFi STA: Networking address (MAC): %s', mac_address)
      log.info('WiFi STA: Networking address (IP):  %s', ifconfig)
    
  • pycom pins sind strings, uPy ints. Das ist nicht hübsch, geht aber

Terkin/sensor/system:

    # ADC channel used for sampling the raw value.
    try:
        self.adc = ADC(id=0)
    except TypeError:
        from machine import Pin
        if type(self.pin) == str:
            self.adc = ADC(Pin(int(self.pin[1:])))
        else:
            self.adc = ADC(Pin(self.pin))

Und das wars schon. :slight_smile:

2 Likes

Genauer gesagt bedeutet ‘läuft’ eigentlich ‘läuft mit Fehlern’ :slight_smile:

Ich hab bei Wifi angefangen und das jetzt soweit, das es tatsächlich funktioniert. Problem: der connect dauert ziemlich lange und dann ist der Logger schon weiter. Das fällt mir jetzt aber schwer, das korrekt so einzubauen, das alles Notwendige darauf wartet. Da bräuchte ich etwas Hilfe.

Ich hab meine Version angehängt.
wifi.py (14,3 KB)

Anbei auch das startup log. Ich hab ein exit() nach dem ersten loop eingebaut. Daher das verfrühte Programmende.
hiveeyes_log_1908251813.zip (5,4 KB)

1 Like

Noch ein paar Bits gefunden:

Terkin/datalogger

  • esp32 kann BT nicht ausschalten

     if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
     self.device.power_off_bluetooth()
    
  • die neueren esp32 haben keinen Temperatursensor mehr

      system_sensors = [
          SystemMemoryFree,
          #SystemTemperature,
          SystemBatteryLevel,
          SystemUptime,
      ]
    

Terkin/util:

  • kein crypto

    def random_from_crypto():
      if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
        import crypto
        r = crypto.getrandbits(32)
      else:
        import urandom
        r = urandom.getrandbits(32)
    return ((r[0]<<24) + (r[1]<<16) + (r[2]<<8) + r[3]) / 4294967295.0
    

Terkin/system:

  • kein init

      # Power on ADC.
      if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:
          self.adc.init()
    

Terkin/wifi:

  • stats gibts nicht

    def read(self):
    
      if self.station is None or sys.platform == 'esp32'
          return
    ...
2 Likes

Hi Markus,

neben MAX17043 mit MicroPython auch an dieser Stelle vielen herzlichen Dank! Wir werden die Änderungen durcharbeiten und in den aktuellen Softwarestand einpflegen.

Sofern das Sandbox-Setup nun u.U. auch für Dich klappt [1], könntest Du die Änderungen ggf. auch per Pull Request zur Verfügung stellen, wenn Du dafür Zeit, Lust und Muße findest. Ansonsten sind wir aber auch über Deine Beschreibungen und die Publikation der notwendigen Änderungen sehr dankbar.

Viele Grüße,
Andreas.


  1. Setup der Terkin-Datenlogger Sandbox schlägt fehl ↩︎

Was ist die beste Methode für eine Codeweiche zwischen pycom und uPy bzw. anderer zukünftiger hardware?

Man könnte ganz stumpf überall sowas abfragen:

if sys.platform in ['WiPy', 'LoPy', 'GPy', 'FiPy']:

Das ist weder hübsch noch wartbar, funktioniert aber überal (wenn man sys importiert).

@Andreas hat vorgeschlagen das so zu machen:

if self.application_info.platform_info.vendor == MicroPythonPlatform.Pycom:

Das funktioniert dann aber an solchen Stellen wie in terkin/configuration.py nicht

class TerkinConfiguration:
    
    A flexible configuration manager.
   
    if self.application_info.platform_info.vendor == MicroPythonPlatform.Pycom:
        CONFIG_PATH = '/flash'
        BACKUP_PATH = '/flash/backup'
    else:
        CONFIG_PATH = '/'
        BACKUP_PATH = '/backup'

weil self hier ein anderes Objekt ist.
Wie macht man das also richtig?

1 Like

Hi Markus,

Du hast Recht, das funktioniert so nicht 1:1 an allen Stellen. Vor allem innerhalb von 3rd-party Bibliotheken will man meist keine zusätzlichen Abhängigkeiten schaffen. In diesem Fall bleibt der Rückgriff auf ein ducktyping-artiges Vorgehen per try / except.

In dem bei Dir vorliegenden Fall – für Plattformweichen innerhalb der Konfigurationseinstellungen – müssen wir u.U. nochmal anders auf den Kontext schauen, weil wir ja aus der aktuellen Python-basierten Konfiguration beizeiten auch eine JSON-Repräsentation erzeugen wollen und wir dann schauen müssen, wie das gut zusammengeht.

Viele Grüße,
Andreas.

Wir haben momentan 8 Module mit 14 Stellen an denen die Weiche eingesetzt wird.
Eine davon in /lib (umal.py), der Rest unter /terkin. Keine davon in /dist-packages. 3rd party ist also momentan gar kein Problem.

try / except würde fast überall funktionieren. Dagegen sprechen mAn zwei Dinge: mir widerstrebt es irgendwie, ein Programm bewusst in einen Fehler laufen zu lassen (da bin ich vermutlich altmodisch :slight_smile:) und das Konstrukt wird doof, wenn wir mal einen dritte Hardware unterstützen wollen.
Wenn das keine Hinderungsgründe sind, bau ich das gerne auf try / except um.

Alternativvorschlag: wir bauen ein Modul nur mit den Platformeigenschaften und importieren das bei Bedarf. Das ist vermutlich ‘billiger’ als sys oder try und leichter zu warten und zu lesen.

Wunderbar, hört sich gut an!

Nein, so war das nicht gemeint. try / except sehe ich nur als Fallback für Fälle, wo das Plattformeigenschaftsmodul nicht zur Verfügung steht oder es ungut ist, es zu importieren.

Ja, genau so dachte ich mir das mit der class PlatformInfo aus umal.py. Entweder man

  • importiert und instanziiert die Klasse (erneut), wo man sie braucht, oder man greift wahlweise auf
  • bootloader.platform_info über die globale Variable bootloader oder eben auf
  • das schon bekannte self.application_info.platform_info zurück.

Ah, ok. Das hatte ich nicht verstanden.

Warum funktioniert dann:

from umal import MicroPythonPlatform

    # Keep track of time since boot.
    from terkin.util import GenericChronometer, PycomChronometer
    if bootloader.platform_info.vendor == MicroPythonPlatform.Pycom:
        _chrono = PycomChronometer()
    else:
        _chrono = GenericChronometer()

nicht?

[main.py] INFO: Starting logging
Traceback (most recent call last):
  File "main.py", line 21, in <module>
  File "terkin/logging.py", line 13, in <module>
NameError: name 'bootloader' isn't defined

bootloader ist doch global?
Ich gestehe, das ich mich mit dem objektorientierten manchmal etwas schwer tue. Dazu bin ich den prozeduralen Sprachen zu sehr verhaftet. :slight_smile:

Streich doch mal

und mache dann dort, wo Du “bootloader” benutzen willst, vorher ein “global bootloader”.

#from umal import MicroPythonPlatform
global bootloader

# Keep track of time since boot.
from terkin.util import GenericChronometer, PycomChronometer
if bootloader.platform_info.vendor == MicroPythonPlatform.Pycom:
    _chrono = PycomChronometer()
else:
    _chrono = GenericChronometer()

führt zu:

[main.py] INFO: Starting logging
Traceback (most recent call last):
  File "main.py", line 21, in <module>
  File "terkin/logging.py", line 14, in <module>
NameError: name 'bootloader' isn't defined

Hast Du ein Indiz dafür, dass der Bootloader zu diesem Zeitpunkt bereits angefahren wurde? z.B. die Log-Nachricht [boot.py] INFO: Starting "umal" bootloader müsste da schon zu sehen gewesen sein.

Benutze doch hier im konkreten Fall kurzerhand doch einfach immer den GenericChronometer. Das aktuelle Pycom-Firmware-Release kennt mindestens die Primitiven time.ticks_ms() sowie time.ticks_diff(), daher sollte dieser auch genauso gut mit Pycom funktionieren.

[boot.py] INFO: Python module search path is: ['', '/lib']
[boot.py] INFO: Starting "umal" bootloader
[umal]     INFO: Python module search path is: ['', '/lib', '/dist-packages', '/terkin', '/hiveeyes']
[main.py] INFO: Loading settings
[main.py] INFO: Starting logging
Traceback (most recent call last):
  File "main.py", line 21, in <module>
  File "terkin/logging.py", line 14, in <module>
NameError: name 'bootloader' isn't defined

Jepp, umal wurde geladen.

Blöd. Ich merge jetzt mal Deinen pull request Port to ESP32 WROVER with MicroPython 1.11 by poesel · Pull Request #22 · hiveeyes/hiveeyes-micropython-firmware · GitHub und kümmere mich hernach um entsprechende Verbesserungen bzgl. der Plattformweiche.

Herzlichen Dank für die Basisarbeit!

@Andreas - bei mir ist machine.freq() ein INT. Ist das beim pyboard anders?
Sonst verstehe ich den Sinn der Weiche in device.py nicht.

   if self.application_info.platform_info.vendor == MicroPythonPlatform.Pycom:
        frequency = machine.freq() / 1000000
    else:
        frequency = machine.freq()[0] / 1000000

Ja, dort ist das ein Quadrupel aus core- und bus frequencies. Die Weiche muss hier wohl noch entsprechend aufgemöbelt werden, falls das bei Dir grade hängt. Entweder über die platform_info Konstanten oder per Typüberprüfung à la

frequency = machine.freq()
if type(frequency) is tuple:
    frequency = frequency[0]
frequency = frequency / 1000000

So wäre es universeller und agnostischer gegenüber der Plattformweiche – at all ;].

1 Like

Da gefällt mir aber das besser:

     if self.application_info.platform_info.mcu == McuFamily.STM32:
         frequency = machine.freq()[0] / 1000000
     else:
         frequency = machine.freq() / 1000000

Wenn Du schon so ne schicke Weiche gebastelt hast, wollen wir sie auch nutzen. :wink:
Ist dann besser nachzuvollziehen.

Noch ein Punkt:

>   log.info('Reading configuration file {}'.format(filepath))
>         try:
>             with open(filepath, "r") as instream:
>                 payload = instream.read()
>                 data = json.loads(payload)
>                 return data
> 
>         except Exception as ex:
>             log.exc(ex, 'Reading configuration from "{}" failed'.format(filepath))

ergibt:

   13.1520 [terkin.configuration     ] ERROR  : Reading configuration from "/settings-user.json" failed
Traceback (most recent call last):
  File "terkin/configuration.py", line 153, in load
  OSError: [Errno 2] ENOENT

Den ERROR verstehe ich - die Datei gibt es noch nicht. Warum fängt der except dann den OSERROR aber nicht ab?