MicroPython
Build firmware with the _honch_core user C module and use the Honch Python wrapper.
The MicroPython port is a thin Python wrapper over the same C core, bound through a user C module named _honch_core. It is not pure Python and not standalone: the module must be compiled into your MicroPython firmware. CircuitPython is out of scope.
Status
0.3.0. PyPI package honch-micropython. Requires firmware built with the _honch_core user C module.Before You Start
- You build MicroPython firmware yourself with the Honch user C module included. The Python wrapper alone cannot work without it — importing
honchwithout_honch_coreraisesImportError. - The wrapper rejects host-side hooks. Passing
platform,transport,battery_callback, orauto_properties_callbackto the constructor raisesInvalidArgumentError— those adapters live in the C module.
1. Build Firmware With _honch_core
Point your MicroPython build at the module's CMake file with USER_C_MODULES:
# Unix port (handy for host testing)
make -C ports/unix \
USER_C_MODULES=/path/to/SDK/ports/micropython/usermod/honch/micropython.cmakeFor a board, also freeze the Python wrapper into the image:
make -C ports/esp32 BOARD=ESP32_GENERIC \
USER_C_MODULES=/path/to/SDK/ports/micropython/usermod/honch/micropython.cmake \
FROZEN_MANIFEST=/path/to/SDK/ports/micropython/manifest.pyThe user C module does not set firmware-global options such as the GC heap size — keep that in your board or host configuration.
2. Install Or Freeze The Wrapper
The five honch/*.py wrapper modules ship on PyPI as honch-micropython and can be frozen via the manifest (recommended for boards) or installed with mip. If the wrapper is frozen into the firmware, do not also copy it into /lib.
3. Configure And Send A First Event
import honch
client = honch.Honch(
api_key="your-api-key",
endpoint_url="https://i.honch.io",
device_id="dev-board-001",
device_model="dev-board",
firmware_version="1.0.0",
event_buffer=bytearray(8192),
)
client.identify("user-123", {"plan": "beta"})
client.session_start("demo")
client.track("button_pressed", {"button": "boot"})
client.session_end()
client.flush()
client.shutdown()Required keyword arguments: api_key, endpoint_url, device_id, device_model, firmware_version, and event_buffer (a bytearray sized to your max_event_bytes, default 8192). Optional arguments map to the shared defaults: environment, batch_size, max_queued_events, max_event_bytes, transport_timeout_ms, flush_interval_seconds, flush_min_interval_ms, flush_event_threshold, flush_retry_initial_ms, flush_retry_max_ms, battery_low_threshold, and connectivity_callback.
Property values may be None, bool, int, float, str, bytes, list/tuple, or dict (with string keys), nested.
Sync the clock before tracking — the Pico W has no RTC
The Pico W (and most MicroPython boards) has no battery-backed real-time clock, so time.time() reads 2000-01-01 until you set it. Honch stamps every event with the on-device time, so if you track() before the clock is set, events upload and ingest fine but are stamped near 1970 and fall outside the dashboard's time window — they look "missing" when they are really just dated to the epoch. Sync NTP once after Wi-Fi is up and before you construct the client / track:
import ntptime
# ... after Wi-Fi is connected ...
ntptime.settime() # set the RTC from NTP (UTC)See Timestamps for how the core treats an unset clock.
4. Pump Delivery, Identity, And Connectivity
There is no background thread. Call client.tick() on an interval from your main loop, and client.flush() to force a send.
while True:
client.tick()
time.sleep(1)Connectivity is explicit. client.connectivity_changed(connected) (and the connected() / disconnected() shorthands) records the state and emits a $connectivity_change event with a state property of "connected" or "disconnected". When the wrapper believes it is offline, flush() raises OfflineError and tick() is a no-op, so it never spends time on DNS/TLS.
Transport And TLS
Uploads POST the compact chunk to <endpoint_url>/capture. On rp2 the port drives the entire exchange — connect, TLS handshake, request, and status read — on a non-blocking socket bounded by a single deadline. This matters because urequests' timeout= does not bound connect() or the TLS handshake on rp2: on a flaky or transitional link a plain upload would park the single-threaded VM indefinitely. With the bounded transport a stalled link raises promptly and the core retries on the next tick()/flush(); transport_timeout_ms (default 8000, max 10000) is the ceiling for the whole exchange.
Certificate verification
The port verifies the server certificate against the Google Trust Services root R1 — the trust anchor for the default i.honch.io endpoint, the same root the ESP-IDF and Arduino ports pin. Verification checks the certificate's validity period, which needs a real clock, so the NTP sync above is required for the handshake too (not just for timestamps) — without it the connection is rejected. For a custom or local endpoint that isn't behind Google Trust Services, call honch_transport.verify_tls(False) before constructing the client. See Security → Transport.
5. Crash And Error Reporting
# Report a non-fatal error you handled — e.g. wire this into your logging:
client.report_log_error("sensor read failed", component="imu")For uncaught crashes, pick one of the two options below. Both report the crash as a $crash carrying the exception type, message, and the Python traceback (file/line/function, crash-site first). On MicroPython the traceback is the symbolicated backtrace — there is no coredump.
Option A — wrap your entry point (works on every build). Run your main through Honch; any uncaught exception is reported, flushed, and re-raised so behavior is unchanged:
def main():
while True:
client.tick()
read_sensors()
client.run(main) # instead of calling main() directlyOption B — install a global hook (needs sys.excepthook). If your firmware is built with MICROPY_PY_SYS_EXCEPTHOOK enabled, you can hook the interpreter instead of wrapping main:
client.install_error_hook() # idempotent; returns False if sys.excepthook is unavailable
# ... later, to restore the previous hook ...
client.uninstall_error_hook()Which one to use
- Stock MicroPython firmware (including the Pico W) ships without
sys.excepthook, soinstall_error_hook()returnsFalseand captures nothing. On those builds useclient.run(main)— it relies only on a plaintry/exceptand needs no firmware changes. Build a custom firmware withMICROPY_PY_SYS_EXCEPTHOOKonly if you genuinely cannot wrap your entry point. - Either way the
$crashis reported in-process: a fatal crash is delivered only if the flush completes before the board resets (or you supply a durable queue).client.run()flushes for you before re-raising. - There is no coredump on MicroPython (that is ESP-IDF only); the Python traceback is the equivalent readable backtrace.
Wrapper exceptions subclass HonchError: InvalidArgumentError, StorageError, RejectedError, NotInitializedError, CompressionUnavailableError, and TransportError — with OfflineError, RateLimitedError, and ServerError subclassing TransportError.
Examples
ports/micropython/examples/ includes basic.py (above), a persistent-queue example, and a Pico W example.
Debugging Failures
Client.last_error() returns a dict describing the most recent failure, so you can see why a flush failed rather than just that it did:
try:
honch.flush()
except HonchError:
err = honch.last_error()
print(err)
# {'status': 'rejected', 'reason': 'auth_invalid_key', 'http': 401,
# 'os_error': 0, 'message': 'API key invalid or revoked', 'component': 'http'}status is 'ok' and reason is 'none' when nothing has failed yet. The SDK also logs the same one-line summary once per distinct failure through the platform log hook. See Error Context & Diagnostics.
Public API
| Method | Purpose |
|---|---|
Honch(**config) | Construct and initialize the client. |
track(event, properties=None) | Queue an event. |
last_error() | Dict of structured detail (reason, http, os_error, message) for the last failure. |
identify(distinct_id, traits=None) | Set identity; emits $identify. |
set_property(key, value=None) | Emit $set_property. |
session_start(name=None) / session_end() | Bracket a session. |
report_log_error(message, *, component=None) | Report a handled, non-fatal error as an $error. |
run(fn, *args, **kwargs) | Run your entry point with crash capture: an uncaught exception becomes a $crash (with traceback), is flushed, and re-raised. Works on every build (no sys.excepthook needed). |
install_error_hook() / uninstall_error_hook() | Alternative to run(): capture uncaught exceptions globally via sys.excepthook (returns False if the firmware lacks it). |
connectivity_changed(connected) / connected() / disconnected() | Report connectivity; emits $connectivity_change. |
tick() | Cooperative delivery step. |
flush() | Send queued batches now (raises OfflineError when offline). |
reset() | Clear identity, session, and queue. |
shutdown() | Emit $device_shutdown, final flush, tear down. |
get_device_id() | Return the device ID. |
queue_stats() | Return queue depth and counters as a dict. |