CircuitPython

The Cyber Ægg has a CircuitPython build. It turns the badge into a device you program in Python, with no toolchain. Flash it once. The badge then mounts as a CIRCUITPY drive. Put a code.py file on the drive, and the badge runs it when you save.

Ordinary CircuitPython modules reach the display, the buttons, the joystick, the LEDs, the buzzer and the battery. The same is true for the LoRa radio, NFC, Bluetooth Low Energy and deep sleep.

Install it

  1. Flash the CircuitPython image from the Flash page.
  2. Reset the badge with no button held.
  3. The badge appears as a CIRCUITPY drive. It also gives a serial console for the REPL: /dev/ttyACM0 on Linux, a usbmodem device on macOS, a COM port on Windows.
  4. Copy the libraries and the examples to the drive, as below.

Install the libraries and examples

The examples do not run without the support libraries. An import of cyberaegg_epd or the LoRa driver fails until the libraries are in CIRCUITPY/lib. Install them first. The same archive holds the examples. They go to CIRCUITPY/examples, so you can open one and save it as code.py.

Install the libraries and examples

The e-paper and LoRa drivers and adafruit_display_text for lib, plus the fifteen examples. They go in lib and examples folders in the root of the CIRCUITPY drive, which this creates for you.

idle
This browser has no File System Access API, so it cannot write to the drive directly. Download the zip instead and unpack it into the root of the drive — it already contains the lib and examples folders.

Pick the CIRCUITPY drive when the browser asks — not the CYBR… drive, which is the badge's own storage and only appears in DFU mode.

Diagnostics

Your first program

After you install the libraries, save this file as CIRCUITPY/code.py:

import cyberaegg_epd

display = cyberaegg_epd.get_display()

get_display() returns a standard displayio.EPaperDisplay. Build a displayio group as usual. Assign the group to display.root_group, then refresh the display.

The example epd_hello.py draws a bordered white field with one black square and one red square. The example hwtest.py tests the LED, the buzzer, the charger, the battery, I²C and all the buttons.

The install above puts every example in CIRCUITPY/examples. Copy one to code.py, and the badge runs it when you save. You can also read the examples online and the libraries, or paste an example into the console below.

The firmware includes terminalio and fontio, so adafruit_display_text can draw text labels. That library is in the repository’s lib/ folder.

The serial console

CircuitPython prints print() output and tracebacks to a USB serial console. It also gives a REPL there. The console tells you why a program did not run. Errors never appear on the display.

You can open the console here, without a terminal program:

This browser has no serial support. Web Serial is required, and only Chromium-family browsers ship it (Chrome, Edge, Brave, Opera, Arc). Firefox and Safari do not. Use a terminal program instead — screen /dev/ttyACM0 115200, picocom, or PuTTY on Windows.
disconnected

Click the console to type into it. Ctrl-C stops the running program and drops you at the >>> prompt; Ctrl-D restarts code.py from the top. Ctrl-V pastes into the console once it has focus, and the Paste button does the same without needing focus — handy for pasting an example straight from this page. Multi-line code goes in through the REPL's paste mode, so its indentation survives and it runs as one block.

Drag the bottom-right corner to resize the console, or use Fullscreen for the whole screen (Esc leaves it).

Ctrl-C stops the running program and gives you the >>> prompt. At that prompt you can test the hardware one line at a time. Ctrl-D starts code.py again from the top. This is the quickest way to run a program again after an edit.

The console is only available while CircuitPython is installed. The firmware gives the console, not the bootloader.

Work with the e-paper display

E-paper does not behave like a normal screen. Most first-time surprises come from this.

The badge shows the last image it drew. E-ink holds its image with no power. An EPaperDisplay object does not change the glass when you create it. To start from a clean panel, use this code:

import cyberaegg_epd

cyberaegg_epd.clear()          # white panel, one full refresh

