ENOMEM with Encoder State Machine

Student received an ENOMEM error when trying to run his code.

Stack Trace:

Traceback (most recent call last):
  File "/lib/XRPLib/imu.py", line 537, in <lambda>
  File "/lib/XRPLib/imu.py", line 544, in _update_imu_readings
  File "/lib/XRPLib/imu.py", line 282, in get_gyro_rates
  File "/lib/XRPLib/imu.py", line 142, in _raw_to_mdps
RuntimeError: maximum recursion depth exceeded
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "grid_pathing.py", line 1, in <module>
  File "xrp.py", line 2, in <module>
  File "drive_code.py", line 1, in <module>
  File "lib/XRPLib/defaults.py", line 17, in <module>
  File "lib/XRPLib/encoded_motor.py", line 27, in get_default_encoded_motor
  File "lib/XRPLib/encoder.py", line 27, in __init__
OSError: [Errno 12] ENOMEM

I tried checking the state machines manually, and saw the following message:

rp2.PIO(0).state_machine(0).active(): False
rp2.PIO(0).state_machine(1).active(): False
rp2.PIO(0).state_machine(2).active(): False
rp2.PIO(0).state_machine(3).active(): False
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "debug.py", line 12, in <module>
  File "debug.py", line 10, in report_state_machines
ValueError: StateMachine claimed by external resource

debug.py is as follows:

def report_state_machines():
    for p in (0, 1):
        for sm_c in range(4):
            fmt = "rp2.PIO(%d).state_machine(%d).active(): %s"
            print( fmt % (p, sm_c, rp2.PIO(p).state_machine(sm_c).active()))
            
report_state_machines()

I’ve seen this error pop up before, but I don’t remember how I solved it then.

Error persists across robots with his code, but it was solved by putting all user code in one file, so maybe the cause has something to do with importing XRPLib.defaults multiple times?

I’m having trouble recreating this. Tried chaining multiple files that all import XRPLib.defaults, manually created more than 4 Encoders, etc. but can’t recreate it. Are you able to share the student’s code? Looks like it occurs in only the first few lines, so you should be able to strip out the rest of the code. Or perhaps you can find some simpler code to recreate it?

The traceback is confusing to me, since it includes both the IMU and the encoder. Are they at all related to each other, or was it just coincidental timing? Since the IMU runs on a timer, my guess is the main thread happened to be setting up the encoders when the IMU timer triggered, and that’s actually what ran into trouble. I think the encoder stuff is a red herring; running your debug.py after importing XRPLib.defaults returns the same ValueError despite no other issues existing. So I think we need to look closer at the IMU.

Looking at the traceback, there’s maximum recursion depth exceeded and ENOMEM, which I believe means the RP2040 ran out of RAM. Does the student’s code import a big file or library or something? Or is the IMU code somehow recursing forever? Can you add some memory logging to the code to maybe figure out what’s eating it up?

Is it possible to upload the files, or create a simplified version the recreates the problem for us to debug? It looks like you had 3 files. Where you importing the whole files or just functions within the files?

Code was share privately, working through it now.

Found a simple way to recreate the encoder error. Paste the following in the shell:

from XRPLib.defaults import *
from lib.XRPLib.defaults import *

Results in:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lib/XRPLib/defaults.py", line 17, in <module>
  File "lib/XRPLib/encoded_motor.py", line 27, in get_default_encoded_motor
  File "lib/XRPLib/encoder.py", line 27, in __init__
OSError: [Errno 12] ENOMEM

Played around some more, looks like the TLDR is only ever use either from XRPLib.defaults import * or from lib.XRPLib.defaults import *, but not both. If either is run multiple times, the import is “skipped” on successive imports, so there’s no issue. But if both are run, then it effectively does the import all over again, which in this case attempts to create the IMU and encoders twice, which leads to the error messages above.

TLDR - We better understand the problem and will put a fix into the next XRPLib release.

For those that want to under stand it all.
I did a little research and here is what I learned. The place that acts as the cache lookup is sys.modules . This keeps a dictionary of module names and locations. But, when they do a module lookup they look at the module name, not the location. So the implicit name XRPLib.defaults is different from the explicit name lib.XRPLib.defaults , even though they map to the same location. The second part is that defaults.py uses relative imports for importing the different parts of XRPLib. For instance from .encoder import Encoder . When the loader loads a relative import it starts with the relative location of the parent. Which is either the implicit or explicit name used to import defaults.py . This means when lib.XRPLib.defaults is used it tries to load all the relative ones the same way, making for new entries in the sys.modules . The quick fix, which I tested, was to make defaults.py use the implicit loading of the imports from XRPLib.encoder import Encoder . While this will still load two different versions of defaults.py all the other imports will be already in the cache and all the local variables in defaults will point to the same objects and none will be reloaded. This does mean that that defaults will only work when in a directory named XRPLib, I do not see it as a problem for this project.