Skip to content

Commit c0ff7fa

Browse files
Piggy: add capture QR code
Also cleanup top layer when opening a new screen.
1 parent faed111 commit c0ff7fa

File tree

2 files changed

+287
-0
lines changed
  • internal_filesystem

2 files changed

+287
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# This code grabs images from the camera in RGB565 format (2 bytes per pixel)
2+
# and sends that to the QR decoder if QR decoding is enabled.
3+
# The QR decoder then converts the RGB565 to grayscale, as that's what quirc operates on.
4+
# It would be slightly more efficient to capture the images from the camera in L8/grayscale format,
5+
# or in YUV format and discarding the U and V planes, but then the image will be gray (not great UX)
6+
# and the performance impact of converting RGB565 to grayscale is probably minimal anyway.
7+
8+
import lvgl as lv
9+
import time
10+
11+
import mpos.apps
12+
import mpos.clipboard
13+
import mpos.ui
14+
15+
# screens:
16+
main_screen = None
17+
18+
keepliveqrdecoding = False
19+
width = 240
20+
height = 240
21+
22+
# Variable to hold the current memoryview to prevent garbage collection
23+
current_cam_buffer = None
24+
image_dsc = None
25+
image = None
26+
qr_label = None
27+
28+
use_webcam = False
29+
qr_button = None
30+
snap_button = None
31+
32+
status_label = None
33+
status_label_cont = None
34+
status_label_text = "No camera found."
35+
status_label_text_searching = "Searching QR codes..."
36+
status_label_text_found = "Decoding QR..."
37+
38+
def print_qr_buffer(buffer):
39+
try:
40+
# Try to decode buffer as a UTF-8 string
41+
result = buffer.decode('utf-8')
42+
# Check if the string is printable (ASCII printable characters)
43+
if all(32 <= ord(c) <= 126 for c in result):
44+
return result
45+
except Exception as e:
46+
pass
47+
# If not a valid string or not printable, convert to hex
48+
hex_str = ' '.join([f'{b:02x}' for b in buffer])
49+
return hex_str.lower()
50+
51+
# Byte-Order-Mark is added sometimes
52+
def remove_bom(buffer):
53+
bom = b'\xEF\xBB\xBF'
54+
if buffer.startswith(bom):
55+
return buffer[3:]
56+
return buffer
57+
58+
def qrdecode_one():
59+
global status_label, status_label_text
60+
try:
61+
import qrdecode
62+
result = qrdecode.qrdecode_rgb565(current_cam_buffer, width, height)
63+
result = bytearray("INSERT_QR_HERE", "utf-8")
64+
if not result:
65+
status_label.set_text(status_label_text_searching)
66+
else:
67+
result = remove_bom(result)
68+
result = print_qr_buffer(result)
69+
print(f"QR decoding found: {result}")
70+
status_label.set_text(result)
71+
stop_qr_decoding()
72+
mpos.clipboard.add(f"Result: {result}")
73+
mpos.ui.back_screen()
74+
except ValueError as e:
75+
print("QR ValueError: ", e)
76+
status_label.set_text(status_label_text_searching)
77+
except TypeError as e:
78+
print("QR TypeError: ", e)
79+
status_label.set_text(status_label_text_found)
80+
except Exception as e:
81+
print("QR got other error: ", e)
82+
83+
84+
def close_button_click(e):
85+
print("Close button clicked")
86+
mpos.ui.back_screen()
87+
88+
89+
def snap_button_click(e):
90+
print("Picture taken!")
91+
import os
92+
try:
93+
os.mkdir("data")
94+
except OSError:
95+
pass
96+
try:
97+
os.mkdir("data/com.example.camtest")
98+
except OSError:
99+
pass
100+
if current_cam_buffer is not None:
101+
filename="data/com.example.camtest/capture.raw"
102+
try:
103+
with open(filename, 'wb') as f:
104+
f.write(current_cam_buffer)
105+
print(f"Successfully wrote current_cam_buffer to {filename}")
106+
except OSError as e:
107+
print(f"Error writing to file: {e}")
108+
109+
110+
def start_qr_decoding():
111+
global qr_label, keepliveqrdecoding, status_label_cont, status_label
112+
print("Activating live QR decoding...")
113+
keepliveqrdecoding = True
114+
qr_label.set_text(lv.SYMBOL.EYE_CLOSE)
115+
status_label_cont.remove_flag(lv.obj.FLAG.HIDDEN)
116+
status_label.set_text(status_label_text_searching)
117+
118+
def stop_qr_decoding():
119+
global qr_label, keepliveqrdecoding
120+
print("Deactivating live QR decoding...")
121+
keepliveqrdecoding = False
122+
qr_label.set_text(lv.SYMBOL.EYE_OPEN)
123+
status_label_text = status_label.get_text()
124+
if status_label_text == status_label_text_searching or status_label_text == status_label_text_found: # if it found a QR code, then leave it
125+
status_label_cont.add_flag(lv.obj.FLAG.HIDDEN)
126+
127+
128+
def qr_button_click(e):
129+
global keepliveqrdecoding
130+
if not keepliveqrdecoding:
131+
start_qr_decoding()
132+
else:
133+
stop_qr_decoding()
134+
135+
def try_capture(event):
136+
#print("capturing camera frame")
137+
global current_cam_buffer, image_dsc, image, use_webcam
138+
try:
139+
if use_webcam:
140+
current_cam_buffer = webcam.capture_frame(cam, "rgb565")
141+
elif cam.frame_available():
142+
current_cam_buffer = cam.capture()
143+
if current_cam_buffer and len(current_cam_buffer):
144+
image_dsc.data = current_cam_buffer
145+
#image.invalidate() # does not work so do this:
146+
image.set_src(image_dsc)
147+
if not use_webcam:
148+
cam.free_buffer() # Free the old buffer
149+
if keepliveqrdecoding:
150+
qrdecode_one()
151+
except Exception as e:
152+
print(f"Camera capture exception: {e}")
153+
154+
155+
def build_ui():
156+
global image, image_dsc,qr_label, status_label, cam, use_webcam, qr_button, snap_button, status_label_cont, main_screen
157+
main_screen = lv.obj()
158+
main_screen.set_style_pad_all(0, 0)
159+
main_screen.set_style_border_width(0, 0)
160+
main_screen.set_size(lv.pct(100), lv.pct(100))
161+
main_screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
162+
close_button = lv.button(main_screen)
163+
close_button.set_size(60,60)
164+
close_button.align(lv.ALIGN.TOP_RIGHT, 0, 0)
165+
close_label = lv.label(close_button)
166+
close_label.set_text(lv.SYMBOL.CLOSE)
167+
close_label.center()
168+
close_button.add_event_cb(close_button_click,lv.EVENT.CLICKED,None)
169+
snap_button = lv.button(main_screen)
170+
snap_button.set_size(60, 60)
171+
snap_button.align(lv.ALIGN.RIGHT_MID, 0, 0)
172+
snap_button.add_flag(lv.obj.FLAG.HIDDEN)
173+
snap_label = lv.label(snap_button)
174+
snap_label.set_text(lv.SYMBOL.OK)
175+
snap_label.center()
176+
snap_button.add_event_cb(snap_button_click,lv.EVENT.CLICKED,None)
177+
qr_button = lv.button(main_screen)
178+
qr_button.set_size(60, 60)
179+
qr_button.add_flag(lv.obj.FLAG.HIDDEN)
180+
qr_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0)
181+
qr_label = lv.label(qr_button)
182+
qr_label.set_text(lv.SYMBOL.EYE_OPEN)
183+
qr_label.center()
184+
qr_button.add_event_cb(qr_button_click,lv.EVENT.CLICKED,None)
185+
# Initialize LVGL image widget
186+
image = lv.image(main_screen)
187+
image.align(lv.ALIGN.LEFT_MID, 0, 0)
188+
# Create image descriptor once
189+
image_dsc = lv.image_dsc_t({
190+
"header": {
191+
"magic": lv.IMAGE_HEADER_MAGIC,
192+
"w": width,
193+
"h": height,
194+
"stride": width * 2,
195+
"cf": lv.COLOR_FORMAT.RGB565
196+
#"cf": lv.COLOR_FORMAT.L8
197+
},
198+
'data_size': width * height * 2,
199+
'data': None # Will be updated per frame
200+
})
201+
image.set_src(image_dsc)
202+
status_label_cont = lv.obj(main_screen)
203+
status_label_cont.set_size(lv.pct(66),lv.pct(60))
204+
status_label_cont.align(lv.ALIGN.LEFT_MID, lv.pct(5), 0)
205+
status_label_cont.set_style_bg_color(lv.color_white(), 0)
206+
status_label_cont.set_style_bg_opa(66, 0)
207+
status_label_cont.set_style_border_width(0, 0)
208+
status_label = lv.label(status_label_cont)
209+
status_label.set_text("No camera found.")
210+
status_label.set_long_mode(lv.label.LONG.WRAP)
211+
status_label.set_style_text_color(lv.color_white(), 0)
212+
status_label.set_width(lv.pct(100))
213+
status_label.center()
214+
mpos.ui.load_screen(main_screen)
215+
216+
217+
def init_cam():
218+
try:
219+
from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling
220+
cam = Camera(
221+
data_pins=[12,13,15,11,14,10,7,2],
222+
vsync_pin=6,
223+
href_pin=4,
224+
sda_pin=21,
225+
scl_pin=16,
226+
pclk_pin=9,
227+
xclk_pin=8,
228+
xclk_freq=20000000,
229+
powerdown_pin=-1,
230+
reset_pin=-1,
231+
pixel_format=PixelFormat.RGB565,
232+
#pixel_format=PixelFormat.GRAYSCALE,
233+
frame_size=FrameSize.R240X240,
234+
grab_mode=GrabMode.LATEST
235+
)
236+
#cam.init() automatically done when creating the Camera()
237+
#cam.reconfigure(frame_size=FrameSize.HVGA)
238+
#frame_size=FrameSize.HVGA, # 480x320
239+
#frame_size=FrameSize.QVGA, # 320x240
240+
#frame_size=FrameSize.QQVGA # 160x120
241+
cam.set_vflip(True)
242+
return cam
243+
except Exception as e:
244+
print(f"init_cam exception: {e}")
245+
return None
246+
247+
248+
249+
def check_running(timer):
250+
if lv.screen_active() != main_screen:
251+
print("camtest.py backgrounded, cleaning up...")
252+
check_running_timer.delete()
253+
if capture_timer:
254+
capture_timer.delete()
255+
if use_webcam:
256+
webcam.deinit(cam)
257+
elif cam:
258+
cam.deinit()
259+
print("camtest.py cleanup done.")
260+
261+
262+
263+
build_ui()
264+
265+
cam = init_cam()
266+
if cam:
267+
image.set_rotation(900) # internal camera is rotated 90 degrees
268+
else:
269+
print("camtest.py: no internal camera found, trying webcam on /dev/video0")
270+
try:
271+
import webcam
272+
cam = webcam.init("/dev/video0")
273+
use_webcam = True
274+
except Exception as e:
275+
print(f"camtest.py: webcam exception: {e}")
276+
277+
if cam:
278+
print("Camera initialized, continuing...")
279+
check_running_timer = lv.timer_create(check_running, 500, None)
280+
qr_button.remove_flag(lv.obj.FLAG.HIDDEN)
281+
#snap_button.remove_flag(lv.obj.FLAG.HIDDEN)
282+
status_label_cont.add_flag(lv.obj.FLAG.HIDDEN)
283+
capture_timer = lv.timer_create(try_capture, 100, None)
284+
start_qr_decoding()
285+
else:
286+
print("No camera found, stopping camtest.py")

internal_filesystem/lib/mpos/ui.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ def load_screen(screen):
480480
screen_stack.append(screen)
481481
else:
482482
print("Warning: not adding new screen to screen_stack because it's already there, just bringing to foreground.")
483+
close_top_layer_msgboxes() # otherwise they remain
483484
lv.screen_load(screen)
484485

485486
def back_screen():

0 commit comments

Comments
 (0)