Langage › Interfaces utilisateur

Langage

Cette page n’est pas encore traduite : voici la version anglaise.

The product's user interface

A product often has a screen: a dashboard in a car, a touch panel on a machine, a small display on a sensor. In arche-lang you describe that screen in the model, next to what it shows: the component that owns the display gets a ui { } block, and each gauge, label and button in it is bound to the model — a variable, a message the part receives, the state of a part beside it. When the model changes, archec tells you what the screen no longer matches.

This page is a guide. Every word and property is in the language reference; archec explain ui lists the checks.

A first screen#

component Dashboard "Driver display" {
  in bus: data receives STATUS, BRAKE_FAULT
  out cmd: data sends ENGAGE_REQ, STOP
  var lights: bool = false

  ui {
    display 800x480
    input touch
    theme dark

    screen Driving "Driving" {
      row {
        gauge speedo "Speed" { value bus.STATUS.speed }
        label mode { text "Mode: ${ctrl.state}" }
      }
      row {
        button engage "Engage" { enabled ctrl.state == Ready; on press send cmd ENGAGE_REQ(true) }
        button stop "Stop" { style danger; on press send cmd STOP(true) }
        button settings "Settings" { on press goto Settings }
      }
    }

    screen Settings "Settings" {
      toggle lights "Lights" { value lights }
      button done "Done" { on press back }
    }

    overlay Warnings {
      telltale brake "Brake fault" { on bus.BRAKE_FAULT; color amber }
    }
  }
}

Read it from the top:

  • The device — display 800x480 (pixels), input touch (or encoder, keys, pointer), theme dark (or light). dpi 160 lets archec check that buttons are big enough for a finger. With no display, the display of a board the part is built on is taken (its hardware { display … }).
  • Screens — what the display shows at one time. The UI starts on the first one (or initial Settings); goto Settings and back move between them. A dialog is shown over the current screen until it is left; an overlay is shown over every screen — the place for warnings.
  • Layout — row (side by side), column (one under the other), grid (columns 3), stack (on top of each other), spacer (empty space). A screen's own children form a column. gap, padding, align, grow, width and height adjust them.
  • Elements — label, gauge, bar, chart, telltale, image, list, table, button, toggle, slider, select, input, stepper, and custom for one you draw yourself. The name after the word (speedo) is optional; the "Text" after it is what the element says.

These words are words only inside ui { }. Anywhere else, label, button or row are ordinary names: a part called label still works.

What an element shows: bindings#

value, enabled, visible and a telltale's on take an expression of the action language — the same one ```arche bodies use. The names in it are what the display's part knows:

You write It is
lights a variable of the part
state the part's own state
bus.STATUS.speed the last STATUS received on port bus, its field speed
bus.BRAKE_FAULT the last value of a message that is one value (message BRAKE_FAULT : bool)
ctrl.state, ctrl.speed the state, or a variable, of a part beside it in the system (the preview and the simulator read the model; a real product gets them as messages)
MAX_SPEED a constant
DriveMode.MANUAL an enum value

A state is compared by its name: ctrl.state == Ready. A text says values with ${…}: text "Obstacle ${bus.STATUS.distance} m".

archec knows each value's type, unit and range from the model. A gauge on bus.STATUS.speed (a float32 unit km/h range 0..200) takes its unit and its range from it; one that says unit mph is reported (ui-binding), as a toggle bound to a number or a name the part does not have.

A toggle, slider, select, input or stepper bound to a variable of its own part sets it when it changes. To do something else, write on change … (value is the new value there).

What a press does: actions#

