Skip to content
This repository was archived by the owner on Oct 28, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .site/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ <h3>Widgets Clinic Queue</h3>
</span>
</div>
<h3>Car Cluster</h3>
<p>Flagship LVGL automobile instrument cluster.</p>
<p>An electric car's instrument cluster, 800x480, in LVGL.</p>
</a>
<a class="card" href="micropython.html?manifests=alien">
<div class="card-top">
Expand Down
21 changes: 2 additions & 19 deletions .site/gallery/python-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,8 @@
"calc_widgets.py",
"car_cluster/__init__.py",
"car_cluster/car_cluster.py",
"car_cluster/chrome.py",
"car_cluster/focus_nav.py",
"car_cluster/gauges.py",
"car_cluster/input_map.py",
"car_cluster/key_probe.py",
"car_cluster/layout.py",
"car_cluster/lv_util.py",
"car_cluster/rails.py",
"car_cluster/screens/__init__.py",
"car_cluster/screens/_common.py",
"car_cluster/screens/assist.py",
"car_cluster/screens/engine.py",
"car_cluster/screens/lights.py",
"car_cluster/screens/speed.py",
"car_cluster/screens/theme_screen.py",
"car_cluster/screens/tires.py",
"car_cluster/screens/trip_a.py",
"car_cluster/screens/trip_b.py",
"car_cluster/theme.py",
"car_cluster/cluster.py",
"car_cluster/ring.py",
"car_cluster/vehicle.py",
"chango/__init__.py",
"chango/chango.py",
Expand Down
2 changes: 1 addition & 1 deletion .site/pyscript/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ <h3>Widgets Clinic Queue</h3>
</span>
</div>
<h3>Car Cluster</h3>
<p>Flagship LVGL automobile instrument cluster.</p>
<p>An electric car's instrument cluster, 800x480, in LVGL.</p>
</a>
<a class="card" href="../pyscript/pyodide.html?manifests=alien&deps=pydevices-palettes%2Cpydevices-pygraphics">
<div class="card-top">
Expand Down
Binary file modified .site/pyscript/thumbnails/car_cluster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| ![tiny_toasters.gif](tiny_toasters.gif) | ![piano.png](piano.png) | ![google_photos.png](google_photos.png) |
| ![google_photos_view.png](google_photos_view.png) | ![google_photos_connect.png](google_photos_connect.png) | ![spectrum_800x480.gif](spectrum_800x480.gif) |
| ![spectrum_800x480.png](spectrum_800x480.png) | ![spectrum_800x480_segmented.png](spectrum_800x480_segmented.png) | ![spectrum_320x170.png](spectrum_320x170.png) |
| ![car_cluster.png](car_cluster.png) | | |

On Android (the PyDevices Runner) and as an installed PWA:

Expand Down
Binary file added docs/screenshots/car_cluster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 74 additions & 55 deletions lib/examples/car_cluster/car_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
"""
car_cluster
====================================================
Flagship LVGL automobile instrument cluster.
An electric car's instrument cluster, 800x480, in LVGL.

Landscape 1024×512 cluster with Material accents, nested chrome, four side
gauges, dual steering-wheel style menu rails, and eight live center screens.
Two neon dials, speed in mph on the left and motor power in kW on the right,
with the odometer and the miles per kWh under them; the clock, the date and
the outside temperature along the top; the turn indicators; a lit lane
running to the horizon with the car's marker at its foot; and P R N D S
along the bottom.

Controls:
Left/Right — switch focus group (left rail / center / right rail)
Up/Down — previous/next control within the active group
Enter — activate focused control
The car drives itself round a 90 second loop (see ``vehicle.py``). Keys:

Vehicle speed, gear, RPM, and oil pressure follow an automatic drive cycle
(speed ≈ 10× gear). Fuel and coolant stay fixed.
Left / Right hold that turn indicator on; press again to cancel
Enter pause and resume the drive

The dials are art drawn once on a canvas (a colour sweep made of many short
arc segments, a tick ring, a hairline) with three cheap moving parts over it
(see ``ring.py``). Everything is sized from the screen, so it also fits a
smaller landscape display, though it is drawn for 800x480.
"""

import sys
Expand All @@ -24,7 +29,7 @@
_file = __file__.replace("\\", "/")
_PKG = _file.rsplit("/", 1)[0]
_parts = _file.split("/")
# src/ is the parent of examples/ (package lives in examples/car_cluster/).
# lib/ is the parent of examples/ (the package lives in examples/car_cluster/).
if "examples" in _parts:
_idx = _parts.index("examples")
_src = "/".join(_parts[:_idx]) if _idx else "."
Expand All @@ -37,31 +42,25 @@
if _PKG not in sys.path:
sys.path.insert(0, _PKG)

import input_map # noqa: E402 — capture hook must run before display_driver

input_map.capture_virtual_devices()
from boarddev import env_bool, env_get, env_set # noqa: E402

from boarddev import env_bool, env_get, env_set

