Skip to content

Commit 4c45820

Browse files
Fix BAGL applications calling NBGL libraries case
1 parent 1c5f045 commit 4c45820

10 files changed

+91
-90
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.17.0] 2025-03-13
8+
9+
## [0.17.0] 2025-03-14
910

1011
### Added
1112

1213
- `--save-nvram` and `--load-nvram` parameters that allow NVRAM testing
1314
- destination boundaries verification for `nvm_write()`
15+
- Make BAGL-based applications able to call NBGL-based libraries, or NBGL-based applications able to call BAGL-based libraries
1416

1517
## [0.16.0] 2025-03-05
1618

speculos/main.py

+22-20
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def get_cx_infos(app_path):
148148
return ei
149149

150150

151-
def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace, use_bagl: bool) -> int:
151+
def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) -> int:
152152
argv = ['qemu-arm-static']
153153

154154
if args.debug:
@@ -183,20 +183,20 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace, use
183183
else:
184184
logger.warning(f"Cx lib {cxlib_filepath} not found")
185185

186-
# for NBGL apps, fonts binary file is mandatory
187-
if not use_bagl:
188-
fonts_filepath = f"fonts/{args.model}-fonts-{args.apiLevel}.bin"
189-
fonts = str(resources.files(__package__) / fonts_filepath)
190-
if os.path.exists(fonts):
191-
argv += ['-f', fonts]
192-
else:
193-
logger.error(f"Fonts {fonts_filepath} not found")
194-
sys.exit(1)
186+
only_bagl = True
187+
# 'bagl' is the default value of the binary.sections.sdk_graphics. We need to
188+
# manage the cases where it is NOT 'bagl' but the section does not exists yet
189+
if args.model in ["stax", "flex"]:
190+
only_bagl = False
195191

196192
extra_ram = ''
197193
app_path = getattr(args, 'app.elf')
198194
for lib in [f'main:{app_path}'] + args.library:
199195
name, lib_path = lib.split(':')
196+
binary = LedgerBinaryApp(lib_path)
197+
use_bagl = binary.sections.sdk_graphics == "bagl"
198+
if not use_bagl:
199+
only_bagl = False
200200
ei = get_elf_infos(lib_path, use_bagl)
201201

202202
# Since binaries loaded as libs could also declare extra RAM page(s), collect them all
@@ -215,6 +215,16 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace, use
215215
lib_arg += f':{1 if args.save_nvram else 0}'
216216
argv.append(lib_arg)
217217

218+
# for NBGL apps, fonts binary file is mandatory
219+
if not only_bagl:
220+
fonts_filepath = f"fonts/{args.model}-fonts-{args.apiLevel}.bin"
221+
fonts = str(resources.files(__package__) / fonts_filepath)
222+
if os.path.exists(fonts):
223+
argv += ['-f', fonts]
224+
else:
225+
logger.error(f"Fonts {fonts_filepath} not found")
226+
sys.exit(1)
227+
218228
if args.model == 'blue':
219229
if args.rampage:
220230
extra_ram = args.rampage
@@ -357,13 +367,6 @@ def main(prog=None) -> int:
357367
args.model = "nanosp" if binary.sections.target == "nanos2" else binary.sections.target
358368
logger.warning(f"Device model detected from metadata: {args.model}")
359369

360-
# 'bagl' is the default value of the binary.sections.sdk_graphics. We need to
361-
# manage the cases where it is NOT 'bagl' but the section does not exists yet
362-
if args.model in ["stax", "flex"]:
363-
use_bagl = False
364-
else:
365-
use_bagl = binary.sections.sdk_graphics == "bagl"
366-
367370
if not args.apiLevel:
368371
if binary.sections.api_level is not None:
369372
args.apiLevel = binary.sections.api_level
@@ -508,7 +511,7 @@ def main(prog=None) -> int:
508511

509512
s1, s2 = socket.socketpair()
510513

511-
qemu_pid = run_qemu(s1, s2, args, use_bagl)
514+
qemu_pid = run_qemu(s1, s2, args)
512515
s1.close()
513516

514517
# The `--transport` argument takes precedence over `--usb`
@@ -521,7 +524,6 @@ def main(prog=None) -> int:
521524
seph = seproxyhal.SeProxyHal(
522525
s2,
523526
args.model,
524-
use_bagl,
525527
automation_path,
526528
automation_server,
527529
transport_type,
@@ -563,7 +565,7 @@ def main(prog=None) -> int:
563565
apirun = ApiRunner(args.api_port)
564566

565567
display_args = DisplayArgs(args.color, args.model, args.ontop, rendering,
566-
args.keymap, zoom, x, y, use_bagl)
568+
args.keymap, zoom, x, y)
567569
server_args = ServerArgs(apdu, apirun, button, finger, seph, vnc)
568570
screen_notifier = ScreenNotifier(display_args, server_args)
569571

