Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Support apps for NanoX and NanoS+ built on NBGL #444

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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")
nroggeman-ledger marked this conversation as resolved.
Show resolved Hide resolved

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
Loading