Skip to content

Commit

Permalink
Support apps for NanoX and NanoS+ built on NBGL
Browse files Browse the repository at this point in the history
  • Loading branch information
nroggeman-ledger committed Feb 22, 2024
1 parent 3a3439f commit daaec3c
Show file tree
Hide file tree
Showing 20 changed files with 136 additions and 53 deletions.
Binary file added speculos/cxlib/nanosp-api-level-cx-15.elf
Binary file not shown.
Binary file added speculos/cxlib/nanox-api-level-cx-15.elf
Binary file not shown.
Binary file modified speculos/cxlib/stax-api-level-cx-15.elf
Binary file not shown.
Binary file added speculos/fonts/nanosp-fonts-15.bin
Binary file not shown.
Binary file added speculos/fonts/nanox-fonts-15.bin
Binary file not shown.
46 changes: 36 additions & 10 deletions speculos/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,26 @@ def get_cx_infos(app_path):
return sh_offset, sh_size, sh_load, cx_ram_size, cx_ram_load


def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) -> int:
def get_elf_fonts_size(app_path):
with open(app_path, 'rb') as fp:
elf = ELFFile(fp)
text = elf.get_section_by_name('.text')
for seg in elf.iter_segments():
if seg['p_type'] != 'PT_LOAD':
continue
if seg.section_in_segment(text):
break
else:
raise RuntimeError("No program header with text section!")
symtab = elf.get_section_by_name('.symtab')
bagl_fonts_symbol = symtab.get_symbol_by_name('C_bagl_fonts')
if bagl_fonts_symbol is not None:
return bagl_fonts_symbol[0]['st_size']

return 0


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

if args.debug:
Expand Down Expand Up @@ -170,13 +189,12 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) ->
else:
logger.warn(f"Cx lib {cxlib_filepath} not found")

if args.model == "stax":
fonts_filepath = f"/fonts/{args.model}-fonts-{args.apiLevel}.bin"
fonts = pkg_resources.resource_filename(__name__, fonts_filepath)
if os.path.exists(fonts):
argv += ['-f', fonts]
else:
logger.warn(f"Fonts {fonts_filepath} not found")
fonts_filepath = f"/fonts/{args.model}-fonts-{args.apiLevel}.bin"
fonts = pkg_resources.resource_filename(__name__, fonts_filepath)
if os.path.exists(fonts):
argv += ['-f', fonts]
elif not is_bagl:
logger.warn(f"Fonts {fonts_filepath} not found")

extra_ram = ''
app_path = getattr(args, 'app.elf')
Expand All @@ -185,6 +203,7 @@ def run_qemu(s1: socket.socket, s2: socket.socket, args: argparse.Namespace) ->
load_offset, load_size, stack, stack_size, ram_addr, ram_size, \
text_load_addr, svc_call_address, svc_cx_call_address, \
fonts_addr, fonts_size = get_elf_infos(lib_path)

# Since binaries loaded as libs could also declare extra RAM page(s), collect them all
if (ram_addr, ram_size) != (0, 0):
arg = f'{ram_addr:#x}:{ram_size:#x}'
Expand Down Expand Up @@ -326,6 +345,12 @@ def main(prog=None) -> int:
args.model = metadata["target"]
logger.warn(f"Device model detected from metadata: {args.model}")

# For Nano S and Blue, it can only be BAGL
if args.model == "nanos" or args.model == "blue":
is_bagl = True
else:
is_bagl = get_elf_fonts_size(app_path) != 0

if not args.apiLevel:
if "api_level" in metadata:
args.apiLevel = metadata["api_level"]
Expand Down Expand Up @@ -473,13 +498,14 @@ def main(prog=None) -> int:

s1, s2 = socket.socketpair()

qemu_pid = run_qemu(s1, s2, args)
qemu_pid = run_qemu(s1, s2, args, is_bagl)
s1.close()