speculos/mcu/display.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,13 @@ def rendering(self):
259259
return self._display_args.rendering
260260

261261
@property
262-
def use_bagl(self) -> bool:
263-
return self._display_args.use_bagl
262+
@abstractmethod
263+
def nbgl_gl(self) -> GraphicLibrary:
264+
pass
264265

265266
@property
266267
@abstractmethod
267-
def gl(self) -> GraphicLibrary:
268+
def bagl_gl(self) -> GraphicLibrary:
268269
pass
269270

270271
@abstractmethod

speculos/mcu/headless.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,32 @@ def __init__(self, display: DisplayArgs, server: ServerArgs) -> None:
1515
super().__init__(display, server)
1616

1717
self.m = HeadlessPaintWidget(self.model, server.vnc)
18-
self._gl: GraphicLibrary
19-
if display.use_bagl:
20-
self._gl = bagl.Bagl(self.m, MODELS[self.model].screen_size, self.model)
21-
else:
22-
self._gl = nbgl.NBGL(self.m, MODELS[self.model].screen_size, self.model)
18+
self._bagl_gl: GraphicLibrary
19+
self._nbgl_gl: GraphicLibrary
20+
self._bagl_gl = bagl.Bagl(self.m, MODELS[self.model].screen_size, self.model)
21+
self._nbgl_gl = nbgl.NBGL(self.m, MODELS[self.model].screen_size, self.model)
2322

2423
@property
25-
def gl(self) -> GraphicLibrary:
26-
return self._gl
24+
def bagl_gl(self):
25+
return self._bagl_gl
26+
27+
@property
28+
def nbgl_gl(self):
29+
return self._nbgl_gl
2730

2831
def display_status(self, data: bytes) -> List[TextEvent]:
29-
assert isinstance(self.gl, bagl.Bagl)
30-
ret = self.gl.display_status(data)
32+
ret = self.bagl_gl.display_status(data)
3133
if MODELS[self.model].name == 'blue':
3234
self.screen_update() # Actually, this method doesn't work
3335
return ret
3436

3537
def display_raw_status(self, data: bytes) -> None:
36-
assert isinstance(self.gl, bagl.Bagl)
37-
self.gl.display_raw_status(data)
38+
self.bagl_gl.display_raw_status(data)
3839
if MODELS[self.model].name == 'blue':
3940
self.screen_update() # Actually, this method doesn't work
4041

4142
def screen_update(self) -> bool:
42-
assert isinstance(self.gl, bagl.Bagl)
43-
return self.gl.refresh()
43+
return self.bagl_gl.refresh()
4444

4545

4646
class HeadlessPaintWidget(FrameBuffer):

speculos/mcu/ocr.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,10 @@ class OCR:
122122
MAX_BLANK_SPACE_STAX = 24
123123
MAX_BLANK_SPACE_FLEX = 26
124124

125-
def __init__(self, model: str, use_bagl: bool):
125+
def __init__(self, model: str):
126126
self.events: List[TextEvent] = []
127127
# Store the model of the device
128128
self.model = model
129-
self.use_bagl = use_bagl
130129
# Maximum space for a letter to be considered part of the same word
131130
if model == "stax":
132131
self.max_blank_space = OCR.MAX_BLANK_SPACE_STAX
@@ -212,14 +211,14 @@ def add_character(self, x: int, y: int, w: int, h: int, char: str) -> None:
212211
# create a new TextEvent if there are no events yet or if there is a new line
213212
self.events.append(TextEvent(char, x, y, w, h, False))
214213

215-
def analyze_bitmap(self, data: bytes) -> None:
214+
def analyze_bitmap(self, data: bytes, use_bagl: bool) -> None:
216215
"""
217216
data contain information about the latest displayed bitmap.
218217
Since unified SDK, speculos added the displayed character to data.
219218
For older SKD versions, legacy behaviour is used: parsing internal
220219
fonts to find a matching bitmap.
221220
"""
222-
if not self.use_bagl:
221+
if not use_bagl:
223222
# Can be called via SephTag.NBGL_DRAW_IMAGE or SephTag.NBGL_DRAW_IMAGE_RLE
224223
# In both cases, data contains:
225224
# - area (sizeof(nbgl_area_t))

speculos/mcu/screen.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -191,24 +191,27 @@ class Screen(Display):
191191
def __init__(self, display: DisplayArgs, server: ServerArgs) -> None:
192192
super().__init__(display, server)
193193
self.app: App
194-
self._gl: GraphicLibrary
194+
self._bagl_gl: GraphicLibrary
195+
self._nbgl_gl: GraphicLibrary
195196

