7.1 KiB
Design Guide
MicroPython has created a great foundation to build upon and to make it even better for beginners we've created CircuitPython. This guide covers a number of ways the core and libraries are geared towards beginners.
Start libraries with the cookiecutter
Cookiecutter is a cool tool that lets you bootstrap a new repo based on another repo. We've made one here for CircuitPython libraries that include configs for Travis CI and ReadTheDocs along with a setup.py, license, code of conduct and readme.
# The first time
pip install cookiecutter
cookiecutter gh:adafruit/cookiecutter-adafruit-circuitpython
Module Naming
Adafruit funded libraries should be under the
adafruit organization and have the format
Adafruit_CircuitPython_ and have a corresponding adafruit_<name>
directory (aka package) or adafruit_<name>.py
file (aka module).
Community created libraries should have the format CircuitPython_ and not
have the adafruit_
module or package prefix.
Both should have the CircuitPython repository topic on GitHub.
Lifetime and ContextManagers
A driver should be initialized and ready to use after construction. If the
device requires deinitialization, then provide it through deinit()
and also
provide __enter__
and __exit__
to create a context manager usable with with
.
Verify your device
Make sure device you are talking to is the device you expect. If not, raise a ValueError. Beware that I2C addresses can be identical on different devices so read registers you know to make sure they match your expectation. Validating this upfront will help catch mistakes.
Getters/Setters
When designing a driver for a device, use properties for device state and use methods for actions that the device performs. Doing this well helps beginners understand when to use what. It is also consistent with Python.
Design for compatibility with CPython
CircuitPython is aimed to be one's first experience with code. It will be the first step into the world of hardware and software. To ease one's exploration out from this first step, make sure that functionality shared with CPython shares the same API. It doesn't need to be the full API it can be a subset. However, do not add non-CPython APIs to the same modules. Instead, use separate non-CPython modules to add extra functionality. By distinguishing API boundaries at modules you increase the likelihood that incorrect expectations are found on import and not randomly during runtime.
Document inline
Whenever possible, document your code right next to the code that implements it. This makes it more likely to stay up to date with the implementation itself. Use Sphinx's automodule to format these all nicely in ReadTheDocs. The cookiecutter helps set these up.
Use rST for markup.
Module description
After the license comment:
"""
`<module name>` - <Short description>
=================================================
<Longer description.>
"""
Class description
class DS3231:
"""Interface to the DS3231 RTC."""
Data descriptor description
Comment is after even though its weird.
lost_power = i2c_bit.RWBit(0x0f, 7)
"""True if the device has lost power since the time was set."""
Property description
@property
def datetime(self):
"""Gets the current date and time or sets the current date and time then starts the clock."""
return self.datetime_register
@datetime.setter
def datetime(self, value):
pass
Method description
def datetime(self):
"""Gets the current date and time or sets the current date and time then starts the clock."""
return self.datetime_register
Use BusDevice
BusDevice is an awesome foundational library that manages talking on a shared I2C or SPI device for you. Use it to ensure the library can be used in threads later.
Lots of small modules
CircuitPython boards tend to have a small amount of internal flash and a small amount of ram but large amounts of external flash for the file system. So, create many small libraries that can be loaded as needed instead of one large file that does everything.
Speed second
Speed isn't as important as API clarity and code size. So, prefer simple APIs like properties for state even if it sacrifices a bit of speed.
Avoid allocations in drivers
Although Python doesn't require managing memory, its still a good practice for
library writers to think about memory allocations. Avoid them in drivers if
you can because you never know how much something will be called. Fewer
allocations means less time spent cleaning up. So, where you can, prefer
bytearray buffers that are created in __init__
and used throughout the
object with methods that read or write into the buffer instead of creating new
objects. nativeio
classes are design to read and write to subsections of
buffers.
However, this is a memory tradeoff so do not do it for large or rarely used buffers.
Examples
ustruct.pack
Use ustruct.pack_into
instead of ustruct.pack
.
Sensor properties and units
The Adafruit Unified Sensor Driver Arduino library has a great list of measurements and their units. Use the same ones including the property name itself so that drivers can be used interchangeably when they have the same properties.
Property name | Python type | Units |
---|---|---|
acceleration |
(float, float, float) | x, y, z meter per second per second |
magnetic |
float | micro-Tesla (uT) |
orientation |
(float, float, float) | x, y, z degrees |
gyro |
(float, float, float) | x, y, z radians per second |
temperature |
float | degrees centigrade |
distance |
float | centimeters |
light |
float | SI lux |
pressure |
float | hectopascal (hPa) |
relative_humidity |
float | percent |
current |
float | milliamps (mA) |
voltage |
float | volts (V) |
color |
int | RGB, eight bits per channel (0xff0000 is red) |
alarm |
(time.struct, str) | Sample alarm time and string to characterize frequency ("secondly", "minutely", "hourly", "daily", "weekly", "monthly") |
datetime |
time.struct | date and time |
Adding native modules
The Python API for a new module should be defined and documented in
shared-bindings
and define an underlying C API. If the implementation is
port-agnostic or relies on underlying APIs of another module, the code should
live in shared-module
. If it is port specific then it should live in common-hal
within the port's folder. In either case, the file and folder structure should
mimic the structure in shared-bindings
.
MicroPython compatibility
Keeping compatibility with MicroPython isn't a high priority. It should be done when its not in conflict with any of the above goals.