on press (also on release, on long-press, on change; a screen's on enter and on exit) takes action-language statements — on its line, or in a block:

button stop "Stop" {
  style danger
  on press {
    send cmd STOP(true)
    goto Driving
  }
}
Statement Does
send cmd STOP(true) sends a message on an out port; arguments in field order, or STOP { hard: true }
goto Settings shows another screen (or a dialog)
back goes back to the screen before
lights = not lights sets a variable of the part
if … { … } else { … } decides
reset() calls a function of the part

archec checks that the port sends that message, that the arguments fit its fields, that the screen exists — and that something receives it: a press on a port with no link, or to a part that does not receive the message, is reported (ui-dead-press).

The checks#

Code Finds
ui-structure a word that is not one of a screen (guage), two elements of one name, no screen, a display that is not WxH
ui-binding a name the UI cannot see, the wrong type for the element, a unit other than the element's, a send its port does not make, a goto to no screen
ui-dead-press a press nothing receives; a button with no action (a hint)
ui-unreachable a screen no goto reaches; a screen with no way out (the quick fix adds a Back button)
ui-safety a message a hazard or a safety goal names, shown on some screens only and not by a telltale in an overlay
ui-fit a screen whose content needs more than the display, a text wider than it, a touch target under 9 mm
ui-a11y contrast under WCAG AA, a button with no text, a telltale that says it by its color alone
ui-text a text not translated when the project's other texts are
ui-test a test step that presses or reads what no ui has (press hmi.engag, expect hmi.mode.colour), a change of a button (Testing a UI)

archec check reports them with the others; the editor shows them as you type, completes the element words inside a screen, the names a binding can read (bus. → its messages, bus.STATUS. → their fields), the screens after goto, and hovering an element says what it is bound to.

In the diagram: the palette#

The diagram's palette has a User interface section: Screen, Dialog, Overlay, the layouts (row, column, grid, stack, spacer) and every element. Drop one on a block, and it is written into the ui { } of the block's definition (for a part, the component it is a copy of), as one undoable edit:

  • a Screen (a dialog, an overlay) asks its name and goes at the end of the ui — and the block gets a ui { } when it has none;
  • an element or a layout goes into the screen (asked when the ui has several), at its end, with a first binding of what the ui can read: a gauge the first number (value bus.STATUS.speed), a toggle a bool variable of the part, a telltale a bool message. A block with no ui gets one, with a screen Main.

Select the block: its panel lists its ui — each screen, layout and element. Click one for its form, made from the word's own entry (the same as completion and hover): value, enabled, visible (and a telltale's on) suggest the names the ui can read, of the kind the element shows (bus.STATUS.speed, ctrl.state, lights), and take any expression; style, size and align are choices; text, range, unit, format, gap… are fields. The actions (on press …) are written in the source.

Themes and UI kits#

theme dark and theme light are archec's; colors { … } changes some. A theme of your own is a constant record, base the theme it starts from:

component Panel "Front panel" {
  ui {
    theme ACME
    screen Main "Main" { label hello { text "Hello" } }
  }
}

/// Ours: light, with the company's blue.
const ACME = { base: "light", primary: "#0d47a1", background: "#fafafa" }

A UI kit of the Arche Store ships one: theme "acme/automotive-dark" takes the theme of the kit the project uses by that name. Kits are themes only for now.

Requirements and translations#

satisfies REQ-HMI-3 on a screen or an element traces the requirement to the UI. Texts are translated like every text of the model: "Driving" fr "Conduite" where it is written, or a translations fr { "Driving" "Conduite" } block.

Seeing it: archec ui#

archec ui main.arche          # the screens, element by element, with what each reads and does
archec ui main.arche --json   # the resolved model

The build writes the same model to ui/<component>.json in the Arche project. It is the one thing previews, simulators, tests and code generators read — the UiModel type in src/ui.ts: the device and theme, each screen's element tree with sizes, every binding as a small expression tree and as JavaScript, every action as checked statements, the navigation, the texts with their translations, and where each element is written.

The autonomous car's HMI has one: examples/autonomous-car/components.arche.

Seeing the screens: archec ui preview#

archec ui preview main.arche                      # screens.html: every screen, drawn
archec ui preview main.arche --screen Settings    # one
archec ui preview main.arche --png screens.png    # a picture of them (headless Chrome)

One page, nothing to install, no external URL: each screen and dialog of each ui at the display's size (scaled to fit), the overlays over them, in the theme's colors. Each element shows a sample value: the model's own first values (each part's variables and state at the start of a run of the browser simulator — the car's mode label says Mode: Boot), its constants, and for a message not received yet a value within its type's range. Click an element: the page says what it is, what it reads (value bus.STATUS.speed, its unit), what a press does, and the file and line it is written on.

--png asks Chrome or Chromium (--chrome <path>, $CHROME, else the usual places). archec doc --html shows the same screens as figures, in a chapter User interface.

