CircuitPython is "an education friendly open source derivative of MicroPython". MicroPython is a port of Python to microcontroller environments; it can run on boards with very few resources such as the ESP8266. I've recently started experimenting with CircuitPython on a Wemos D1 mini, which is a small form-factor ESP8266 board.

I had previously been using Mike Causer's micropython-tm1637 for MicroPython to drive a 4 digit LED display. I was hoping to get the same code working under CircuitPython, but when I tried to build an image that included the tm1637 module I ran into:

>>> import tm1637
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "tm1637.py", line 6, in <module>
ImportError: cannot import name sleep_us

One of CircuitPython's goals is to be as close to CPython as possible. This means that in many cases that the CircuitPython folks have re-implemented MicroPython modules to have syntax that is more a strict subset of the CPython equivalent, and the MicroPython time module as impacted by this change. With stock MicroPython, the time module has:

>>> print('\n'.join(dir(time)))
__class__
__name__
localtime
mktime
sleep
sleep_ms
sleep_us
ticks_add
ticks_cpu
ticks_diff
ticks_ms
ticks_us
time

But the corresponding CircuitPython module has:

>>> print('\n'.join(dir(time)))
__name__
monotonic
sleep
struct_time
localtime
mktime
time

It turns out that the necessary functions are defined in the utime module, which is implemented by ports/esp8266/modutime.c, but this module is not included in the CircuitPython build. How do we fix that?

The most obvious change is to add modutime.c to the SRC_C variable in ports/esp8266/Makefile, which gets us:

SRC_C = \
        [...]
        modesp.c \
        modnetwork.c \
        modutime.c \
        [...]

After making this change and trying to build CircuitPython, I hit 70 or so lines like:

Generating build/genhdr/mpversion.h
In file included from ../../py/mpstate.h:35:0,
                 from ../../py/runtime.h:29,
                 from modutime.c:32:
modutime.c:109:50: error: 'MP_QSTR_utime' undeclared here (not in a function)
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_utime) },
                                                  ^

The MP_QSTR_ macros are sort of magical: they are generated during the build process by scanning for references of the form MP_QSTR_utime and creating definitions that look like this:

QDEF(MP_QSTR_utime, (const byte*)"\xe5\x9d\x05" "utime")

But...and this is the immediate problem...this generation only happens with a clean build. Running make clean and then re-running the build yields:

build/shared-bindings/time/__init__.o:(.rodata.time_localtime_obj+0x0): multiple definition of `time_localtime_obj'
build/modutime.o:(.rodata.time_localtime_obj+0x0): first defined here
build/shared-bindings/time/__init__.o:(.rodata.time_mktime_obj+0x0): multiple definition of `time_mktime_obj'
build/modutime.o:(.rodata.time_mktime_obj+0x0): first defined here
build/shared-bindings/time/__init__.o:(.rodata.time_time_obj+0x0): multiple definition of `time_time_obj'
build/modutime.o:(.rodata.time_time_obj+0x0): first defined here

The above errors show a conflict between the structures defined in utime, which have just activated, and the existing time module. A simple rename will take care of that problem; instead of:

MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(time_localtime_obj, 0, 1, time_localtime);

We want:

MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(utime_localtime_obj, 0, 1, time_localtime);

And so forth. At this point, everything builds correctly, but if we deploy the image to our board and try to import the utime module, we see:

>>> import utime
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'utime'

The final piece of this puzzle is that there is a list of built-in modules defined in mpconfigport.h. We need to add our utime module to that list:

#define MICROPY_PORT_BUILTIN_MODULES \
    [...]
    { MP_ROM_QSTR(MP_QSTR_utime), MP_ROM_PTR(&utime_module) }, \
    [...]

If we build and deploy our image, we're now able to use the methods from the utime module:

Adafruit CircuitPython 3.0.0-alpha.6-42-gb46567004 on 2018-05-06; ESP module with ESP8266
>>> import utime
>>> utime.sleep_ms(1000)
>>>

We need to make one final change to the tm1637 module, since as written it imports methods from the time module. Instead of:

from time import sleep_us, sleep_ms

We have to modify it to read:

try:
    from time import sleep_us, sleep_ms
except ImportError:
    from utime import sleep_us, sleep_ms

With our working utime module and the modified tm1637 module, we are now able to drive our display: