Skip to main content

Microcycler Python SDK

Python SDK for the Microcycler battery testing platform.

Status: Pre-alpha — API subject to change.

Installation

pip install microcycler

The package is a thin Python layer over a Rust core (core/ in the repository: the same code the Microcycler dashboard and the headless agent run), shipped as one prebuilt wheel per platform (Windows, macOS, Linux; Python 3.9+). pip install microcycler needs no compiler. Installing from a source checkout (pip install -e sdk) builds the core and needs a Rust toolchain (rustup.rs).

Two ways to run an experiment

  • Standard: build a recipe from steps (CC, CV, rest, loop, EIS, …) and store it on the cycler. It runs on the cycler by itself, so the computer can be switched off (the Recipes section below).
  • Dynamic: a Python script on a host computer drives the cycler directly over TCP/UDP, for a test that does not fit the building blocks. The computer must stay connected: if the script dies or the host goes quiet for 30 s, the cycler releases the output on its own. The cycler reports its state as DYNAMIC while a script holds it. Scripts are never uploaded to a cycler; one process runs per cycler:
microcycler-dynamic run my_test.py --cycler 192.168.1.50 --cycler 192.168.1.51 --cell-id NMC-A1-001 --cell-id NMC-A1-002 --run-id run-7
from microcycler.dynamic import context

async def main():
    ctx = context()                                   # the cycler this process drives, and the test's metadata
    async with ctx.cycler() as c, c.dynamic(voltage_upper=4.2, voltage_lower=3.0, current_upper=0.5) as out:
        await out.set_current(0.2)
        ...

C-rates and the Capacity Check

Wherever a step takes a current you can give amps or a C-rate: CC(current=0.25) is 250 mA, CC(current=C(1)) is 1 C (250 mA for a 250 mAh cell) and C(0.25) is a quarter of that. The cycler turns a C-rate into amps when the test starts, from the capacity you give it, so one recipe serves cells of any size; start_recipe refuses (before sending anything) a recipe that uses one without capacity_mah:

from microcycler.recipes import C, CC, CapacityCheck, Recipe

recipe = Recipe("Characterise", steps=[CapacityCheck(charge_current=C(0.25), discharge_current=C(0.25), cutoff_current=C(0.05)), CC(current=C(1), cutoff_voltage_high=4.2)])
await cycler.start_recipe("characterise.json", cell_id="NMC-A1-001", run_id="run-7", content=recipe,
                          capacity_mah=250, v_min=2.5, v_max=4.2)

CapacityCheck(charge_current=..., discharge_current=..., cutoff_current=...) charges to the top voltage, holds until the current tapers to the cutoff (default C/20), rests, and discharges to the bottom voltage; the discharge counter starts from zero, so what it delivers is the result. The capacity it delivers is saved on the cycler's SD card as one small artifact file (<result file>_CAP_sNN.csv), so the result is kept whether or not a computer is connected; the dashboard adds it to the cell's record when it next sees the cycler. Leave voltage_max / voltage_min out to use the cell's own window (v_min / v_max).

Recipes on the cycler

A cycler stores recipes on its SD card (recipes/<name>.json); upload one once and any client can start it by name:

name = await cycler.upload_recipe(Recipe("NMC formation", steps=[...]))     # -> "NMC_formation.json"
print(await cycler.recipe_files())
await cycler.start_recipe(name, cell_id="NMC-A1-001", run_id="run-7", test_type="formation")

cell_id, run_id and test_type (1-24 characters from A-Za-z0-9._-) name the data files the test writes under data/ on the card. The dashboard and the phone start recipes the same way.

Set up a headless computer (for example a Linux box in the lab) in two steps, with no browser and nothing to copy:

pip install microcycler
microcycler-agent enroll --install-service     # prints a code like ABCD-EFGH

An admin enters the code in the dashboard (Settings → Organization → Uplink → Add a computer) after checking the fingerprint, and the computer then uploads this network's licensed cyclers to your organization as a background service. Whether uplink is persistent is explicit: enroll and import ask "Keep uplink running in the background and start it at boot?" (or pass --service / --no-service), microcycler-agent run says on its first line whether it is running in the foreground (stops when the terminal closes) or as a service, every status line carries mode and persistent, and microcycler-agent status shows the service state. Only one agent runs per computer: starting a second one (another terminal, the service, or the dashboard) is refused with who already has it. microcycler-agent run runs it in the foreground instead; import <credential> is the offline alternative for a credential file made in the dashboard. Local telemetry needs UDP 5001/5002 open on a host firewall (sudo ufw allow in proto udp from 169.254.0.0/16 to any port 5001,5002, adjusting the source range to your cyclers' subnet).