In VS Code, UI Preview (the button in the editor's title bar of a model with a ui, or Arche: UI Preview) shows the screens beside the editor and draws them again as you type; a click on an element shows its line.

Simulating them#

The screens run with the model in the browser simulator:

  • archec sim main.arche --html sim.html — the page shows the screens above the block diagram. Their values come from the run: the part's variables and state, the last message each of its ports received, the parts beside it. A press sends its message into the run, where the port's links take it; goto and back move between the screens. Engage on the car's dashboard: the ADCU goes Autonomous, and the mode label says so.
  • VS Code ▶ Run (the diagram's Run, Simulate): the UI Preview opens beside and shows the screens live — press the buttons there.
  • The store's "Try it" page of a package with a ui: the same.

A test drives them with no page#

The runtime under all of them is headless (src/uiruntime.ts): it holds the screen shown and the way back, works out what each element shows from a value source, and runs presses and changes. It interprets the model's expression trees — no eval — so it runs in a webview, in a page, or in a test:

const { buildUi, createUiRuntime } = require('arche-lang');
const rt = createUiRuntime(buildUi(program)[0], {
  values: (key) => values[key],                  // 'ctrl.state', 'bus.STATUS.speed', …
  send: (port, message, fields) => sent.push({ port, message, fields }),
});
rt.screen;                     // 'Driving'
rt.element('engage');          // { text: 'Engage', enabled: false, visible: true, … }
rt.press('engage');            // { ok: false, why: 'Driving.row1.engage is disabled' }
rt.change('Settings.lights', true);
rt.snapshot();                 // the screen, the way back, every element shown

uiSimLink().create(model, simProgram, live) is the value source and the send over the live browser simulator (src/websimlive.ts).

Testing a UI#

A test case drives the screens as the driver would, in its do and expect lines — the element as the part with the ui, then its id (hmi.engage), or Screen.id when two screens have one of that name:

test-plan TP-UI "Driver HMI, on the screen" {
  case TC-UI-01 "Engage, then take over" {
    step "Ready" {
      wait 5ms
      expect hmi.engage.enabled
      expect hmi.mode.text == "Mode: Ready"
    }
    step "Engage" {
      do press hmi.engage
      wait 5ms
      expect hmi.mode.text == "Mode: Autonomous"
      expect sent("ENGAGE_REQ") == 1
      expect not hmi.mrm.on
    }
  }
  case TC-UI-02 "Details and back" {
    step { do press hmi.details; expect hmi.screen == Details }
    step { do press hmi.back; expect hmi.screen == Driving }
  }
}
Line Does
do press hmi.engage presses it (long-press, release the same): what its on press does — a send into the run, a goto, a back. A press on an element hidden, disabled or not on the screen shown fails the case, saying why
do change hmi.limit 80 a toggle, slider, select, input or stepper set (True, "text", a constant): its variable, its on change
expect hmi.mode.text == "…" what an element shows: text, label, value, display (formatted, with its unit), enabled, visible, on (a telltale), shown (visible, on a screen shown), options
expect hmi.screen == Details the screen shown (a screen by its name, or "Details")
expect within 50ms: sent("ENGAGE_REQ") >= 1 the messages sent so far, a press's among them

They go in a timeline too (at 5ms do press hmi.engage), with the rest of the case's Python (adcu.state == "Manual"). The checker knows them: a name that is no element, a part with no ui, a change of a button is an error (ui-test), as you type.

Where they run. Arche's Python simulator has no screen, no enabled, no text formatting: a case with a ui step runs on the browser simulator (the model's behaviour in node, src/websimrt.ts) with the ui's runtime over it, the same pair as archec sim --html and ▶ Run. archec test runs those cases there and the others in arche-cli, in one report (and with no arche-cli at all when every case it runs has a ui step); --sim web runs every case there. In the Arche project the ui lines are written as Python that stops the case, saying that it runs on the browser simulator — run from Arche, such a case fails, it never passes for nothing.

Generated. archec gen --target tests writes, beside the transitions and the limits, a plan TP-GEN-UI: a case per button — the presses that bring the ui to its screen (the shortest way by gotos from where it starts), a wait and an expect … .enabled when the button has a condition, the press, then what it must do (its sent("MSG"), its screen after a goto or a back) — and a case per screen, reaching it. A screen no goto reaches is written with a TODO and listed in tests/generated.allow, as the transitions nothing brings.

In CI. archec ci has a ui gate: a binding, a press or a ui step that does not hold fails it — a message renamed while a button still sends the old name (ui-binding), a press nothing receives (ui-dead-press). Its test gate runs the ui cases on the browser simulator even when arche-cli is not there.

Golden screenshots#

archec ui snapshot main.arche --update   # ui/screenshots/Hmi/Driving.png, Details.png
archec ui snapshot main.arche            # each screen drawn again, compared
archec test main.arche --ui-golden       # the cases, then the same comparison

Each screen and dialog is drawn as archec ui preview --png draws it (headless Chrome, the sample values) and compared pixel by pixel with its golden, committed beside the model. A pixel whose channels are within 24 of the golden's is the same (a text's antialiasing); a screen with more than --tolerance percent of its pixels changed (0.1 by default) fails, and its diff is written — arche-out/.archec/ui-diff/Hmi/Driving.png, the golden faded and what changed in red, with Driving.now.png beside it. When the change is meant, --update. No Chrome: the comparison is skipped and said so. Goldens drawn on one machine may differ on another (its fonts): make them where the comparison runs.