apdu = apdu_server.ApduServer(host="0.0.0.0", port=args.apdu_port)
seph = seproxyhal.SeProxyHal(
s2,
model=args.model,
is_bagl=is_bagl,
automation=automation_path,
automation_server=automation_server,
transport=args.usb)
Expand Down Expand Up @@ -521,7 +547,7 @@ def main(prog=None) -> int:
display_args = DisplayArgs(args.color, args.model, args.ontop, rendering,
args.keymap, zoom, x, y)
server_args = ServerArgs(apdu, apirun, button, finger, seph, vnc)
screen_notifier = ScreenNotifier(display_args, server_args)
screen_notifier = ScreenNotifier(display_args, server_args, is_bagl)

if apirun is not None:
assert automation_server is not None
Expand Down
5 changes: 3 additions & 2 deletions speculos/mcu/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,19 @@ class DisplayNotifier(ABC):
through VNC if activated.
"""

def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs, is_bagl: bool) -> None:
# TODO: this should be Dict[int, IODevice], but in QtScreen, it is
# a QSocketNotifier, which has a completely different interface
# and is not used in the same way in the mcu/screen.py module.
self.notifiers: Dict[int, Any] = {}
self._server_args = server_args
self._display_args = display_args
self._display: Display
self.is_bagl = is_bagl
self.__init_notifiers()

def _set_display_class(self, display_class: type):
self._display = display_class(self._display_args, self._server_args)
self._display = display_class(self._display_args, self._server_args, self.is_bagl)

@property
def display(self) -> Display:
Expand Down
8 changes: 4 additions & 4 deletions speculos/mcu/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@


class Headless(Display):
def __init__(self, display: DisplayArgs, server: ServerArgs) -> None:
def __init__(self, display: DisplayArgs, server: ServerArgs, is_bagl: bool) -> None:
super().__init__(display, server)

self.m = HeadlessPaintWidget(self.model, server.vnc)
self._gl: GraphicLibrary
if display.model != "stax":
if is_bagl:
self._gl = bagl.Bagl(self.m, MODELS[self.model].screen_size, self.model)
else:
self._gl = nbgl.NBGL(self.m, MODELS[self.model].screen_size, self.model)
Expand Down Expand Up @@ -67,8 +67,8 @@ def _redraw(self) -> None:

class HeadlessNotifier(DisplayNotifier):

def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
super().__init__(display_args, server_args)
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs, is_bagl: bool) -> None:
super().__init__(display_args, server_args, is_bagl)
self._set_display_class(Headless)

def run(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion speculos/mcu/nbgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(self,
self.logger = logging.getLogger("NBGL")

def __assert_area(self, area) -> None:
if area.y0 % 4 or area.height % 4:
if self.model == "stax" and (area.y0 % 4 or area.height % 4):
raise AssertionError("X(%d) or height(%d) not 4 aligned " % (area.y0, area.height))
if area.x0 > self.SCREEN_WIDTH or (area.x0+area.width) > self.SCREEN_WIDTH:
raise AssertionError("left edge (%d) or right edge (%d) out of screen" % (area.x0, (area.x0 + area.width)))
Expand Down
5 changes: 3 additions & 2 deletions speculos/mcu/ocr.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ class OCR:
MAX_BLANK_SPACE_NANO = 12
MAX_BLANK_SPACE_STAX = 24

def __init__(self, model: str):
def __init__(self, model: str, is_bagl: bool):
self.events: List[TextEvent] = []
# Store the model of the device
self.model = model
self.is_bagl = is_bagl
# Maximum space for a letter to be considered part of the same word
if model == "stax":
self.max_blank_space = OCR.MAX_BLANK_SPACE_STAX
Expand Down Expand Up @@ -215,7 +216,7 @@ def analyze_bitmap(self, data: bytes) -> None:
For older SKD versions, legacy behaviour is used: parsing internal
fonts to find a matching bitmap.
"""
if self.model == "stax":
if not self.is_bagl:
# Can be called via SephTag.NBGL_DRAW_IMAGE or SephTag.NBGL_DRAW_IMAGE_RLE
# In both cases, data contains:
# - area (sizeof(nbgl_area_t))
Expand Down
5 changes: 2 additions & 3 deletions speculos/mcu/rle_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,8 @@ def values_to_bpp1(data):
if bits != 7:
output += bytes([byte])