Quickstart

Connect and read telemetry

import asyncio
from microcycler import Cycler

async def main():
    async with Cycler("192.168.1.100") as cycler:
        print(await cycler.identify())
        t = await cycler.get_telemetry()
        print(f"State:    {t.state}")
        print(f"Voltage:  {t.voltage:.3f} V")
        print(f"Current:  {t.current:.3f} A")
        print(f"Temp:     {t.temperature:.1f} °C")
        print(f"Capacity: {t.capacity_mah:.1f} mAh")

asyncio.run(main())

Discover cyclers on the network

import asyncio
from microcycler import Cycler, discover

async def main():
    # Listens for the discovery beacon on UDP 5002. The beacon is periodic
    # and unsolicited, so this is a collection window, not a deadline —
    # allow at least one beacon period (10 s by default).
    for dev in await discover():
        print(dev)                      # HADES-042 @ 169.254.10.5:5000 (standby)
        async with Cycler(dev.host, port=dev.port) as cycler:
            print(await cycler.get_life())

asyncio.run(main())

Stream live telemetry via UDP

from microcycler.streaming import CyclerStream

async def monitor():
    async with CyclerStream() as stream:
        async for telemetry in stream:
            print(telemetry.voltage, telemetry.current)

Build and run a recipe

from microcycler import Cycler
from microcycler.recipes import Recipe, CC, CV, Rest, Loop

recipe = (
    Recipe(name="Standard Formation")
    .add(CC(current=0.1, cutoff_voltage_high=4.2))
    .add(CV(voltage=4.2, limit_current_high=0.1, cutoff_current_low=0.02))
    .add(Rest(duration=1800))
    .add(CC(current=-0.2, cutoff_voltage_low=2.8))
    .add(Rest(duration=1800))
    .add(Loop(cycles=3))
)

async def run():
    async with Cycler("192.168.1.100") as cycler:
        await cycler.load_recipe(recipe)
        await cycler.start()

Recipe steps

Step Description
CC(current, cutoff_voltage_high, cutoff_voltage_low, cutoff_time, cutoff_capacity) Constant current. current is signed: positive = charge, negative = discharge. cutoff_capacity (mAh) ends the step after that much charge in either direction; CV, CP and CR take it too.
CV(voltage, limit_current_high, cutoff_current_low, cutoff_time) Constant voltage
CP(power, cutoff_voltage, cutoff_time) Constant power
CR(resistance, cutoff_voltage, cutoff_time) Constant resistance
Rest(duration) Open-circuit rest
EIS(freq_start, freq_end, amplitude, points_per_decade) Impedance sweep (freq_start is the lower frequency)
Loop(cycles, jump_to) Return to step jump_to (1-based) cycles more times
GoToSoc(target_soc, current, soc, capacity_mah, ...) Take the cell to a state of charge by counting mAh from the running state of charge (anchored by a Capacity Check, or at 0 / 100 %); needs the cell's capacity
CapacityCheck(charge_current, discharge_current, ...) Charge, rest and discharge that measure the capacity
ACRipple(duration, dc_current, ac_amplitude, ac_freq, cutoff_temp) AC ripple excitation for calorimetry / thermal characterisation
Pulse(amplitude, width, lead, tail, current) A current pulse on a DC base (edges exact to 20 µs)
Gitt(current, pulse, rest, rest_dvdt, pulses, ...) Galvanostatic intermittent titration: pulses and settling rests, recorded in a GITT file; analysed with microcycler.analysis.gitt (OCV, IR drop, diffusion coefficients)
Hppc(current, charge_current, soc_current, pulse, rest, soc_step, soc_rest, levels, ...) Hybrid pulse power characterisation: pulse sets at stepped states of charge, recorded in an HPPC file; analysed with microcycler.analysis.hppc (resistance and pulse power against state of charge). Needs the cell's capacity.
Pitt(voltage_start, voltage_end, voltage_step, limit_current_high, cutoff_current_low, ...) Staircase of constant-voltage holds, each until its current has decayed, recorded in a PITT file; analysed with microcycler.analysis.pitt (incremental capacity, diffusion time constants)
Profile(file, scale, repeat, ...) Replays a current or power time series stored on the cycler's SD card (profiles/<file>); upload_profile, profile_files, profile_info, delete_profile, and microcycler.profiles build the files

Any step takes .with_capture(duration, start, rate): a window of the control loop's voltage and current at 50 kS/s, saved on the cycler as a SCOPE file. See the recipe steps page.

License

MIT


Lifetime wear counters