The web target: gen --target ui-web#

archec gen main.arche --target ui-web -o out    # out/ui-web/index.html, ui.js, ui.css, ui.json

A folder that runs on its own — open index.html, or serve the folder. By default the values come from the model, simulated in the page in real time (the system the ui's part is in): it boots, the buttons send into the run. ui.json holds the UI models, the sample values and the simulator's program, for other tools.

With index.html?ws=ws://host:port the values come from a WebSocket instead — the real product, a bridge to a CAN bus, a test bench. One JSON object per message:

From Message Means
the server {"type":"value","key":"bus.STATUS.speed","value":72.5} a ref's value now (key: a key of the model's refs)
the server {"type":"values","values":{"ctrl.state":"Ready","bus.STATUS.speed":72.5}} several at once
the page {"type":"hello","uis":["Dashboard"],"keys":{"Dashboard":["bus.STATUS.speed","ctrl.state","lights"]}} on connecting: the keys it reads
the page {"type":"send","ui":"Dashboard","port":"cmd","message":"STOP","fields":{"hard":true}} a press sent a message (one that is one value: "fields":{"value":true})
the page {"type":"set","ui":"Dashboard","key":"lights","value":true} the UI set a variable of its part (a toggle)

Any message may carry "ui" to name one ui of several. The page reconnects when the socket closes.

Code for a device: ui-lvgl and ui-slint#

archec gen main.arche --target c -o generated        # the parts, as C
archec gen main.arche --target ui-lvgl -o generated  # the ui, for LVGL 9, beside it
archec gen main.arche --target ui-slint -o generated # the ui, in Slint, as a Rust crate

LVGL (ui-lvgl/): ui_<id>.c/.h for each ui, written again at each run, and ui_<id>_task.c, written once — yours.

  • Each screen is an LVGL screen, a dialog a box over it (on lv_layer_top), an overlay on lv_layer_top too, over every screen. row, column and grid are flex containers, stack children centred on one another; the theme's colors are the screens', the text's and the buttons'.
  • ui_<id>_create(&part) builds them for the part the ui belongs to (its C struct of --target c); ui_<id>_update() reads every binding again — call it in the ui's thread before lv_timer_handler(). A variable is the part's (self->lights), a message is the last one received: the part keeps the last of each message its ui reads (arche_ui_bus_status in its struct, set by dashboard_ui_got_bus_status(), which the wiring calls as each one comes). A state or a variable of a part beside it (ctrl.state) is in ui_<id>_peers: with a deployment the wiring copies it from that part when it runs in the same binary; else set ui_<id>_peers_hook (a real product gets it over a message).
  • An event is a callback: send cmd STOP(true) calls the part's send function — the one the C target writes (dashboard_send_cmd_stop) —, goto/back a navigation stack (ui_<id>_goto, ui_<id>_back, and each screen's on enter/on exit), lights = not lights the part's variable; a toggle, slider, select, input or stepper bound to a variable sets it as it changes.
  • ui_<id>_task.c is the ui's thread: lv_init, the display (an SDL window on a host with LV_USE_SDL, else a buffer and the flush callback your panel's driver gives), the loop. A binary of a deployment built with -DARCHE_UI starts it from <binary>_start.

Slint (ui-slint/<id>/): a Rust crate. ui/<id>.slint has a global <Id>State — a property for each name the ui reads, the screen shown, a callback for each event — a component per screen, dialog and overlay, and the <Id>Window; the bindings are Slint expressions over the global. src/<id>_glue.rs is the model behind it: Received (each message the ui reads, each part beside it), Sent (each message it sends), <Id>Ui with the values, receive(), apply() to the window, and the events in Rust; bind() installs the callbacks. src/main.rs, Cargo.toml and build.rs are written once: a host binary that feeds Received from a channel and prints each Sent — give it your bus.

What each makes. Every element word has an entry in each target's list (src/gen/uitargets.ts); one a target makes only in part is a note of archec gen, never dropped:

Element LVGL Slint
row column grid stack spacer flex containers layouts
label button toggle slider select input lv_label, lv_button, lv_switch, lv_slider (its step), lv_dropdown, lv_textarea Text, Button, Switch, Slider, ComboBox, LineEdit
gauge lv_arc, its value in it its value and a ProgressIndicator (no dial in Slint)
bar chart lv_bar, lv_chart (a point each window/60) ProgressIndicator, a Path the glue draws
telltale lv_led and its text, hidden while off a dot and its text, while on
stepper lv_spinbox with − and + SpinBox (its step 1)
image lv_image of the file through LVGL's file system (UI_IMAGE_DRIVE) Image (@image-url, beside the .slint)
list table their fixed options, columns; a bound value's items are yours the same
custom its lvgl code its slint code

release is a click in Slint; a long press is not made there (a note). Texts are in the language they are written in.

The display. With no display in the ui, a display part beside the ui's part — in its system, or in the platform of its deployment — gives the size, the density and the input: the store has an ILI9341 (320×240, touch), ST7789s (240×320, 240×240), the SSD1306 (128×64) and an HDMI display (boards). The rover template (archec new --template rover-esp32) has a status screen on an ILI9341.

On a Linux board: Qt Quick (--target ui-qml)#

archec gen main.arche --target ui-qml -o generated
cd generated/ui-qml/Dashboard
cmake -B build && cmake --build build && ./build/dashboard_ui     # C++, Qt 6.5+
python3 main.py                                                 # or PySide6

Each ui { } becomes a Qt Quick application in ui-qml/<component>/, written from the resolved model (never from the source):

File What it is
Main.qml the window at the display's size: a StackView of the screens (goto shows one, back to it when it is under the current one; back pops), a Popup for the dialogs, the overlays over everything, the theme's colors
<Screen>Screen.qml a component per screen (<D>Dialog.qml, <O>Overlay.qml): row, column, grid as RowLayout, ColumnLayout, GridLayout; on enter / on exit on StackView.onActivated / onDeactivated
uimodel.h, uimodel.py UiModel, the object the bindings read: a property per name the UI reads (bus.STATUS.speed → bus_STATUS_speed), in C++ (Q_PROPERTY) and for PySide6
uibridge.h, bridge.py the bridge to the product (below)
main.cpp, CMakeLists.txt, main.py the application — written once, then yours
README.md the properties and the protocol

The elements are Qt Quick Controls (the Basic style, with the theme as its palette): label a Label, gauge an arc on a Canvas, bar a ProgressBar, chart a sparkline on a Canvas over its window, telltale a lit Rectangle shown while its condition holds, image an Image, list a ListView, table a TableView (a column per field of columns), button a Button (style danger in the theme's danger color), toggle a Switch, slider a Slider, select a ComboBox, input a TextField, stepper a SpinBox, and custom its qml block, handed value. Each has its path in the UI model as its objectName (Driving.column0.row1.stop): what a test finds it by.

A binding is a QML binding on root.model.<name>; an event is the checked statements as JavaScript: root.model.send("cmd", "STOP", { "hard": true }), root.nav.goto("Settings"), root.nav.back(), root.model.lights = !root.model.lights. A control bound to a variable of its part sets it when it changes.

What QML cannot show is said in a note, never dropped: a custom with no qml block (a placeholder is shown), an image to copy next to the QML, a table whose columns are not known (shown as a list), a SpinBox's whole numbers, translations (the texts are in the language of the model).

The bridge#

On a Raspberry Pi or a Jetson, the UI talks to the rest of the product through UiModel: set its values, connect to its sent signal. Two bridges are written:

  • WebSocket, JSON text frames — ARCHE_UI_WS=ws://host:8765 (C++ when Qt WebSockets is installed, and Python):
    • in {"type":"value","key":"bus.STATUS.speed","value":42} sets a value the UI reads;
    • out {"type":"send","port":"cmd","message":"STOP","fields":{"hard":true}} for a press;
    • out {"type":"value","key":"lights","value":true} when the UI changes a variable of its part.
  • SocketCAN (Python: pip install python-can cantools) — ARCHE_UI_CAN=can0 ARCHE_UI_DBC=bus.dbc python3 main.py, with the DBC of archec gen --target dbc: a received frame sets the keys its message is read by (uimodel.RECEIVES, by signal name; a field a.b is the signal a_b), a press is encoded by its message's name. The state or a variable of a part beside the display is not on a bus: it comes over the WebSocket.