CircuitPython is a programming language based on Python, one of the fastest growing programming languages in the world. It is specifically designed to simplify experimenting and learning to code on low-cost microcontroller boards. Here is a guide which covers the basics:

Be sure you have the latest CircuitPython for the Feather M4 Express loaded onto your board, as described here

CircuitPython is easiest to use within the Mu Editor. If you haven't previously used Mu, this guide will get you started.


Plug your Feather M4 Express board into your computer via a USB cable. Please be sure the cable is a good power+data cable so the computer can talk to the Feather board.

A new disk should appear in your computer's file explorer/finder called CIRCUITPY. This is the place we'll copy the code and code library. If you can only get a drive named FEATHERBOOT, load CircuitPython per the Feather guide above.

Create a new directory on the CIRCUITPY drive named lib.

Download the latest CircuitPython driver package to your computer using the green button below. Match the library you get to the version of CircuitPython you are using. Save to your computer's hard drive where you can find it.

With your file explorer/finder, browse to the bundle and open it up. Copy the following folders and files from the library bundle to your CIRCUITPY lib directory you made earlier:

  • adafruit_bme280
  • adafruit_logging
  • adafruit_esp32spi
  • adafruit_gps

All of the other necessary libraries are baked into CircuitPython!

Your CIRCUITPY/lib directory should look like the snapshot below.

The Secrets File

The file in the root/main directory of the CIRCUITPY drive contains bits of information that should never be put in source code files. That includes things like usernames, passwords, and location specific information.  In the case of this projects that's:

  • SSID (name) and password for connecting to the local WiFi network,
  • timezone for fetching the local time, and
  • AdafruitIO account credentials.

The file should look like the following, except that it contains your information. Note that the WiFi credentials are encoded as bytearrays (the b prefix on the strings)

secrets = {
    'ssid' : b'My_SSID',
    'password' : b'My_WIFI_Password',
    'timezone' : 'Area/City',
    'aio_username' : 'my_username',
    'aio_key' : 'my_key',

Download the Code

You'll need to download the zip of the project to get all the required files. You can do this here.

# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
# SPDX-License-Identifier: MIT

IoT environmental sensor node.

Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!

Written by Dave Astels for Adafruit Industries
Copyright (c) 2019 Adafruit Industries
Licensed under the MIT license.

All text above must be included in any redistribution.

import time
import board
import busio
import air_quality
import gps
import adafruit_bme280
import aio
import adafruit_logging as logging

logger = logging.getLogger('main')
if not logger.hasHandlers():

gps_uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=3.000)
gps_interface = gps.Gps(gps_uart)

logger.debug('GPS started')

aio_interface = aio.AIO()

if aio_interface.onboard_esp:
    air_uart = busio.UART(board.D5, board.D7, baudrate=9600)
    air_uart = busio.UART(board.A2, board.A3, baudrate=9600)
air = air_quality.AirQualitySensor(air_uart)

logger.debug('Air quality sensor started')

i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

reading_interval = 300.0
reading_time = time.monotonic()

time_update_interval = 3600.0
time_update_time = time.monotonic()'Getting data from GPS')

while True:
    if gps_interface.get_fix():
    logger.error('Failed getting fix... retrying')'Starting reading loop')

payload = {'value' : 0,
           'lat' : gps_interface.latitude,
           'lon' : gps_interface.longitude,
           'created_at' : ''}

while True:
    now = time.monotonic()

    if now >= time_update_time:
        time_update_time = now + time_update_interval'refreshing time')
        except RuntimeError as e:
            logger.debug('Time refresh failed with: %s', str(e))

    if now >= reading_time:
        reading_time = now + reading_interval'Taking a reading')

        st = time.localtime()
        timestamp = '{0}/{1:02}/{2:02} {3:2}:{4:02}:{5:02}'.format(st.tm_year,
        payload['created_at'] = timestamp

  'Air Quality pm10 standard: %d', air.pm10_standard)
            payload['value'] = air.pm10_standard
            if not'environmental-sensor.pm10-std', payload):
                logger.critical('post of pm10 standard failed')

  'Air Quality pm25 standard: %d', air.pm25_standard)
            payload['value'] = air.pm25_standard
            if not'environmental-sensor.pm25-std', payload):
                logger.critical('post of pm25 standard failed')

  'Air Quality pm100 standard: %d', air.pm100_standard)
            payload['value'] = air.pm100_standard
            if not'environmental-sensor.pm100-std', payload):
                logger.critical('post of pm100 standard failed')

  'Air Quality pm10 env: %d', air.pm10_env)
            payload['value'] = air.pm10_env
            if not'environmental-sensor.pm10-env', payload):
                logger.critical('post of pm10 env failed')

  'Air Quality pm25 env: %d', air.pm25_env)
            payload['value'] = air.pm25_env
            if not'environmental-sensor.pm25-env', payload):
                logger.critical('post of pm10 env failed')

  'Air Quality pm100 env: %d', air.pm100_env)
            payload['value'] = air.pm100_env
            if not'environmental-sensor.pm100-env', payload):
                logger.critical('post of pm100 env failed')

  'Air Quality particles 03um: %d', air.particles_03um)
            payload['value'] = air.particles_03um
            if not'environmental-sensor.03um', payload):
                logger.critical('post of particles 03um failed')

  'Air Quality particles 05um: %d', air.particles_05um)
            payload['value'] = air.particles_05um
            if not'environmental-sensor.05um', payload):
                logger.critical('post of particles 05um failed')

  'Air Quality particles 10um: %d', air.particles_10um)
            payload['value'] = air.particles_10um
            if not'environmental-sensor.10um', payload):
                logger.critical('post of particles 10um failed')

  'Air Quality particles 25um: %d', air.particles_25um)
            payload['value'] = air.particles_25um
            if not'environmental-sensor.25um', payload):
                logger.critical('post of particles 25um failed')

  'Air Quality particles 50um: %d', air.particles_50um)
            payload['value'] = air.particles_50um
            if not'environmental-sensor.50um', payload):
                logger.critical('post of particles 50um failed')

  'Air Quality particles 100um: %d', air.particles_100um)
            payload['value'] = air.particles_100um
            if not'environmental-sensor.100um', payload):
                logger.critical('post of particles 100um failed')
                continue'Temperature: %f', bme280.temperature)
        payload['value'] = bme280.temperature
        if not'environmental-sensor.temperature', payload):
            logger.critical('post of temperature failed')
            continue'Humidity: %f', bme280.humidity)
        payload['value'] = bme280.humidity
        if not'environmental-sensor.humidity', payload):
            logger.critical('post of humidity failed')
            continue'Pressure: %f', bme280.pressure)
        payload['value'] = bme280.pressure
        if not'environmental-sensor.pressure', payload):
            logger.critical('post of pressure failed')
            continue'Waiting for next reading')

This guide was first published on May 01, 2019. It was last updated on Nov 27, 2023.

This page (Code) was last updated on Nov 27, 2023.

Text editor powered by tinymce.