Xteink SugarTV turns a dedicated Xteink X3 into a direct Home Assistant glucose display. The device runs a patched CrossPoint build, reads Home Assistant state and history itself, renders a native 528×792 portrait frame, and wakes once per minute. No laptop, display server, or Lovelace card is required at runtime.
The image above contains synthetic data and is generated by the same C++ renderer and bitmap assets compiled into the firmware.
While the SugarTV cycle is active, a short power-button press wakes the X3 and
requests an immediate update instead of waiting for the next timer wake. Holding
the power button exits the cycle and returns to the normal CrossPoint UI. The
screen labels the rightmost physical button as Manage Wi-Fi; pressing it opens
the network list without automatically reconnecting to the current network.
The screen follows the behavior of homeassistant-sugartv-card: source and
trend resolution for common glucose integrations, measurement timestamps,
history-derived delta and cadence, prediction text, threshold states, relative
time, missing data, and cadence-based aging. Color semantics are translated to
the one-bit panel: low and high readings gain an outline, urgent readings invert
the frame, and stale readings use ordered ink density. The upper-right value is
the battery percentage measured by the X3. The upper-left status always shows
the result and local time of the latest update attempt: Updated at Aug 9, 3:55 PM
after success or Update failed at Aug 9, 3:56 PM after failure. The battery is
measured on every update attempt, including failures. If Wi-Fi, Home
Assistant, or the configured glucose sensor is unavailable, the last successful
reading remains visible without changing its value, trend, delta, or displayed
age, while the battery percentage remains live.
This is an informational display, not a medical device or a source for treatment decisions.
The project is released under the MIT License. CrossPoint and icon attribution
are recorded in THIRD_PARTY_NOTICES.md.
- An Xteink X3, a data-capable USB-C cable, and an SD card.
- Git, a C++17 compiler, Python 3.11 or later, and pioarduino Core installed with the current pioarduino Core installer.
- A Home Assistant instance reachable from a Wi-Fi network saved on the X3.
- A
sensor.*entity with Home Assistantdevice_class: blood_glucose_concentration, or an explicit entity ID in the optional settings file. - A Home Assistant long-lived access token. Create one under User profile > Security > Long-lived access tokens as described by the official REST API documentation.
The firmware binary embeds the Home Assistant token. Treat the binary as a credential: do not publish it, attach it to an issue, or upload it as a public CI artifact.
Set the URL without a trailing /, export the token only in the current shell,
and verify the same REST endpoint the device will use:
export HA_URL='http://homeassistant.local:8123'
export HA_TOKEN='replace-with-token'
curl --fail --silent --show-error \
-H "Authorization: Bearer ${HA_TOKEN}" \
"${HA_URL}/api/" >/dev/nullFor repeatable local builds, copy .env.example to the Git-ignored .env,
uncomment and fill HA_URL and HA_TOKEN, then load them into the current
shell with set -a; source .env; set +a. The firmware does not read .env
directly; PlatformIO receives the values through the build flags below.
Home Assistant uses port 8123 by default. An http:// URL sends the bearer
token without transport encryption; use it only on a network whose risk you
accept. HTTPS is the appropriate production choice once server certificate
validation is configured.
The patch is pinned to CrossPoint commit
e00f5958dfeea2a3e640c39eb78186fd20996f4b on the develop branch.
git clone https://github.com/wiltodelta/xteink-sugartv.git
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader.git
cd crosspoint-reader
git checkout e00f5958dfeea2a3e640c39eb78186fd20996f4b
git submodule update --init --recursive
git apply --check ../xteink-sugartv/firmware/patches/crosspoint-sugartv.patch
git apply ../xteink-sugartv/firmware/patches/crosspoint-sugartv.patchDo not apply the patch to another CrossPoint revision: it includes deliberate changes to sleep, Wi-Fi, HTTP, rendering, fonts, and the home menu, all tested together at the pinned commit.
Supply the Home Assistant URL, token, and optional POSIX timezone through build flags. Never add real values to the patch or another tracked file:
export PLATFORMIO_BUILD_FLAGS="-DSUGARTV_HA_URL=\\\"${HA_URL}\\\" -DSUGARTV_HA_TOKEN=\\\"${HA_TOKEN}\\\" -DSUGARTV_TZ=\\\"PST8PDT,M3.2.0,M11.1.0\\\""
pio run -e defaultThe dedicated build defaults to Pacific time
(PST8PDT,M3.2.0,M11.1.0) if SUGARTV_TZ is omitted. Set the build flag
explicitly when installing the firmware in another timezone.
The result is .pio/build/default/firmware.bin. Building does not alter the X3;
flashing is a separate step. Investigate any failed or interrupted build instead
of flashing whatever binary happens to remain from an earlier run.
Read CrossPoint's pinned
installation and USB-unlocker notes
before flashing a locked or stock device. Connect and wake the X3, then find its
serial port. Typical values are /dev/cu.usbmodem1101 on macOS and
/dev/ttyACM0 on Linux.
Keep the build variables from the previous step in the same shell and run:
export X3_PORT='/dev/cu.usbmodem1101'
pio run -e default --target upload --upload-port "${X3_PORT}"PlatformIO rebuilds before uploading, which is why HA_URL, HA_TOKEN, and
PLATFORMIO_BUILD_FLAGS must still be present. Do not use an old binary merely
because it happens to exist under .pio/build/default/.
After a successful upload, remove the secrets from the shell:
unset HA_TOKEN PLATFORMIO_BUILD_FLAGS- Wake the X3 and select SugarTV from the CrossPoint home menu.
- Select a Wi-Fi network. Saved networks are reused silently on later timed wakes; the visible list remains manual so it can be used to change networks.
- Wait for the first successful Home Assistant update. The upper-left status must show local time, the upper-right value must show the current X3 battery, and the next update should occur on the one-minute cadence.
- If automatic sensor discovery does not select the intended entity, copy
firmware/sugartv.example.jsonto/.crosspoint/sugartv.jsonon the SD card and setglucose_valueexplicitly.
| Control | SugarTV behavior |
|---|---|
| Short power-button press | Wake and update immediately. |
| Long power-button hold | Leave the scheduled SugarTV cycle and return to CrossPoint. |
Rightmost button, labelled Manage Wi-Fi |
Open the manual network list without reconnecting automatically. |
Automatic timer wakes keep the existing e-ink frame visible. They do not show the boot logo or Wi-Fi selector before every update.
Without settings, the firmware scans /api/states and selects the first valid
sensor whose Home Assistant device_class is
blood_glucose_concentration. The selected entity is cached, and a failed
entity request causes rediscovery. A temporarily unknown or unavailable
reading does not discard the cached source.
The latest successful reading is cached in
/.crosspoint/sugartv-reading.json. A successful cycle shows its absolute local
update time in the upper-left status. A failed automatic cycle renders the
cached reading exactly as it appeared on the last successful cycle, replaces
that status with Update failed and the failed attempt time on the same line,
then retries on the next one-minute wake. If no successful reading has been
cached, the main value is N/A.
Every automatic cycle appends a diagnostic JSON object to the daily SD-card log
at /.crosspoint/logs/sugartv-YYYY-MM-DD.jsonl. It retains 30 days of cycle,
reading, network, system, and error context without tokens, Wi-Fi passwords, or
response payloads. Schema version 2 also records the requested sleep interval
and whether ESP-IDF accepted the timer wake source. The active cycle is marked
by /.crosspoint/sugartv-cycle-armed, so a reset or USB power event resumes
SugarTV instead of silently returning to the retained Home screen. See
Architecture for the complete logging contract.
On a cold wake, SugarTV first tries the last successful saved Wi-Fi network. If the radio rejects that early attempt, a fresh scan tries visible saved networks and permits one scan-confirmed retry of the original network. The attempt set remains bounded, so an unavailable network cannot trap the device awake. A manually selected open network is saved automatically with an empty password so later headless wakes can reconnect to it.
Each SugarTV update has a shared 20-second HTTP budget across its Home Assistant requests. A Wi-Fi network with a captive portal, a stalled response, or no internet route therefore produces a failed update and returns the device to its normal sleep cadence instead of leaving it awake indefinitely.
The saved-network store is loaded immediately after SD-card initialization, so the device UI and API do not temporarily report an empty list before the first Wi-Fi activity opens it.
An optional /.crosspoint/sugartv.json file overrides discovery and display
defaults. The shared fields use the same names as homeassistant-sugartv-card:
{
"glucose_value": "sensor.jane_glucose_value",
"glucose_trend": "sensor.jane_glucose_trend",
"timestamp_attribute": "measurement_timestamp",
"show_prediction": true,
"relative_time": true,
"dim_by_age": false,
"show_age_states": false,
"color_thresholds": false,
"decimal_comma": false,
"thresholds": {
"urgent_low": 54,
"low": 70,
"high": 180,
"urgent_high": 250
}
}glucose_value, glucose_trend, timestamp_attribute, show_prediction,
relative_time, dim_by_age, color_thresholds, and thresholds have the
card's meanings. Unlike the Lovelace card, glucose_value is optional because
the dedicated device can discover the first blood-glucose sensor itself.
Individual threshold values are optional and are merged with the defaults for
the sensor unit. A completed set that is not strictly ascending falls back to
the unit defaults, as does a complete set containing the other unit's defaults.
show_age_states and decimal_comma are X3-only parameters. The former gates
all age-based dithering while preserving the textual reading age; the latter
uses a comma as the mmol/L decimal separator. The ready-to-copy example is
firmware/sugartv.example.json.
See Troubleshooting for clock, Wi-Fi, sensor, captive-portal, flashing, and diagnostic-log checks.
Run these commands from the patched CrossPoint checkout:
c++ -std=c++17 -Isrc tools/test_sugartv_logic.cpp \
src/activities/network/SugarTvLogic.cpp -o /tmp/test_sugartv_logic
/tmp/test_sugartv_logic
python3 tools/test_sugartv_preview.py
python3 tools/render_sugartv_preview.py \
--font lib/EpdFont/builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
--output /tmp/sugartv-feature-matrix.png --matrixThe C++ test exercises the same timestamp, trend, sibling-entity, cadence,
aging, event-result, cycle-resume, filename, and retention code compiled into
the firmware.
The preview is not a second Python
implementation: it compiles and executes the same C++ frame renderer, bitmap
fonts, layout, and icon assets as the X3. Its tests reject clipped pixels, lock
the default framebuffer with a golden digest, and cover prediction placement,
threshold states, age density, missing history, unknown trend, the single-line
update status, and the complete visual matrix. The final command above writes that
matrix to /tmp for local inspection without adding generated device data to
the repository.
To reproduce the tracked synthetic screenshots from the same patched checkout, run this command from the Xteink SugarTV repository:
uv run python scripts/generate_firmware_screenshots.py ../crosspoint-reader
git diff -- docs/imagesThe generator contains only synthetic values and must remain independent of a real Home Assistant response or device framebuffer.
The sleep lifecycle requires a hardware soak test after flashing. Observe at least 15 consecutive one-minute updates with the X3 disconnected from USB, then repeat with USB connected. Both runs must retain timer wake and power-button wake, advance the displayed measurement, and avoid hanging on entry to deep sleep. A single successful wake is not sufficient validation.
The Python application under src/xteink_sugartv is retained as a local reference
renderer and a compatibility harness for the earlier x3-trmnl prototype. It
is not part of the direct-device runtime. It starts in synthetic-data mode with
the defaults from .env.example:
uv sync
uv run uvicorn xteink_sugartv.app:app --host 0.0.0.0 --port 8000Copy .env.example to .env, customize it, and add --env-file .env to the
command when Home Assistant-backed rendering is needed. Run the complete local
quality gate with ./maintain.sh.