196197
def set_app(self, app: App) -> None:
197198
self.app = app
198199
self.app.set_screen(self)
199200
model = self._display_args.model
200-
if self.use_bagl:
201-
self._gl = bagl.Bagl(app.widget, MODELS[model].screen_size, model)
202-
else:
203-
self._gl = nbgl.NBGL(app.widget, MODELS[model].screen_size, model)
201+
self._bagl_gl = bagl.Bagl(app.widget, MODELS[model].screen_size, model)
202+
self._nbgl_gl = nbgl.NBGL(app.widget, MODELS[model].screen_size, model)
204203

205204
@property
206205
def m(self) -> QWidget:
207206
return self.app.widget
208207

209208
@property
210-
def gl(self):
211-
return self._gl
209+
def bagl_gl(self):
210+
return self._bagl_gl
211+
212+
@property
213+
def nbgl_gl(self):
214+
return self._nbgl_gl
212215

213216
def _key_event(self, event: QKeyEvent, pressed) -> None:
214217
key = Qt.Key(event.key())
@@ -223,20 +226,18 @@ def _key_event(self, event: QKeyEvent, pressed) -> None:
223226
self.app.close()
224227

225228
def display_status(self, data: bytes) -> List[TextEvent]:
226-
assert isinstance(self.gl, bagl.Bagl)
227-
ret = self.gl.display_status(data)
229+
ret = self.bagl_gl.display_status(data)
228230
if MODELS[self.model].name == 'blue':
229231
self.screen_update() # Actually, this method doesn't work
230232
return ret
231233

232234
def display_raw_status(self, data: bytes) -> None:
233-
assert isinstance(self.gl, bagl.Bagl)
234-
self.gl.display_raw_status(data)
235+
self.bagl_gl.display_raw_status(data)
235236
if MODELS[self.model].name == 'blue':
236237
self.screen_update() # Actually, this method doesn't work
237238

238239
def screen_update(self) -> bool:
239-
return self.gl.refresh()
240+
return self.bagl_gl.refresh()
240241

241242

242243
class QtScreenNotifier(DisplayNotifier):

speculos/mcu/screen_text.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
105105

106106
self.width, self.height = MODELS[display_args.model].screen_size
107107
self.m = TextWidget(self, display_args.model)
108-
if self.use_bagl:
109-
self._gl = bagl.Bagl(self.m, MODELS[display_args.model].screen_size, display_args.model)
110-
else:
111-
raise NotImplementedError("This display can not emulate NBGL OS yet")
108+
self._gl = bagl.Bagl(self.m, MODELS[display_args.model].screen_size, display_args.model)
112109

113110
if display_args.keymap is not None:
114111
self.ARROW_KEYS = list(map(ord, display_args.keymap))

speculos/mcu/seproxyhal.py

