Complex UI management - Flex / Stax

Interacting programmatically with an application tends to be a non-trivial thing, as complex processes (like performing a complete transaction) have to be implemented through low-level actions on the device: forging bytes payloads (the APDUs), triggering the buttons or the screen at the right time, in the right places, in the right order, comparing runtime screens with expected ones, …

All these actions can be automated, with Ragger. When speaking more specifically about the UI, we saw previously that Ragger had some capabilities allowing to cope with simple physical interactions (like on the Nano devices: only two buttons). But what to do with more complex interfaces?

In particular, interacting with the touch screen devices (Stax or Flex) can be bothersome. It is hard to track of button positions, pages layouts and such.

Study case

For instance let’s imagine you develop an application with a welcome screen with a “start” button in the center, a “quit” button beneath and an “info” button on the top right.

If you click on the “quit” button, well the application shuts down.

If you click on the “info” button the screen shows some application infos, with a clickable “return” button on the lower center, which brings back to the previous, welcome screen.

This layouts has three clickable buttons. Low-level interaction with them would be something like:

1# going into the "info" screen
2backend.touch_finger(197, 606)
3
4# going back into the "welcome" screen
5backend.touch_finger(197, 606)
6
7# quitting the application
8backend.touch_finger(342, 55)

This does not look very complicated. However, this is just obfuscated code. Without extended comment, you can’t ask someone to understand or remember what this code does. This is a guaranteed path to hard to maintain code.

Moreover, these pixel positions are not guaranteed to last. If the SDK chooses to change some button position, or if higher-level graphic objects (such as Pages or UseCase) changes the position - nothing prevents them to move the “info” button to the top left -, all this code becomes deprecated.

That’s why Ragger mimics the Flex/Stax SDK graphics library and provides Layout and Use Case (Page will also come soon) classes that keep track of every interactive screen elements and expose meaningful methods to interact with them.

Layouts

Ragger’s Layouts and UseCases allows to quickly describe an application screens and its attached behavior in a purely declarative way, thanks to the MetaScreen metaclass. For instance, with the previously described application:

1from ragger.firmware.touch.screen import MetaScreen
2from ragger.firmware.touch.layouts import CancelFooter, ExitFooter, InfoHeader
3
4class RecoveryAppScreen(metaclass=MetaScreen)
5    layout_quit = ExitFooter
6    layout_go_to_info_page = InfoHeader
7    layout_return_to_welcome_page = CancelFooter

The metaclass will automatically detect all variables starting with layout_ and create related attributes when the RecoveryAppScreen will be instantiated. This latter will need - like a lot of Ragger classes - a backend and a firmware as arguments.

Once instantiated, the created screen can be interacted with in a more flexible way than if positions were still necessary:

 1# let's say we still have a ``backend`` and a ``firmware`` fixture
 2screen = RecoveryAppScreen(backend, firmware)
 3
 4# the application starts on the "welcome" page, from here we can either quit
 5# the application, or go to the "info" page
 6
 7# this method call will trigger a ``finger_touch`` with the positions related
 8# to the "info" centered lower button
 9screen.go_to_info_page.tap()
10
11# now the application is on the "info" screen, it can only go back to the
12# "welcome" page
13screen.return_to_welcome_page.tap()
14
15# now the application is back on the "welcome" screen. Let's quit
16screen.quit.tap()
17
18# the application is now stopped

Note

You may have noticed that the two centered lower buttons (the welcome page “quit” button and the info page “return” button) are exactly at the same (x, y) positions, so why bother declaring them twice?

First of all, the buttons may be at the same place, but they don’t carry the same purpose, and it is a good idea to reflect that on the code.

Second, if in a future version the Flex/Stax design changes and one of these button moves somewhere else on the screen’s footer, the layouts will be updated accordingly in Ragger, and the CancelFooter or ExitFooter will still be valid, hence all code using this class remains valid too.

If these arguments does not convince you, Ragger provides purely positional Layouts, and you can use CenteredFooter in replacement of both of these Layouts.

Use cases

But this is not simple enough yet. The previously shown screens are very common, so common in fact that the SDK provides dedicated high-level Use Cases to simplify their creation.

In this case, there is two. In the SDK, they are named:

  • nbgl_useCaseHome, which displays the “welcome” page, while allowing to access an “info” or “settings” page.

  • nbgl_useCaseSettings, which displays an “info” or “settings” page. This Use Case is very convenient when dealing with multiple info or settings which need several pages to be displayed (hence needs navigation buttons).

Ragger replicates these Use Cases, and provides more meaningful methods on top of them. Using Use Cases is very similar to Layouts; they need to be declared as attribute of a class using the MetaScreen metaclass, and start with use_case_:

 1from ragger.firmware.touch.screen import MetaScreen
 2from ragger.firmware.touch.use_case import UseCaseHome, UseCaseSettings
 3
 4class RecoveryAppScreen(metaclass=MetaScreen)
 5    use_case_welcome = UseCaseHome
 6    use_case_info = UseCaseSettings
 7
 8# let's say we still have a ``backend`` and a ``firmware`` fixture
 9screen = RecoveryAppScreen(backend, firmware)
10
11# the application starts on the "welcome" page, from here we can either quit
12# the application, or go to the "info" page
13
14# this method call will trigger a ``finger_touch`` with the positions related
15# to the "info" centered lower button
16screen.welcome.info()
17
18# now the application is on the "info" screen, it can only go back to the
19# "welcome" page.
20# if the info needed to be shown on several pages, this Use Case also
21# provides navigation methods, ``.next`` and ``.back``
22screen.info.exit()
23
24# now the application is back on the "welcome" screen. Let's quit
25screen.welcome.quit()
26
27# the application is now stopped

All-in-one solution: the FullScreen

All these classes helps you tailoring a fairly elegant and straight-forward client with meaningful and easy to write screen controls. However if you don’t feel like crafting you own screen representation, Ragger comes with a FullScreen class which embeds every existing Layout and Use Case.

It can be used to quickly instantiate a screen which could work with any application screen, however of course, all action on this class are not guaranteed to trigger a desired reaction (or no reaction at all) on the application screen, as declared button can be totally fictional.

 1from ragger.firmware.touch.screen import FullScreen
 2
 3screen = FullScreen(backend, firmware)
 4
 5# these use case methods will work in our case
 6screen.home.info()
 7screen.settings.exit()
 8screen.welcome.quit()
 9
10# layouts are also available, on these method will work too
11screen.info_footer.tap()
12screen.exit_footer.tap()
13screen.exit_header.tap()
14
15# this, however, will just randomly click on the screen and may or may not
16# trigger totally unrelated reaction
17screen.letter_only_keyboard.write("hello world!")