nb_bytes = len(data)/8
if len(data) % 8:
nb_bytes += 1
nb_bytes = (len(data)+7)//8

assert len(output) == nb_bytes

return output
Expand Down
18 changes: 11 additions & 7 deletions speculos/mcu/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ def __init__(self, parent, model: str, pixel_size: int, vnc: Optional[VNC] = Non
self.pixel_size = pixel_size
self.mPixmap = QPixmap()
self.vnc = vnc
self.updateRequested = False

def paintEvent(self, event: QEvent):
if self.pixels:
if self.pixels and self.updateRequested:
pixmap = QPixmap(self.size() / self.pixel_size)
pixmap.fill(Qt.white)
painter = QPainter(pixmap)
painter.drawPixmap(0, 0, self.mPixmap)
self._redraw(painter)
self.mPixmap = pixmap
self.pixels = {}
self.updateRequested = False

qp = QPainter(self)
copied_pixmap = self.mPixmap
Expand All @@ -53,6 +55,7 @@ def update(self, # type: ignore[override]
y: Optional[int] = None,
w: Optional[int] = None,
h: Optional[int] = None) -> bool:
self.updateRequested = True
if x and y and w and h:
QWidget.update(self, QRect(x, y, w, h))
else:
Expand Down Expand Up @@ -192,16 +195,17 @@ def closeEvent(self, event: QEvent):


class Screen(Display):
def __init__(self, display: DisplayArgs, server: ServerArgs) -> None:
def __init__(self, display: DisplayArgs, server: ServerArgs, is_bagl: bool) -> None:
super().__init__(display, server)
self.app: App
self._gl: GraphicLibrary
self.is_bagl = is_bagl

def set_app(self, app: App) -> None:
def set_app(self, app: App, is_bagl: bool) -> None:
self.app = app
self.app.set_screen(self)
model = self._display_args.model
if model != "stax":
if self.is_bagl:
self._gl = bagl.Bagl(app.widget, MODELS[model].screen_size, model)
else:
self._gl = nbgl.NBGL(app.widget, MODELS[model].screen_size, model)
Expand Down Expand Up @@ -244,13 +248,13 @@ def screen_update(self) -> bool:


class QtScreenNotifier(DisplayNotifier):
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs, is_bagl: bool) -> None:
self._qapp = QApplication([])
super().__init__(display_args, server_args)
super().__init__(display_args, server_args, is_bagl)
self._set_display_class(Screen)
self._app_widget = App(self._qapp, display_args, server_args)
assert isinstance(self.display, Screen)
self.display.set_app(self._app_widget)
self.display.set_app(self._app_widget, is_bagl)

def _can_read(self, device: IODevice) -> None:
try:
Expand Down
8 changes: 4 additions & 4 deletions speculos/mcu/screen_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ def _redraw(self):


class TextScreen(Display):
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs, is_bagl: bool) -> None:
super().__init__(display_args, server_args)

self.width, self.height = MODELS[display_args.model].screen_size
self.m = TextWidget(self, display_args.model)
if display_args.model != "stax":
if is_bagl:
self._gl = bagl.Bagl(self.m, MODELS[display_args.model].screen_size, display_args.model)
else:
raise NotImplementedError("This display can not emulate NBGL OS yet")
Expand Down Expand Up @@ -151,8 +151,8 @@ def get_keypress(self) -> bool:

class TextScreenNotifier(DisplayNotifier):

def __init__(self, display_args: DisplayArgs, server_args: ServerArgs) -> None:
super().__init__(display_args, server_args)
def __init__(self, display_args: DisplayArgs, server_args: ServerArgs, _is_bagl: bool) -> None:
super().__init__(display_args, server_args, _is_bagl)
self._set_display_class(TextScreen)