+28-28
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ class SeProxyHal(IODevice):
253253
def __init__(self,
254254
sock: socket,
255255
model: str,
256-
use_bagl: bool,
257256
automation: Optional[Automation] = None,
258257
automation_server: Optional[BroadcastInterface] = None,
259258
transport: TransportType = TransportType.HID,
@@ -264,7 +263,8 @@ def __init__(self,
264263
self.automation = automation
265264
self.automation_server = automation_server
266265
self.events: List[TextEvent] = []
267-
self.refreshed = False
266+
self.need_nbgl_refresh = False
267+
self.is_last_draw_nbgl = False
268268
self.verbose = verbose
269269

270270
self.status_event = threading.Event()
@@ -277,9 +277,7 @@ def __init__(self,
277277

278278
self.transport = build_transport(self.socket_helper.queue_packet, transport)
279279

280-
self.ocr = OCR(model, use_bagl)
281-
282-
self.use_bagl = use_bagl
280+
self.ocr = OCR(model)
283281

284282
# A list of callback methods when an APDU response is received
285283
self.apdu_callbacks: List[Callable[[bytes], None]] = []
@@ -333,17 +331,17 @@ def can_read(self, screen: DisplayNotifier):
333331

334332
if tag == SephTag.GENERAL_STATUS:
335333
if int.from_bytes(data[:2], 'big') == SephTag.GENERAL_STATUS_LAST_COMMAND:
336-
if self.refreshed:
337-
self.refreshed = False
334+
if self.need_nbgl_refresh:
335+
self.need_nbgl_refresh = False
338336

339337
# Update the screenshot, we'll upload its associated events shortly
340-
screen.display.gl.update_screenshot()
341-
screen.display.gl.update_public_screenshot()
338+
screen.display.nbgl_gl.update_screenshot()
339+
screen.display.nbgl_gl.update_public_screenshot()
342340

343-
if self.use_bagl and screen.display.screen_update():
341+
if self.is_last_draw_nbgl is False and screen.display.screen_update():
344342
if screen.display.model in ["nanox", "nanosp"]:
345343
self.events += self.ocr.get_events()
346-
elif not self.use_bagl:
344+
elif self.is_last_draw_nbgl:
347345
self.events += self.ocr.get_events()
348346

349347
# Apply automation rules after having received a GENERAL_STATUS_LAST_COMMAND tag. It allows the
@@ -362,6 +360,7 @@ def can_read(self, screen: DisplayNotifier):
362360
SephTag.DBG_SCREEN_DISPLAY_STATUS,
363361
SephTag.BAGL_DRAW_RECT]:
364362
self.logger.debug(f"DISPLAY_STATUS {data!r}")
363+
self.is_last_draw_nbgl = False
365364
if screen.display.model not in ["nanox", "nanosp"] or tag == SephTag.BAGL_DRAW_RECT:
366365
events = screen.display.display_status(data)
367366
self.events += events
@@ -371,8 +370,9 @@ def can_read(self, screen: DisplayNotifier):
371370
elif tag in [SephTag.SCREEN_DISPLAY_RAW_STATUS, SephTag.BAGL_DRAW_BITMAP]:
372371
self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS")
373372
screen.display.display_raw_status(data)
373+
self.is_last_draw_nbgl = False
374374
if screen.display.model in ["nanox", "nanosp"]:
375-
self.ocr.analyze_bitmap(data)
375+
self.ocr.analyze_bitmap(data, True)
376376
if tag != SephTag.BAGL_DRAW_BITMAP:
377377
self.socket_helper.send_packet(SephTag.DISPLAY_PROCESSED_EVENT)
378378
if screen.display.rendering == RENDER_METHOD.PROGRESSIVE:
@@ -425,34 +425,34 @@ def can_read(self, screen: DisplayNotifier):
425425
pass
426426

427427
elif tag == SephTag.NBGL_DRAW_RECT:
428-
assert isinstance(screen.display.gl, NBGL)
429-
self.events += screen.display.gl.hal_draw_rect(data)
428+
assert isinstance(screen.display.nbgl_gl, NBGL)
429+
self.events += screen.display.nbgl_gl.hal_draw_rect(data)
430430

431431
elif tag == SephTag.NBGL_REFRESH:
432-
assert isinstance(screen.display.gl, NBGL)
433-
screen.display.gl.refresh(data)
432+
screen.display.nbgl_gl.refresh(data)
434433
# Stax/Flex only
435-
# We have refreshed the screen, remember it for the next time we have SephTag.GENERAL_STATUS
434+
# We have need_nbgl_refresh the screen, remember it for the next time we have SephTag.GENERAL_STATUS
436435
# then we'll perform a screen update and make public the resulting screenshot
437-
self.refreshed = True
436+
self.need_nbgl_refresh = True
437+
self.is_last_draw_nbgl = True
438438

439439
elif tag == SephTag.NBGL_DRAW_LINE:
440-
assert isinstance(screen.display.gl, NBGL)
441-
screen.display.gl.hal_draw_line(data)
440+
assert isinstance(screen.display.nbgl_gl, NBGL)
441+
screen.display.nbgl_gl.hal_draw_line(data)
442442

443443
elif tag == SephTag.NBGL_DRAW_IMAGE:
444-
assert isinstance(screen.display.gl, NBGL)
445-
self.ocr.analyze_bitmap(data)
446-
screen.display.gl.hal_draw_image(data)
444+
assert isinstance(screen.display.nbgl_gl, NBGL)
445+
self.ocr.analyze_bitmap(data, False)
446+
screen.display.nbgl_gl.hal_draw_image(data)
447447

448448
elif tag == SephTag.NBGL_DRAW_IMAGE_RLE:
449-
assert isinstance(screen.display.gl, NBGL)
450-
self.ocr.analyze_bitmap(data)
451-
screen.display.gl.hal_draw_image_rle(data)
449+
assert isinstance(screen.display.nbgl_gl, NBGL)
450+
self.ocr.analyze_bitmap(data, False)
451+
screen.display.nbgl_gl.hal_draw_image_rle(data)
452452

453453
elif tag == SephTag.NBGL_DRAW_IMAGE_FILE:
454-
assert isinstance(screen.display.gl, NBGL)
455-
screen.display.gl.hal_draw_image_file(data)
454+
assert isinstance(screen.display.nbgl_gl, NBGL)
455+
screen.display.nbgl_gl.hal_draw_image_file(data)
456456

457457
elif tag == SephTag.NFC_RAPDU:
458458
data = self.transport.handle_rapdu(data)

0 commit comments

Comments
 (0)