Skip to content

Commit d12e6c7

Browse files
committed
feat: publish updated portal pages with 8 new bespoke hero apps
1 parent 1466f5e commit d12e6c7

19 files changed

Lines changed: 2117 additions & 55 deletions

File tree

assets/apps/bus_scope.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""
2+
PyDevices Bus Timing & Logic Scope (Hero Canvas App for displayif)
3+
=================================================================
4+
Digital logic analyzer visualizing high-speed SPI / 8080 / MIPI-DSI
5+
hardware bus signals, clock pulses, frame throughput, and TE sync.
6+
"""
7+
8+
import sys
9+
import types
10+
import time
11+
import math
12+
from random import random
13+
14+
try:
15+
import js
16+
from js import document, window
17+
from pyodide.ffi import create_proxy
18+
except ImportError:
19+
js = None
20+
document = None
21+
window = None
22+
create_proxy = lambda fn: fn
23+
24+
from displaydev.psdisplay import PSDisplay
25+
26+
27+
class BusScopeHero:
28+
def __init__(self, canvas_id="hero_canvas", size=240):
29+
self.canvas_id = canvas_id
30+
self.size = size
31+
self.w = size
32+
self.h = size
33+
34+
if "board_config" not in sys.modules:
35+
bc = types.ModuleType("board_config")
36+
bc.display_drv = PSDisplay(canvas_id, width=size, height=size)
37+
sys.modules["board_config"] = bc
38+
self.drv = bc.display_drv
39+
else:
40+
self.drv = sys.modules["board_config"].display_drv
41+
42+
self.offset = 0.0
43+
self.fps = 60.0
44+
self.mbps = 80.0
45+
self.frame_cnt = 0
46+
47+
self.draw()
48+
self._tick_proxy = create_proxy(self._js_tick_cb) if window else None
49+
self._tick_interval = window.setInterval(self._tick_proxy, 30) if window else None
50+
51+
def _js_tick_cb(self):
52+
self.tick()
53+
54+
def tick(self):
55+
self.offset += 2.5
56+
self.frame_cnt += 1
57+
self.draw()
58+
59+
def draw(self):
60+
if not hasattr(self.drv, "_buf_ctx") or not self.drv._buf_ctx:
61+
return
62+
ctx = self.drv._buf_ctx
63+
w, h = self.w, self.h
64+
65+
# 1. Oscilloscope Dark Grid Background
66+
ctx.fillStyle = "#070A0E"
67+
ctx.fillRect(0, 0, w, h)
68+
69+
# Oscilloscope Grid Lines
70+
ctx.strokeStyle = "rgba(30, 41, 59, 0.6)"
71+
ctx.lineWidth = 1
72+
for gx in range(20, w, 20):
73+
ctx.beginPath()
74+
ctx.moveTo(gx, 0)
75+
ctx.lineTo(gx, h)
76+
ctx.stroke()
77+
for gy in range(20, h, 20):
78+
ctx.beginPath()
79+
ctx.moveTo(0, gy)
80+
ctx.lineTo(w, gy)
81+
ctx.stroke()
82+
83+
# 2. Header Bar
84+
ctx.fillStyle = "rgba(15, 23, 42, 0.9)"
85+
ctx.fillRect(0, 0, w, 28)
86+
ctx.strokeStyle = "#1E293B"
87+
ctx.lineWidth = 1
88+
ctx.beginPath()
89+
ctx.moveTo(0, 28)
90+
ctx.lineTo(w, 28)
91+
ctx.stroke()
92+
93+
ctx.fillStyle = "#F54E00"
94+
ctx.font = "bold 9px system-ui, monospace"
95+
ctx.textAlign = "left"
96+
ctx.fillText("● BUS TIMING", 10, 18)
97+
98+
ctx.fillStyle = "#38BDF8"
99+
ctx.font = "9px system-ui, monospace"
100+
ctx.textAlign = "right"
101+
ctx.fillText(f"{self.mbps:.0f}M | 60FPS", w - 10, 18)
102+
103+
# 3. Waveform Channels (CS, SCK, MOSI, TE/VSYNC)
104+
channels = [
105+
("CS", "#EF4444", 45, 14), # Red
106+
("SCK", "#38BDF8", 85, 14), # Blue
107+
("MOSI", "#10B981", 125, 14), # Emerald
108+
("TE", "#F59E0B", 165, 14), # Amber
109+
]
110+
111+
t = self.offset
112+
113+
for name, color, base_y, amp in channels:
114+
# Label
115+
ctx.fillStyle = color
116+
ctx.font = "bold 9px system-ui, monospace"
117+
ctx.textAlign = "left"
118+
ctx.fillText(name, 10, base_y + amp / 2 + 3)
119+
120+
# Trace Signal
121+
ctx.beginPath()
122+
ctx.strokeStyle = color
123+
ctx.lineWidth = 1.5
124+
125+
plot_x_start = 42
126+
for x in range(plot_x_start, w - 8):
127+
ph = (x + t)
128+
if name == "CS":
129+
# Periodic active-low chip select burst
130+
val = 0 if (ph % 160 < 120) else 1
131+
elif name == "SCK":
132+
# High speed clock pulse
133+
val = 1 if (int(ph / 4) % 2 == 0) else 0
134+
elif name == "MOSI":
135+
# Data packet burst
136+
val = 1 if (int(ph / 8 + math.sin(ph * 0.05) * 2) % 2 == 0) else 0
137+
else: # TE / VSYNC
138+
# VSync pulse once every 200px
139+
val = 1 if (ph % 220 < 18) else 0
140+
141+
y = base_y + (0 if val == 1 else amp)
142+
if x == plot_x_start:
143+
ctx.moveTo(x, y)
144+
else:
145+
ctx.lineTo(x, y)
146+
147+
ctx.stroke()
148+
149+
# 4. Trigger Cursor
150+
trig_x = 130
151+
ctx.beginPath()
152+
ctx.setLineDash([3, 3])
153+
ctx.moveTo(trig_x, 28)
154+
ctx.lineTo(trig_x, 195)
155+
ctx.strokeStyle = "rgba(255, 255, 255, 0.4)"
156+
ctx.stroke()
157+
ctx.setLineDash([])
158+
159+
# 5. Bottom Status Strip
160+
ctx.fillStyle = "rgba(15, 23, 42, 0.9)"
161+
ctx.fillRect(0, 198, w, 42)
162+
ctx.strokeStyle = "#1E293B"
163+
ctx.lineWidth = 1
164+
ctx.beginPath()
165+
ctx.moveTo(0, 198)
166+
ctx.lineTo(w, 198)
167+
ctx.stroke()
168+
169+
ctx.fillStyle = "#94A3B8"
170+
ctx.font = "9px system-ui, monospace"
171+
ctx.textAlign = "left"
172+
ctx.fillText("BUS: QSPI / 8080 (DMA ACTIVE)", 10, 214)
173+
ctx.fillText(f"FRAME_TX: #{self.frame_cnt:05d} DROPPED: 0", 10, 228)
174+
175+
if hasattr(self.drv, "show"):
176+
self.drv.show()
177+
178+
179+
_bus_scope_app = None
180+
181+
182+
def main(canvas_id="hero_canvas"):
183+
global _bus_scope_app
184+
print(f"Initializing PyDevices Bus Scope on canvas '{canvas_id}'...")
185+
_bus_scope_app = BusScopeHero(canvas_id, size=240)
186+
print("PyDevices Bus Scope running successfully!")

0 commit comments

Comments
 (0)