If your program clears the screen and then stops, call displayio.release_displays() before it stops. If you do not, CircuitPython draws its own start logo over the clean panel.

Refreshes are slow, and each one wears the panel. A full tri-color refresh takes about twenty seconds. Obey these rules:

  • Use one refresh for each start. Draw the final image with the white background in the same frame, instead of one refresh to clear and a second refresh to draw.
  • Do not refresh more often than every 180 seconds.
  • Use full refreshes only. This panel does not do partial updates.
  • If the badge stays unused for a long time, leave the panel white. This prevents image retention.

A refresh does not block the program. display.refresh() returns immediately, and the panel continues to update for about six seconds. Read display.busy if you must wait. You can also call cyberaegg_epd.refresh(display). That function waits for the minimum interval of the panel, then blocks until the refresh ends.

LoRa

The repository’s lib/ folder has a driver for the SX1262 radio. The driver uses MeshCore-compatible EU settings: 869.618 MHz, SF8, 62.5 kHz bandwidth, coding rate 4/5, sync word 0x1424, no TCXO.

The driver and the SX126x files it needs are part of the library set that you installed above. You copy no more files:

import cyberaegg_lora

radio = cyberaegg_lora.LoRa()
radio.send("hello")
data, err = radio.receive(timeout_ms=5000)

examples/lora_tx.py sends a counter. examples/lora_rx.py receives, and prints the size and the signal strength of each packet. The receiver works against a second badge that runs lora_tx.py. It also works against live MeshCore traffic. lora_dashboard.py shows live packet statistics on the e-paper display.

NFC

The firmware includes NFC, so you copy nothing into lib/. The badge serves a read-only tag. A phone that touches the badge opens a URL. See examples/nfc_tag.py and the repository’s docs/NFC.md.

Bluetooth

The badge advertises and accepts Bluetooth Low Energy connections from CircuitPython, through the standard _bleio API. It uses the factory address from FICR.DEVICEADDR. The normal Rust firmware advertises with the same address.

import _bleio

adapter = _bleio.adapter
print(adapter.address)

# Flags: LE General Discoverable, BR/EDR not supported, then the complete name.
advertisement = bytes((2, 0x01, 0x06)) + bytes((10, 0x09)) + b"CyberAegg"
adapter.start_advertising(advertisement, scan_response=None, connectable=True,
                          anonymous=False, timeout=0, interval=0.1,
                          tx_power=0, directed_to=None)

A phone scanner then shows the badge. On Linux, use this command:

bluetoothctl --timeout 20 scan le

examples/ble_advertise.py does the same, and advertises again after a central disconnects. Connections and GATT also work. examples/ble_uart.py serves a Nordic UART Service. Any BLE terminal app, such as nRF Connect or Adafruit Bluefruit Connect, connects to CyberAegg. The text you send arrives on the badge’s serial console.

Talk to the badge from a phone

Three examples use that same UART service, so they need no library and no app of your own.

examples/ble_remote.py controls the badge. Send a color name to set the LED, or beep for the buzzer. The badge reports each button press back to the phone. examples/ble_repl.py gives you a Python prompt. Send a line, and the badge runs it and returns the result. Names stay for the whole session, so you build up state line by line. This is not the CircuitPython REPL. The REPL of the supervisor cannot run over Bluetooth on this firmware, because it starts before the MicroPython heap exists.

examples/ble_telemetry.py needs no connection at all. It puts the battery voltage, the buttons and the uptime in the advertising packet, and it refreshes that packet every two seconds. Any scanner reads the values. Advertising is the most dependable part of Bluetooth here. Nothing holds the single connection slot, so many badges report at the same time, and any number of phones read them. This is the right shape for a field full of badges.

Find and connect to other badges

The badge also scans, which is the observer role. It connects out, which is the central role. It reads, writes and subscribes on the other device, which is the GATT client. Two badges therefore talk to each other in both directions.