def run(self) -> None:
Expand Down
7 changes: 4 additions & 3 deletions speculos/mcu/seproxyhal.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class SeProxyHal(IODevice):
def __init__(self,
sock: socket,
model: str,
is_bagl: bool,
automation: Optional[Automation] = None,
automation_server: Optional[BroadcastInterface] = None,
transport: str = 'hid'):
Expand All @@ -265,7 +266,7 @@ def __init__(self,

self.usb = usb.USB(self.socket_helper.queue_packet, transport=transport)

self.ocr = OCR(model)
self.ocr = OCR(model, is_bagl)

# A list of callback methods when an APDU response is received
self.apdu_callbacks: List[Callable[[bytes], None]] = []
Expand Down Expand Up @@ -329,10 +330,10 @@ def can_read(self, screen: DisplayNotifier):
# Publish the new screenshot, we'll upload its associated events shortly
screen.display.gl.update_public_screenshot()

if screen.display.model != "stax" and screen.display.screen_update():
if not isinstance(screen.display.gl, NBGL) and screen.display.screen_update():
if screen.display.model in ["nanox", "nanosp"]:
self.events += self.ocr.get_events()
elif screen.display.model == "stax":
elif isinstance(screen.display.gl, NBGL):
self.events += self.ocr.get_events()

# Apply automation rules after having received a GENERAL_STATUS_LAST_COMMAND tag. It allows the
Expand Down
2 changes: 1 addition & 1 deletion src/bolos/bagl.c
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ unsigned long sys_bagl_hal_draw_bitmap_within_rect(
sys_io_seph_send(buf, len);
}

return 0;
return 0x9000; // SWO_SUCCESS
}

unsigned long sys_screen_update(void)
Expand Down
27 changes: 19 additions & 8 deletions src/bolos/fonts_info.c
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ void parse_fonts(void *code, unsigned long text_load_addr,
unsigned long fonts_addr, unsigned long fonts_size)
{
// Number of fonts stored at fonts_addr
uint32_t nb_fonts;
uint32_t nb_fonts = 0;
uint32_t *fonts;

nb_bitmap_char = 0;
Expand All @@ -236,14 +236,25 @@ void parse_fonts(void *code, unsigned long text_load_addr,
// Unsupported API_LEVEL, will not parse fonts!
return;
}
// On Stax, fonts are loaded at a known location
if (hw_model == MODEL_STAX) {
fonts = (void *)STAX_FONTS_ARRAY_ADDR;
if (sdk_version > SDK_API_LEVEL_14) {
nb_fonts = STAX_NB_FONTS;
// With NBGL apps, fonts are loaded at a known location, in the OS
if (fonts_size == 0) {
if (hw_model == MODEL_STAX) {
fonts = (void *)STAX_FONTS_ARRAY_ADDR;
if (sdk_version >= SDK_API_LEVEL_15) {
nb_fonts = STAX_NB_FONTS;
} else {
nb_fonts = STAX_NB_FONTS_12;
}
} else if (hw_model == MODEL_NANO_SP) {
fonts = (void *)NANOSP_FONTS_ARRAY_ADDR;
nb_fonts = NANO_NB_FONTS;
} else if (hw_model == MODEL_NANO_X) {
fonts = (void *)NANOX_FONTS_ARRAY_ADDR;
nb_fonts = NANO_NB_FONTS;
} else {
nb_fonts = STAX_NB_FONTS_12;
return;
}

} else {
fonts = remap_addr(code, fonts_addr, text_load_addr);
nb_fonts = fonts_size / 4;
Expand Down Expand Up @@ -278,7 +289,7 @@ void parse_fonts(void *code, unsigned long text_load_addr,

// Parse all those fonts and add bitmap/character pairs
for (uint32_t i = 0; i < nb_fonts; i++) {
if (hw_model == MODEL_STAX) {
if (fonts_size == 0) {
switch (sdk_version) {
case SDK_API_LEVEL_12:
case SDK_API_LEVEL_13:
Expand Down
Loading

0 comments on commit daaec3c

Please sign in to comment.