env_set("PYDEVICES_WIDTH", "1024")
env_set("PYDEVICES_HEIGHT", "512")
env_set("PYDEVICES_WIDTH", "800")
env_set("PYDEVICES_HEIGHT", "480")
if env_get("PYDEVICES_SCALE") is None:
env_set("PYDEVICES_SCALE", "1")

import display_driver # noqa: E402 — wires LVGL into the shared app
import lvgl as lv # noqa: E402
from board_config import display_drv # noqa: E402
from display_driver import app # noqa: E402
from display_driver import app # noqa: E402, F401

import focus_nav # noqa: E402
import input_map # noqa: E402
import layout # noqa: E402
from cluster import Cluster # noqa: E402
from vehicle import Vehicle # noqa: E402

_ui = None
_vehicle = None
_last_ms = None
_timer = None
_keys = None


def _now_ms():
Expand All @@ -75,50 +74,82 @@ def _dt_s(now):
global _last_ms
if _last_ms is None:
_last_ms = now
return 0.05
return 0.04
try:
dt = time.ticks_diff(now, _last_ms) / 1000.0
except AttributeError:
dt = (now - _last_ms) / 1000.0
_last_ms = now
if dt < 0:
dt = 0.05
dt = 0.04
if dt > 0.25:
dt = 0.25
return dt


# Design / interactive freeze: set CAR_CLUSTER_FREEZE=1 (or truthy) to hold
# gauges/screens at static values so layout can be tweaked from the REPL.
def _clock_text():
try:
t = time.localtime()
hour = t[3] % 12 or 12
return "%d:%02d %s" % (hour, t[4], "PM" if t[3] >= 12 else "AM")
except Exception:
return "9:59 PM"


# Set CAR_CLUSTER_FREEZE=1 to hold the dials still for layout work at the REPL.
_FREEZE = env_bool("CAR_CLUSTER_FREEZE", False)


def _on_tick(timer):
global _ui, _vehicle
if _ui is None or _vehicle is None:
return
if _FREEZE:
if _ui is None or _vehicle is None or _FREEZE:
return
dt = _dt_s(_now_ms())
_vehicle.tick(dt)
_vehicle.tick(_dt_s(_now_ms()))
_ui.set_clock(_clock_text())
_ui.update()


def freeze(static=True):
"""Hold or resume vehicle/gauge animation (interactive design helper)."""
"""Hold or resume the drive (a design helper for the REPL)."""
global _FREEZE
_FREEZE = bool(static)
if _FREEZE and _ui is not None and _vehicle is not None:
# Snap once to a readable static scene.
_vehicle.rpm = 3200
_vehicle.speed_mph = 48
_vehicle.fuel_frac = 0.62
_vehicle.coolant_f = 195
_vehicle.oil_psi = 42
_vehicle.speed_mph = 51
_vehicle.power_kw = 138
_vehicle.gear = "N"
_ui.update()
return _FREEZE


def _on_key(e):
key = e.get_key()
if key == lv.KEY.LEFT:
_vehicle.toggle_signal("left")
elif key == lv.KEY.RIGHT:
_vehicle.toggle_signal("right")
elif key == lv.KEY.ENTER:
_vehicle.running = not _vehicle.running


def _listen_for_keys(scr):
"""A focused, invisible object in the default group receives the keypad."""
global _keys
group = lv.group_get_default()
if group is None:
group = lv.group_create()
group.set_default()
_keys = lv.obj(scr)
_keys.set_size(1, 1)
_keys.set_pos(0, 0)
_keys.set_style_bg_opa(lv.OPA.TRANSP, 0)
_keys.set_style_border_width(0, 0)
_keys.set_style_outline_width(0, lv.STATE.FOCUSED)
_keys.set_style_outline_width(0, lv.STATE.FOCUS_KEY)
_keys.remove_flag(lv.obj.FLAG.SCROLLABLE)
group.add_obj(_keys)
lv.group_focus_obj(_keys)
_keys.add_event_cb(_on_key, lv.EVENT.KEY, None)


def build_ui():
global _ui, _vehicle, _timer, _last_ms

Expand All @@ -127,24 +158,12 @@ def build_ui():
inst.disable()
try:
_vehicle = Vehicle()
nav = focus_nav.FocusNav()

bridge = input_map.InputBridge(app, _vehicle, focus_nav=nav)
bridge.install()

_ui = layout.ClusterUI(_vehicle, focus_nav=nav)
# Groups are populated by layout; start focus on the left rail.
nav.set_active(focus_nav.FocusNav.LEFT)
scr = lv.screen_active()
_ui = Cluster(_vehicle, scr)
_ui.set_clock(_clock_text())
_listen_for_keys(scr)
_last_ms = _now_ms()
_timer = lv.timer_create(_on_tick, 40, None)

# Apply deferred rail→tabview changes outside LVGL task_handler.
def _drain_rails(_t):
if _ui is not None and _ui.rails is not None:
_ui.rails.drain_pending()

app.on_tick(_drain_rails, period=30)
_ui.rails.drain_pending()
if _FREEZE:
freeze(True)
finally:
Expand Down
Loading
Loading