examples/ble_scan.py lists what the badge hears, and it decodes the beacon of a badge that runs ble_telemetry.py. It scans and advertises at the same time, so two badges watch each other with no connection between them. examples/ble_central.py connects to a second badge that runs examples/ble_uart.py. The link comes up in about 0.2 seconds, and both badges report the same state. examples/ble_explore.py connects and then lists every service, characteristic and descriptor of the other device. It works against any device, not only against a badge.

examples/ble_client.py is the mirror of examples/ble_uart.py. Put ble_uart.py on one badge and ble_client.py on a second one. The client badge finds the other badge, connects, subscribes, writes a line and prints the reply. Every protocol driver has this shape. Only the UUIDs and the bytes change.

To read a remote value, read characteristic.value. To write it, assign to the same attribute. To subscribe, call characteristic.set_cccd(notify=True), and put a _bleio.CharacteristicBuffer on the same characteristic. Without the buffer, the badge receives the notifications and drops them, because nothing observes that characteristic.

The two sides negotiate the ATT MTU at each connection. The badge offers 247 bytes, so one notification carries 244 bytes instead of the minimum 20. A reply of 180 bytes then arrives in one piece. The other side has the last word, because the connection uses the lower of the two offers. Read connection.max_packet_length instead of assuming a size. A device that does not negotiate stays at 20 bytes. The link layer packet size sets the ceiling at 251 bytes.

Scanning uses the multirole controller library, because the peripheral library has no scanning. That library costs about 31 KB more flash.

Three limits are important:

  • The badge cannot pair. A device that asks for pairing before it answers does not give its attribute table to the badge.
  • Legacy advertising only. The advertisement must fit in 31 bytes.
  • Bluetooth does not work together with the display. A full tri-color refresh keeps the panel busy for tens of seconds. The background work of displayio competes with the Bluetooth poll, and a connection does not survive a redraw. Keep Bluetooth and the display in separate programs.

Bluetooth starts when a program first imports _bleio, and then stays on. This uses battery power.

The MeshCore companion app cannot talk to the badge while CircuitPython is installed. MeshCore is part of the normal badge firmware, and this image replaces that firmware.

Deep sleep

The alarm module works, with pin wake-up and time wake-up. A battery-powered program can sleep between refreshes instead of a busy loop:

import alarm
import time

alarm.exit_and_deep_sleep_until_alarms(
    alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 300)
)

E-paper keeps its image with no power. The display stays readable during the sleep.

If something goes wrong

No CIRCUITPY drive appears. The badge is still in the bootloader, or the flashing tool rebooted it instead of a reset. Reset the badge with no button held.

My code did not run. CircuitPython runs code.py from the root of the drive. Check the file name. Open the serial console. The console prints syntax errors and tracebacks. The display does not show them.

The display shows the CircuitPython logo over my drawing. Your program cleared the panel and then stopped. Call displayio.release_displays() before the program stops, or keep the program running.

The screen did not change. The firmware limits the refresh rate. It ignores a request that comes too soon after the last refresh. Wait for the interval, or use cyberaegg_epd.refresh(display). That function does the wait for you.

A Bluetooth connection times out, but scanning still works. Check the other computer or phone first. A connected Bluetooth headset takes almost all of the radio time there, and connection attempts then fail while scans still succeed. Disconnect the headset and try again.

Source

The firmware, the libraries and the examples are at rarenerd/cyberaegg-circuitpython. Go directly to the examples or the libraries. The image on the Flash page is that repository’s prebuilt binary, byte for byte. Its checksum matches the published SHA256SUMS file. The repository’s docs/BUILDING.md has the build instructions.

CircuitPython is MIT, and the badge port is Apache-2.0. The Bluetooth build also links Nordic’s SoftDevice Controller and MPSL under LicenseRef-Nordic-5-Clause. That license allows redistribution, but only for use on Nordic silicon. The badge’s nRF52840 is Nordic silicon.