Every board keeps an odometer in its EEPROM. Three independent axes, because a unit can be worn out along any one of them while the others look fine: mechanical (relay actuations), calendar (powered time) and electrical (charge through the output).

from microcycler import Cycler

async with Cycler("192.168.1.200") as cycler:
    life = await cycler.get_life()

    print(f"boots            {life.boot_count}")
    print(f"powered          {life.powered_hours:,.0f} h")
    print(f"throughput       {life.throughput_ah:,.1f} Ah")
    print(f"MAIN relay       {life.relay_main:,} closings")
    print(f"FAN relay        {life.relay_fan:,} closings")

    if not life.durable:
        print("WARNING: counters are not reaching the EEPROM")

These belong to the physical board, not to your database, so they survive a reinstall, an MCU swap, or the unit moving to another lab. There is deliberately no way to reset them — an odometer that can be wound back is not evidence of anything.

life.durable (i.e. write_errors == 0) is the field worth alerting on. A unit with a dead EEPROM still counts perfectly well in RAM and reports entirely plausible numbers; nothing else distinguishes a kept odometer from one that is lost at every power cut.

Counters are flushed at most once every few minutes to spare the part, so up to that much accrued wear is in RAM only. Before deliberately powering a unit down:

await cycler.flush_life()

Metadata

Release files for microcycler 0.1.39

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for microcycler 0.1.39
File Size Uploaded Provenance
microcycler-0.1.39.tar.gz 253.9 kB Without provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for microcycler 0.1.39
File
microcycler-0.1.39-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details
microcycler-0.1.39-cp39-abi3-manylinux_2_28_aarch64.whl CPython 3.9 abi3 Linux glibc 2.28+ ARM64 Details
microcycler-0.1.39-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details
microcycler-0.1.39-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
microcycler-0.1.39-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 21.9 MB

Release files / microcycler-0.1.39.tar.gz

Download URL microcycler-0.1.39.tar.gz
Size 253.9 kB
Tags Source
SHA-256 checksum
How to use checksums
202e7a5eb366269b7f6617045c81fe0bc17664ea42386f43d8216528204b1c69
BLAKE2b-256 checksum
How to use checksums
b5a1053ca3c1877ef4f8c86c43a8a30643de3f52e855e09f40c61a7a425c285c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / microcycler-0.1.39-cp39-abi3-win_amd64.whl

Download URL microcycler-0.1.39-cp39-abi3-win_amd64.whl
Size 4.2 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a7da5ea8692d65fa037ee7dd004e9b524caabf4d5688cdea7c33bf87e8d940ce
BLAKE2b-256 checksum
How to use checksums
fd0444c816036056f18628bb199808429973faaacc5e8088e2711bd051926165
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / microcycler-0.1.39-cp39-abi3-manylinux_2_28_aarch64.whl

Download URL microcycler-0.1.39-cp39-abi3-manylinux_2_28_aarch64.whl
Size 4.0 MB
Tags CPython 3.9 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
b3eefb2ca8f5818a0ea3e2a687f20bb33a29037d81db8d33caf98be81c61003d
BLAKE2b-256 checksum
How to use checksums
079339086b35399cde1238b00b2b3e568d4e909f76454c9bceeec4a28c1985f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / microcycler-0.1.39-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL microcycler-0.1.39-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 4.8 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
0794b55b3d482b4a045151368ae94d0689019ac9eb39ac69390f6aa89d8711e6
BLAKE2b-256 checksum
How to use checksums
af670abc6f42154666688a370a9e2be24038d9cf8dd9630119c3955459755c3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / microcycler-0.1.39-cp39-abi3-macosx_11_0_arm64.whl

Download URL microcycler-0.1.39-cp39-abi3-macosx_11_0_arm64.whl
Size 4.3 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
5382c06d0ca0a709b0dd0198db4cc1ec9e4397bda86a467b339c1934d699ad06
BLAKE2b-256 checksum
How to use checksums
1291254f2d5983913f172923be9898e2c3e111340d4c1a30d67b850d5172f939
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / microcycler-0.1.39-cp39-abi3-macosx_10_12_x86_64.whl

Download URL microcycler-0.1.39-cp39-abi3-macosx_10_12_x86_64.whl
Size 4.4 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
908bfd6780f6f02f5eeaca9ac20445c38d27c55a2737bd802f8977d286cf84a6
BLAKE2b-256 checksum
How to use checksums
b571d332201c8ebb77eb44dca52e8b5dc8d8f5111be40d0688baab1b0b6f649f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Security for microcycler 0.1.39

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.39 This release

6 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page