Networking¶
scrollkit.network provides WiFi connection management and a cross-platform HTTP
client.
WiFiManager¶
scrollkit.network.wifi_manager.WiFiManager handles CircuitPython WiFi:
connecting (with retries), reconnecting, scanning networks, creating the
adafruit_requests session the HTTP client uses after a successful connection
(create_http_session()), and — when the device has no working credentials —
running the WiFi onboarding portal
over its own access point (start_access_point() / stop_access_point() /
run_setup_portal()).
Credentials are resolved settings first, secrets.py second: whatever the
onboarding portal saved into settings.json (wifi_ssid / wifi_password)
beats a stale secrets.py, so a device configured from a phone never needs a
file edited.
scrollkit.network.wifi_manager.is_dev_mode() reports whether a real WiFi
radio is available (always False on CircuitPython; True on desktop unless
the test suite mocks a wifi module) — the canonical desktop-vs-device check
for network code.
WiFi onboarding portal (no file editing)¶
A brand-new (or moved) device has no way onto the local network, and asking a
user to edit secrets.py defeats the point of a finished product. The
onboarding portal fixes that end-to-end:
class MyApp(ScrollKitApp):
async def setup(self):
wm = WiFiManager(self.settings)
if not await wm.connect():
# Blocks here until the user configures WiFi from a phone,
# then reboots the device with the saved credentials.
await wm.run_setup_portal(display=self.display)
...
What the user sees:
- The panel scrolls “WiFi setup: join "WifiManager_XXXX" (password: password) then open http://192.168.4.1”.
- They join that access point from a phone and open the address: a page lists the scanned nearby networks (with signal bars), plus a manual network-name field (for hidden SSIDs) and a password field.
- Submitting saves
wifi_ssid/wifi_passwordthrough theSettingsManager(intosettings.json— never a code file), shows a confirmation page, and reboots the device, which then connects with the saved credentials (they take precedence oversecrets.py).
Details worth knowing:
run_setup_portal(display=..., port=80, reboot=True, timeout_s=None)returnsTruewhen credentials were saved.rebootapplies on hardware only; on desktop the call simply returns so the flow is testable.- The portal is a boot-phase flow: it owns the screen exclusively before
the app's display loop starts (like
OTAProgressDisplay.install_pending()), and it only ever writes settings — the same discipline as the settings web UI (seedocs/guide/web.md). - Everything is imported lazily (
scrollkit.web.wifi_setup,adafruit_httpserver) — a device that boots with working credentials never pays a byte of RAM for the portal. - The network scan happens before AP mode starts (some radio builds can't scan while running an access point).
- The AP is WPA2 with the default password
password(attributesAP_SSID/AP_PASSWORDonWiFiManager, derived from the radio MAC).
HttpClient¶
scrollkit.network.http_client.HttpClient exposes one API across platforms:
from scrollkit.network.http_client import HttpClient
from scrollkit.exceptions import NetworkError
http = HttpClient()
try:
resp = await http.get("https://api.open-meteo.com/v1/forecast?...")
data = resp.json()
except NetworkError as e:
... # every retry failed; http.last_error holds the raw cause
- CircuitPython →
adafruit_requests(synchronous, behindawait). - Desktop →
urllibfallback when no session is supplied.
It supports retries with backoff and a pluggable mock provider for tests.
get(), get_sync(), and post() raise scrollkit.exceptions.NetworkError
when every retry fails (rather than returning a synthesized 500). The raw
underlying exception is retained on http.last_error for diagnostics —
seconds_since_last_success() and the diagnostics note_fetch_result hook read
it to decide when displayed data has gone stale. A mock provider that returns a
response is passed through unchanged (no raise).
Blocking I/O is real on CircuitPython¶
adafruit_requests is synchronous — a request blocks the event loop until it
returns, pausing the scroll. ScrollKit does not pretend this is transparently
async. Design around it:
- Render a static/loading frame before a known-slow fetch.
- For lots of data, chunk the requests and
await asyncio.sleep(0)between chunks so the display keeps moving. See the hard tutorial for the full pattern.
mDNS: reach the device by name¶
scrollkit.network.mdns.advertise() advertises <hostname>.local plus a service
record, so the config web UI is reachable by name without knowing the IP. It is
CircuitPython-only — a no-op returning None on desktop / when there's no radio —
and never raises, so it can't block boot:
from scrollkit.network import mdns
# Keep the returned server alive for the app's lifetime!
self._mdns = mdns.advertise(self.settings.get("hostname", "scrollkit"))
Retain the server
You must hold a reference to the returned mdns.Server. If it is
garbage-collected the responder stops and .local resolution dies after the
first cached query expires — an intermittent failure that's painful to debug.