2.1 Module & Library Reference (API Guide)
Scope
This page documents the production code tree β app/main.py, src/modules/ (high-level wrappers) and src/lib/ (low-level drivers) β as tracked on this repository's main branch. If you work from the internal development sandbox, some experimental modules (async variants, alternate storage drivers, etc.) may not be listed here yet because they haven't been promoted to this repository. See [8. Development Workflow (Sandbox β Production)] (coming soon) for how that promotion works.
This page complements 2. Architecture Overview: that page explains the src/lib vs src/modules split conceptually, this one is the per-file reference β what each module does, what it depends on, and what configuration it reads.
2.1.1 Quick Reference
Module (modules/) |
Purpose | Sync/Async |
|---|---|---|
utils.py |
Logging (log_debug/log_info/...) and small helpers. Imported by almost everything. |
sync |
config_manager.py |
Loads static_config.json / dynamic_config.json, applies remote (downlink) config updates. |
sync |
_auth.py |
PIN-protected REPL console lock (AES). | sync |
version.py |
Firmware version constant. | sync |
power_manager.py |
RTC (DS3231/RV3028), wake-up scheduling, voltage rails, deep sleep. | sync |
rtc_memory.py |
Deep-sleep-persistent state (cycle counter, valve states, payload buffer). | sync |
wifi.py |
Wi-Fi STA connection, enforces WPA2+. | async |
nb_iot.py |
Nordic nRF9160 modem driver: network registration, MQTT-over-modem, HTTP (OTA), GPS/NTN. | async |
lorawan.py |
RAK3172 (RUI3 AT) LoRaWAN modem driver: join, uplink, downlink, Class C polling. | async |
umqttsimple.py |
Lightweight MQTT client (Wi-Fi transport only). | sync |
ble_manager.py |
BLE GATT server for local config/monitoring (magnet-triggered). | async |
downlink_manager.py |
Unifies downlink handling across Wi-Fi/NB-IoT/LoRaWAN; triggers OTA and remote config. | async |
remote_repl.py |
Remote Python REPL over MQTT (debugging backdoor β see notes). | mixed |
analog_sensor.py |
ADS1115 4-20mA analog input wrapper. | sync |
digital_sensor.py |
ULP-based low-power pulse counter. | sync |
bme_sensor.py |
Auto-detecting BMP280/BME280/BME680 wrapper (temp/hum/press/gas + IAQ). | sync |
sht30_sensor.py |
SHT30 temp/humidity wrapper. | sync |
max31865_sensor.py |
PT100/PT1000 (via MAX31865) wrapper. | sync |
modbus_sensor.py |
Modbus RTU master wrapper over RS485. | sync |
accel_manager.py |
LIS2DH12 + MCP23008 anti-theft/tamper detection. | sync |
battery_monitor.py |
ESP32 ADC battery voltage fallback (no MAX17048). | sync |
internal_storage.py |
FIFO payload backup on internal flash. | sync |
eeprom_memory.py |
FIFO payload backup on external I2C EEPROM (24LC1025) β survives full power loss, not just deep sleep. | sync |
update_manager.py |
OTA support: checksum, base64 decode, safe main.py replacement. |
sync |
led_manager.py |
ULP-driven status LED blink pattern. | sync |
2.1.2 Modules β Detailed Reference
Core / Boot
utils.py
- Purpose: centralized logging (
log_debug,log_info,log_warning,log_error) gated bystatic_config.log_level, plusget_datetime_string()/save_data_to_file(). - Depends on: nothing (base module β imported by nearly every other module).
- Config:
static_config.log_level, resolved fromconfig_manageron first use (a deferred import βconfig_manager.pyimportsutilsat module level, so importing back atutils' module level would be circular).
config_manager.py
- Purpose: the canonical owner of
static_config.json(immutable, hardware) anddynamic_config.json(runtime-mutable). Applies remote config updates viaCONFIG_MAP, which maps ~90 remote command names (e.g.setLatencyTime) to nested JSON paths. - Public API:
ConfigManager.get_static(*keys, default=None),get_dynamic(*keys, default=None),apply_single_update(...),apply_conf_update(decoded_data),save_dynamic_config(). Global singleton:config_manager. - Depends on:
utils.
_auth.py
- Purpose: PIN-gated REPL console lock. Runs automatically on import (not just definitions) β up to 3 attempts, resets the device on failure.
- Public API:
run_authentication(),pad_data(...),get_encrypted_pin_secret(). - Depends on:
config_manager(readsstatic_config.pin); uses builtincryptolib(AES-ECB) directly.
version.py
- Purpose: single
VERSIONconstant, shown in the boot banner. Bump this on every release.
Power & Time
power_manager.py
- Purpose: the system's power/time hub. Detects the RTC chip present (DS3231 or RV3028), syncs time from NB-IoT/Wi-Fi(NTP)/LoRaWAN/GPS, computes the next aligned wake-up, drives the 12V/5V rails and digital outputs, sets CPU frequency, configures wake sources, and triggers deep sleep. Global singleton:
pm. - Public API (selection):
check_rtc_status(),set_rtc_time(time_str, mode),seconds2wakeup(),control_5v(state),control_vdc(state),configure_wakeup_sources(...),go_to_sleep(). - Depends on:
lib/uds3231orlib/RV3028(whichever is detected on I2C). - Config:
static_config.pinout.{i2c,rs485,control,magnet_pin},dynamic_config.general.{rtc_sync,latency_time,magnet_wakeup},dynamic_config.digital_config.{enable,counter}. - Note:
downlink_manager.py's proportional-valve (EV) and OTA-failure sleeps use plaintime.sleep_ms()/await asyncio.sleep_ms()directly rather than a method onpower_managerβ keep that in mind if you're looking for a "sleep" helper here, there isn't one.
rtc_memory.py
- Purpose: state kept in ESP32 RTC memory (survives deep sleep, not power loss): accumulated cycle counter, alarm flag, manual valve (EV) states, last RTC-sync timestamp, and a buffer of encoded LPP payloads pending transmission.
- Public API:
get_alarm_flag/set_alarm_flag,get_last_rtc_sync/set_last_rtc_sync,rtc_resync_due(...),get_ev_state/set_ev_state,store_payload(...),get_payloads(),should_transmit(). - Depends on:
config_manager(readsgeneral.register_acumulator). - Note: hand-rolled binary layout with fixed byte offsets; respects the ~2048-byte hardware limit of
rtc.memory(), trimmingn_cyclesautomatically if config exceeds it.
Connectivity
wifi.py
- Purpose: Wi-Fi STA connection with a security gate β refuses to associate below WPA2-PSK (explicit CRA/Cyber Resilience Act compliance comment in the code).
- Public API:
is_connected(),do_connect(ssid, password, timeout_seconds=15)(async),do_disconnect()(async). - Depends on: builtin
networkonly. SSID/password are passed in bymain.pyfromdynamic_config.communications.wifi.
nb_iot.py (largest module, ~57 KB)
- Purpose: driver for the Nordic nRF9160-family NB-IoT/LTE-M modem (Nordic SLM AT%/AT#X command set): network registration (with operator selection/blacklist), MQTT client over the modem, HTTP client for OTA downloads, GPS/NTN, sleep/wake management.
- Public API (all
asyncunless noted):connect(connection_preference, edrx=True, apn=None, ntn=False),wake_up(),sleep(),hard_reset(),mqtt_configure/connect/publish/subscribe,get_mqtt_messages()(sync),send_udp_data(...),download_file(...)(chunked HTTP range requests for OTA),get_gps_coords(...),connect_ntn(...),get_signal_data(). - Depends on: no
lib/drivers β talks AT directly over UART. - Config:
dynamic_config.communications.cellular_iot.{external_sim,preference,apn,ntn,signal_data},static_config.pinout.control.en_nbiot_pin,static_config.pinout.nb-iot.{tx_pin,rx_pin}. - AT command groups used (grep-verified against source):
- Network:
AT+CFUN=,AT#XGPIOCFG/AT#XGPIO(SIM select),AT%XSYSTEMMODE=,AT%XBANDLOCK=,AT+CGDCONT=,AT%XMONITOR,AT%RAI,AT%PERIODICSEARCHCONF=,AT+CEDRXS=,AT%XPTW=,AT+COPS=(scan/manual/auto),AT+CPSMS=,AT+CEDRXRDP,AT+CESQ,AT+CGPADDR=. - Clock:
AT+CCLK?. - Sleep/reset:
AT#XSLEEP=,AT#XRESET(soft);hard_reset()instead cuts modem power physically viaen_nbiot_pinfor 5s. - GPS:
AT#XGPS=1,0,0,0/AT#XGPS=0. - MQTT over modem:
AT#XMQTTCFG=,AT#XMQTTCON=,AT#XMQTTPUB=,AT#XMQTTSUB=(inbound messages arrive as#XMQTTMSG:URCs). - UDP socket:
AT#XSOCKET=,AT#XSENDTO=. - HTTP (OTA):
AT#XHTTPCCON=,AT#XHTTPCREQ="GET",...,"Range: bytes=X-Y"(chunked download; response via#XHTTPCRSP:URC). - Note: comments like
"USER BASE v17","CHANGE","NEW"around the OTA download path suggest this is the most actively iterated/least stable part of the module currently β worth extra care/testing before shipping changes here.
lorawan.py
- Purpose: driver for a RAK3172-class LoRaWAN modem (standard RUI3 AT set): join, uplink send, downlink receive (including Class C via polling), network time sync.
- Public API (all
asyncunless noted):connect(lorawan_class="A", attempts=1),join_network(),send_uplink(port, data),get_downlink_messages(),request_time(),get_network_time(),sleep(),check_network_connection(). - Depends on: no
lib/drivers β talks AT directly over UART. Reuses the same physical UART pins asnb_iot.py(static_config.pinout.nb-iot.{tx_pin,rx_pin}) β confirms WiFi/NB-IoT/LoRaWAN modem variants are mutually exclusive at the hardware level, selected bystatic_config's modem field. - Config:
dynamic_config.communications.lorawan.{dev_eui,app_key,app_eui,class,network_mode,join_mode,band}. - AT command groups used:
AT+NWM=(network mode),AT+NJM=(join mode ABP/OTAA),AT+CLASS=,AT+BAND=,AT+CFM=(confirmed uplinks),AT+DEVEUI=/AT+APPEUI=/AT+APPKEY=,AT+JOIN=1:0:<interval>:<attempts>(waits for+EVT:JOINED),AT+TIMEREQ=1+AT+LTIME=?(network time),AT+SEND=<port>:<data>(waits+EVT:SEND_CONFIRMED_OKor+EVT:TX_DONE),AT+LPM=1/AT+SLEEP(low power),ATZ(reset),AT+NJS=?(join status),ATC+GETDL(custom: poll buffered Class C downlinks), and passive+EVT:RX...URCs parsed for real-time downlinks.
umqttsimple.py
- Purpose: lightweight MQTT 3.1.1 client over TCP/TLS socket, used only for the Wi-Fi transport.
- Origin: based on Paul Sokolovsky's classic
micropython-libMQTT client (2013β2016), modified by ISURKI/Steminds βcheck_msg()was rewritten to return a list of all pending messages instead of a single callback. QoS 2 is not implemented. Technically lives inmodules/but is functionally a third-party library β arguably belongs closer tolib/.
ble_manager.py
- Purpose: BLE GATT server for local configuration/monitoring, activated by the magnet-wakeup mode. One notify characteristic (data) and one write characteristic (commands).
- Public API:
BLEManager(device_name=..., command_callback=...),update_data_payload(payload),stop(). - Depends on:
lib/aioble(Service/Characteristic/advertise/security). - Config:
static_config.pin(same 6-digit PIN as_auth.py, used for fixed BLE pairing). - Note: implements LE Secure pairing/bonding with MITM protection and a fixed PIN (DISPLAY_ONLY IO capability); rejects commands on unencrypted characteristics.
downlink_manager.py
- Purpose: unifies downlink handling across the three transports (Wi-Fi, NB-IoT, LoRaWAN): manual SD/EV commands, remote config application (LPP-decoded), and OTA kick-off (NB-IoT only).
- Public API:
process_wifi_downlinks(...),process_nbiot_downlinks(...),process_lorawan_downlinks(...)(allasync),apply_config(hex_text),is_manual_command(text). - Depends on:
lib/IsurlogLPP,lib/ota.rollback; deferred imports ofmodbus_sensor,update_manager,remote_repl. - Note: contains the full OTA flow β download, SHA-256 checksum verification, base64 decode, partition write, and
rollback.cancel_force()on failure.
remote_repl.py
- Purpose: remote Python REPL over MQTT (Wi-Fi or NB-IoT) for field debugging β receives Python code as an MQTT message,
eval/execs it, returns captured stdout on another topic. - β οΈ Security note: this executes arbitrary code with no authentication beyond whatever the MQTT broker/channel provides. It's a powerful admin backdoor β worth confirming the MQTT channel is properly secured (TLS + broker ACLs) wherever this is enabled in the field.
Sensors
analog_sensor.py β ADS1115-based 4-20mA wrapper; read_analog(channel), convert_value(value, zero, full_scale). Depends on lib/ADS1115. Config: static_config.pinout.i2c.*, static_config.ads1115_addr; per-channel zero/full_scale come from dynamic_config.analog_config.inputs[].
digital_sensor.py β ULP-based low-power pulse counter with software debounce (only GPIO 36/39, RTC-capable pins). Depends on lib/esp32_ulp. Reads its wake-up edge count from dynamic_config.digital_config.inputs[] β a list with one entry per channel β by finding the entry where channel == 0 and reading its wake key.
bme_sensor.py β auto-detects BMP280/BME280/BME680 via I2C CHIP_ID; optional IAQ (air quality) calculation with gas-sensor burn-in. Depends on lib/pimoroni_bme680, lib/bme280_float. Note: _burn_in() blocks for up to 300s if IAQ=True β main.py currently instantiates it with IAQ=False.
sht30_sensor.py β SHT30 temp/humidity wrapper. Depends on lib/SHT30.
max31865_sensor.py β PT100/PT1000 wrapper over SPI. Depends on lib/adafruit_max31865. Reads wires from dynamic_config.pt100_config.wires (default 4 if absent).
modbus_sensor.py β Modbus RTU master over RS485 (holding/input registers, coils, discrete inputs, register writes, 2-register-to-float conversion). Depends on lib/umodbus. Heavily used by main.py, downlink_manager.py, and isurnode integrations.
accel_manager.py β anti-theft: LIS2DH12 accelerometer + MCP23008 I/O expander to detect movement/tamper and wake from deep sleep. Depends on lib/mcp23008, lib/LIS2DH12. Note: _verify_theft() blocks for up to 10s polling β fine as called today, but not asyncio-friendly if reused elsewhere.
battery_monitor.py β ESP32-internal ADC battery voltage reading (fallback when no MAX17048 fuel gauge is present). No config, no lib/ dependency.
Storage & OTA
internal_storage.py β FIFO payload backup on internal flash (used when general.internal_register is enabled); auto-trims oldest lines when free space or line count thresholds are hit. Note: delete_oldest_lines() rewrites the whole file each time β O(n) per cleanup, worth watching flash wear if the log grows large.
eeprom_memory.py β FIFO payload backup on an external I2C EEPROM (24AA1025/24LC1025/24FC1025, 128KB), initialized once from main.py (init_eeprom_memory()) as a same-level counterpart to rtc_memory.py rather than something it calls into directly β main.py decides which of the two to write to, and migrates the RTC backlog to EEPROM if RTC RAM fills up. Unlike RTC RAM, this survives a full power loss, not just deep sleep. Payload slots are sized to exactly one EEPROM page (32/64/128/256 bytes, configurable via general.max_payload_size), so every write is a single atomic page write. Depends on lib/EEPROM24LC1025. Config: static_config.pinout.i2c.*, general.max_payload_size, general.eeprom_overwrite_oldest.
update_manager.py β OTA support utilities: SHA-256 checksum verification, chunked base64 decode, and safe main.py replacement (backup + rename, with rollback attempt on failure).
Peripherals
led_manager.py β ULP-driven status LED blink pattern (configurable pulses/bursts/timing), so the CPU doesn't have to stay awake for it. Depends on lib/esp32_ulp. Shares the ULP coprocessor with digital_sensor.py's pulse counter β is_enabled() acts as a mutex between the two (they can't run simultaneously).
2.1.3 Library Reference (lib/)
| File / package | What it is | Origin |
|---|---|---|
adafruit_max31865.py |
MAX31865 RTD amplifier driver (SPI) | Third-party β Adafruit, MIT |
ADS1115.py |
ADS1115 16-bit ADC driver (I2C) | Third-party β W. Ewald, MIT |
aioble/ |
Async BLE (GATT, pairing, L2CAP) | Third-party β official micropython-lib, MIT |
bme280_float.py |
BME280 driver (I2C) | Third-party β Adafruit-derived, MIT/BSD-style |
crontab.py |
Cron-expression scheduling (MicroPython port, wired to power_manager as its time source) |
Third-party β MicroPython port of Josiah Carlson's crontab, LGPL v2.1/v3. Not currently used by any module β vendored with a reserved config key (crontab: "inactive") but not yet wired up. |
EEPROM24LC1025.py |
24AA1025/24LC1025/24FC1025 I2C EEPROM driver (128KB, dual 64KB-block addressing) | ISURKI-authored, GPL-3.0-or-later. Used by modules/eeprom_memory.py. |
esp32_ulp/ |
ULP coprocessor assembler/linker | Third-party β micropython-esp32-ulp, MIT |
IsurlogLPP.py |
ISURKI's own "Isurlog LPP" payload codec (Cayenne-LPP-like, custom sensor + config types) | ISURKI own, GPL-3.0-or-later. Central interoperability piece with config_manager.py's CONFIG_MAP. |
LIS2DH12.py |
LIS2DH12 accelerometer driver (I2C) | Third-party β Quectel, Apache 2.0 |
max1704x.py |
MAX17048/17044 LiPo fuel-gauge driver (I2C) | Third-party (A. Peeters), adapted |
mcp23008.py |
MCP23008 I/O expander driver (I2C) | Third-party β M. Causer, MIT |
mcp4017.py |
MCP4017 digital potentiometer driver (I2C), sets the VDC boost regulator output | ISURKI own, GPL-3.0-or-later |
ota/ |
ESP32 partition OTA framework (block writer, rollback, status) | Third-party β G. Moloney, MIT |
pimoroni_bme680.py |
BME680 driver (I2C) | Third-party β Pimoroni, MIT |
RV3028.py |
RV3028 low-power RTC driver (I2C) | Third-party β Core Electronics/Makerverse |
SHT30.py |
SHT30 temp/humidity driver (I2C) | Third-party β R. SΓ‘nchez, Apache 2.0 |
uds3231.py |
DS3231 RTC driver (I2C) | Third-party β Adafruit-derived, adapted by ISURKI, MIT |
umodbus/ |
Full Modbus RTU/TCP master+slave stack (v2.3.7) | Third-party β Pycom, GPL v3 + Pycom License v1.0 |
2.1.4 Module Dependency Map
main.py
ββ power_manager ββββ lib/uds3231 | lib/RV3028
ββ rtc_memory (same-level as eeprom_memory below - main.py routes
ββ eeprom_memory βββββ lib/EEPROM24LC1025 between them, they don't call each other)
ββ led_manager βββββββ lib/esp32_ulp
ββ accel_manager βββββ lib/mcp23008, lib/LIS2DH12
ββ downlink_manager ββ lib/IsurlogLPP, lib/ota.rollback
β ββ modbus_sensor ββ lib/umodbus
β ββ update_manager
β ββ remote_repl
ββ config_manager ββββ utils
ββ lib/mcp4017, lib/IsurlogLPP, lib/ota.rollback (direct, top-level)
ββ (deferred, config/hardware-dependent)
ββ wifi βββββββββββ umqttsimple
ββ nb_iot
ββ lorawan
ββ ble_manager ββββ lib/aioble
ββ battery_monitor
ββ sht30_sensor βββ lib/SHT30
ββ bme_sensor ββββββ lib/pimoroni_bme680, lib/bme280_float
ββ max31865_sensor β lib/adafruit_max31865
ββ analog_sensor βββ lib/ADS1115
ββ digital_sensor ββ lib/esp32_ulp
ββ internal_storage
utils.py is imported by nearly every module above (omitted from the tree for readability).
main.py doesn't import every module unconditionally β sensors and connectivity modules are imported lazily, gated by static_config/dynamic_config (which modem, which sensors are enabled, whether a magnet-wakeup or OTA/REPL downlink was received). This keeps RAM usage down at boot, which matters on MicroPython.