1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 20:49:49 +04:00

[FL-3893] JS modules (#3841)

* feat: backport js_gpio from unleashed
* feat: backport js_keyboard, TextInputModel::minimum_length from unleashed
* fix: api version inconsistency
* style: js_gpio
* build: fix submodule ._ .
* refactor: js_gpio
* docs: type declarations for gpio
* feat: gpio interrupts
* fix: js_gpio freeing, resetting and minor stylistic changes
* style: js_gpio
* style: mlib array, fixme's
* feat: js_gpio adc
* feat: js_event_loop
* docs: js_event_loop
* feat: js_event_loop subscription cancellation
* feat: js_event_loop + js_gpio integration
* fix: js_event_loop memory leak
* feat: stop event loop on back button
* test: js: basic, math, event_loop
* feat: js_event_loop queue
* feat: js linkage to previously loaded plugins
* build: fix ci errors
* feat: js module ordered teardown
* feat: js_gui_defer_free
* feat: basic hourglass view
* style: JS ASS (Argument Schema for Scripts)
* fix: js_event_loop mem leaks and lifetime problems
* fix: crashing test and pvs false positives
* feat: mjs custom obj destructors, gui submenu view
* refactor: yank js_gui_defer_free (yuck)
* refactor: maybe_unsubscribe
* empty_screen, docs, typing fix-ups
* docs: navigation event & demo
* feat: submenu setHeader
* feat: text_input
* feat: text_box
* docs: text_box availability
* ci: silence irrelevant pvs low priority warning
* style: use furistring
* style: _get_at -> _safe_get
* fix: built-in module name assignment
* feat: js_dialog; refactor, optimize: js_gui
* docs: js_gui
* ci: silence pvs warning: Memory allocation is infallible
* style: fix storage spelling
* feat: foreign pointer signature checks
* feat: js_storage
* docs: js_storage
* fix: my unit test was breaking other tests ;_;
* ci: fix ci?
* Make doxygen happy
* docs: flipper, math, notification, global
* style: review suggestions
* style: review fixups
* fix: badusb demo script
* docs: badusb
* ci: add nofl
* ci: make linter happy
* Bump api version

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
porta
2024-10-14 21:42:11 +03:00
committed by GitHub
parent 57c438d91a
commit 8a95cb8d6b
114 changed files with 4978 additions and 931 deletions

View File

@@ -41,16 +41,10 @@ print("string1", "string2", 123);
Same as `print`, but output to serial console only, with corresponding log level.
## to_string
Convert a number to string.
Convert a number to string with an optional base.
### Examples:
```js
to_string(123)
```
## to_hex_string
Convert a number to string(hex format).
### Examples:
```js
to_hex_string(0xFF)
to_string(123) // "123"
to_string(123, 16) // "0x7b"
```

View File

@@ -1,49 +0,0 @@
# js_dialog {#js_dialog}
# Dialog module
```js
let dialog = require("dialog");
```
# Methods
## message
Show a simple message dialog with header, text and "OK" button.
### Parameters
- Dialog header text
- Dialog text
### Returns
true if central button was pressed, false if the dialog was closed by back key press
### Examples:
```js
dialog.message("Dialog demo", "Press OK to start");
```
## custom
More complex dialog with configurable buttons
### Parameters
Configuration object with the following fields:
- header: Dialog header text
- text: Dialog text
- button_left: (optional) left button name
- button_right: (optional) right button name
- button_center: (optional) central button name
### Returns
Name of pressed button or empty string if the dialog was closed by back key press
### Examples:
```js
let dialog_params = ({
header: "Dialog header",
text: "Dialog text",
button_left: "Left",
button_right: "Right",
button_center: "OK"
});
dialog.custom(dialog_params);
```

View File

@@ -0,0 +1,144 @@
# js_event_loop {#js_event_loop}
# Event Loop module
```js
let eventLoop = require("event_loop");
```
The event loop is central to event-based programming in many frameworks, and our
JS subsystem is no exception. It is a good idea to familiarize yourself with the
event loop first before using any of the advanced modules (e.g. GPIO and GUI).
## Conceptualizing the event loop
If you ever wrote JavaScript before, you have definitely seen callbacks. It's
when a function accepts another function (usually an anonymous one) as one of
the arguments, which it will call later on, e.g. when an event happens or when
data becomes ready:
```js
setTimeout(function() { console.log("Hello, World!") }, 1000);
```
Many JavaScript engines employ a queue that the runtime fetches events from as
they occur, subsequently calling the corresponding callbacks. This is done in a
long-running loop, hence the name "event loop". Here's the pseudocode for a
typical event loop:
```js
while(loop_is_running()) {
if(event_available_in_queue()) {
let event = fetch_event_from_queue();
let callback = get_callback_associated_with(event);
if(callback)
callback(get_extra_data_for(event));
} else {
// avoid wasting CPU time
sleep_until_any_event_becomes_available();
}
}
```
Most JS runtimes enclose the event loop within themselves, so that most JS
programmers does not even need to be aware of its existence. This is not the
case with our JS subsystem.
# Example
This is how one would write something similar to the `setTimeout` example above:
```js
// import module
let eventLoop = require("event_loop");
// create an event source that will fire once 1 second after it has been created
let timer = eventLoop.timer("oneshot", 1000);
// subscribe a callback to the event source
eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) {
print("Hello, World!");
eventLoop.stop();
}, eventLoop); // notice this extra argument. we'll come back to this later
// run the loop until it is stopped
eventLoop.run();
// the previous line will only finish executing once `.stop()` is called, hence
// the following line will execute only after "Hello, World!" is printed
print("Stopped");
```
I promised you that we'll come back to the extra argument after the callback
function. Our JavaScript engine does not support closures (anonymous functions
that access values outside of their arguments), so we ask `subscribe` to pass an
outside value (namely, `eventLoop`) as an argument to the callback so that we
can access it. We can modify this extra state:
```js
// this timer will fire every second
let timer = eventLoop.timer("periodic", 1000);
eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) {
print("Counter is at:", counter);
if(counter === 10)
eventLoop.stop();
// modify the extra arguments that will be passed to us the next time
return [counter + 1, eventLoop];
}, 0, eventLoop);
```
Because we have two extra arguments, if we return anything other than an array
of length 2, the arguments will be kept as-is for the next call.
The first two arguments that get passed to our callback are:
- The subscription manager that lets us `.cancel()` our subscription
- The event item, used for events that have extra data. Timer events do not,
they just produce `undefined`.
# API reference
## `run`
Runs the event loop until it is stopped with `stop`.
## `subscribe`
Subscribes a function to an event.
### Parameters
- `contract`: an event source identifier
- `callback`: the function to call when the event happens
- extra arguments: will be passed as extra arguments to the callback
The callback will be called with at least two arguments, plus however many were
passed as extra arguments to `subscribe`. The first argument is the subscription
manager (the same one that `subscribe` itself returns). The second argument is
the event item for events that produce extra data; the ones that don't set this
to `undefined`. The callback may return an array of the same length as the count
of the extra arguments to modify them for the next time that the event handler
is called. Any other returns values are discarded.
### Returns
A `SubscriptionManager` object:
- `SubscriptionManager.cancel()`: unsubscribes the callback from the event
### Warning
Each event source may only have one callback associated with it.
## `stop`
Stops the event loop.
## `timer`
Produces an event source that fires with a constant interval either once or
indefinitely.
### Parameters
- `mode`: either `"oneshot"` or `"periodic"`
- `interval`: the timeout (for `"oneshot"`) timers or the period (for
`"periodic"` timers)
### Returns
A `Contract` object, as expected by `subscribe`'s first parameter.
## `queue`
Produces a queue that can be used to exchange messages.
### Parameters
- `length`: the maximum number of items that the queue may contain
### Returns
A `Queue` object:
- `Queue.send(message)`:
- `message`: a value of any type that will be placed at the end of the queue
- `input`: a `Contract` (event source) that pops items from the front of the
queue

View File

@@ -0,0 +1,77 @@
# js_gpio {#js_gpio}
# GPIO module
```js
let eventLoop = require("event_loop");
let gpio = require("gpio");
```
This module depends on the `event_loop` module, so it _must_ only be imported
after `event_loop` is imported.
# Example
```js
let eventLoop = require("event_loop");
let gpio = require("gpio");
let led = gpio.get("pc3");
led.init({ direction: "out", outMode: "push_pull" });
led.write(true);
delay(1000);
led.write(false);
delay(1000);
```
# API reference
## `get`
Gets a `Pin` object that can be used to manage a pin.
### Parameters
- `pin`: pin identifier (examples: `"pc3"`, `7`, `"pa6"`, `3`)
### Returns
A `Pin` object
## `Pin` object
### `Pin.init()`
Configures a pin
#### Parameters
- `mode`: `Mode` object:
- `direction` (required): either `"in"` or `"out"`
- `outMode` (required for `direction: "out"`): either `"open_drain"` or
`"push_pull"`
- `inMode` (required for `direction: "in"`): either `"analog"`,
`"plain_digital"`, `"interrupt"` or `"event"`
- `edge` (required for `inMode: "interrupt"` or `"event"`): either
`"rising"`, `"falling"` or `"both"`
- `pull` (optional): either `"up"`, `"down"` or unset
### `Pin.write()`
Writes a digital value to a pin configured with `direction: "out"`
#### Parameters
- `value`: boolean logic level to write
### `Pin.read()`
Reads a digital value from a pin configured with `direction: "in"` and any
`inMode` except `"analog"`
#### Returns
Boolean logic level
### `Pin.read_analog()`
Reads an analog voltage level in millivolts from a pin configured with
`direction: "in"` and `inMode: "analog"`
#### Returns
Voltage on pin in millivolts
### `Pin.interrupt()`
Attaches an interrupt to a pin configured with `direction: "in"` and
`inMode: "interrupt"` or `"event"`
#### Returns
An event loop `Contract` object that identifies the interrupt event source. The
event does not produce any extra data.

161
documentation/js/js_gui.md Normal file
View File

@@ -0,0 +1,161 @@
# js_gui {#js_gui}
# GUI module
```js
let eventLoop = require("event_loop");
let gui = require("gui");
```
This module depends on the `event_loop` module, so it _must_ only be imported
after `event_loop` is imported.
## Conceptualizing GUI
### Event loop
It is highly recommended to familiarize yourself with the event loop first
before doing GUI-related things.
### Canvas
The canvas is just a drawing area with no abstractions over it. Drawing on the
canvas directly (i.e. not through a viewport) is useful in case you want to
implement a custom design element, but this is rather uncommon.
### Viewport
A viewport is a window into a rectangular portion of the canvas. Applications
always access the canvas through a viewport.
### View
In Flipper's terminology, a "View" is a fullscreen design element that assumes
control over the entire viewport and all input events. Different types of views
are available (not all of which are unfortunately currently implemented in JS):
| View | Has JS adapter? |
|----------------------|------------------|
| `button_menu` | ❌ |
| `button_panel` | ❌ |
| `byte_input` | ❌ |
| `dialog_ex` | ✅ (as `dialog`) |
| `empty_screen` | ✅ |
| `file_browser` | ❌ |
| `loading` | ✅ |
| `menu` | ❌ |
| `number_input` | ❌ |
| `popup` | ❌ |
| `submenu` | ✅ |
| `text_box` | ✅ |
| `text_input` | ✅ |
| `variable_item_list` | ❌ |
| `widget` | ❌ |
In JS, each view has its own set of properties (or just "props"). The programmer
can manipulate these properties in two ways:
- Instantiate a `View` using the `makeWith(props)` method, passing an object
with the initial properties
- Call `set(name, value)` to modify a property of an existing `View`
### View Dispatcher
The view dispatcher holds references to all the views that an application needs
and switches between them as the application makes requests to do so.
### Scene Manager
The scene manager is an optional add-on to the view dispatcher that makes
managing applications with complex navigation flows easier. It is currently
inaccessible from JS.
### Approaches
In total, there are three different approaches that you may take when writing
a GUI application:
| Approach | Use cases | Available from JS |
|----------------|------------------------------------------------------------------------------|-------------------|
| ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ |
| ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ |
| SceneManager | Additional navigation flow management for complex applications | ❌ |
# Example
An example with three different views using the ViewDispatcher approach:
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let loadingView = require("gui/loading");
let submenuView = require("gui/submenu");
let emptyView = require("gui/empty_screen");
// Common pattern: declare all the views in an object. This is absolutely not
// required, but adds clarity to the script.
let views = {
// the view dispatcher auto-✨magically✨ remembers views as they are created
loading: loadingView.make(),
empty: emptyView.make(),
demos: submenuView.makeWith({
items: [
"Hourglass screen",
"Empty screen",
"Exit app",
],
}),
};
// go to different screens depending on what was selected
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
if (index === 0) {
gui.viewDispatcher.switchTo(views.loading);
} else if (index === 1) {
gui.viewDispatcher.switchTo(views.empty);
} else if (index === 2) {
eventLoop.stop();
}
}, gui, eventLoop, views);
// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();
```
# API reference
## `viewDispatcher`
The `viewDispatcher` constant holds the `ViewDispatcher` singleton.
### `viewDispatcher.switchTo(view)`
Switches to a view, giving it control over the display and input
#### Parameters
- `view`: the `View` to switch to
### `viewDispatcher.sendTo(direction)`
Sends the viewport that the dispatcher manages to the front of the stackup
(effectively making it visible), or to the back (effectively making it
invisible)
#### Parameters
- `direction`: either `"front"` or `"back"`
### `viewDispatcher.sendCustom(event)`
Sends a custom number to the `custom` event handler
#### Parameters
- `event`: number to send
### `viewDispatcher.custom`
An event loop `Contract` object that identifies the custom event source,
triggered by `ViewDispatcher.sendCustom(event)`
### `viewDispatcher.navigation`
An event loop `Contract` object that identifies the navigation event source,
triggered when the back key is pressed
## `ViewFactory`
When you import a module implementing a view, a `ViewFactory` is instantiated.
For example, in the example above, `loadingView`, `submenuView` and `emptyView`
are view factories.
### `ViewFactory.make()`
Creates an instance of a `View`
### `ViewFactory.make(props)`
Creates an instance of a `View` and assigns initial properties from `props`
#### Parameters
- `props`: simple key-value object, e.g. `{ header: "Header" }`

View File

@@ -0,0 +1,53 @@
# js_gui__dialog {#js_gui__dialog}
# Dialog GUI view
Displays a dialog with up to three options.
<img src="dialog.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let dialogView = require("gui/dialog");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
# View props
## `header`
Text that appears in bold at the top of the screen
Type: `string`
## `text`
Text that appears in the middle of the screen
Type: `string`
## `left`
Text for the left button. If unset, the left button does not show up.
Type: `string`
## `center`
Text for the center button. If unset, the center button does not show up.
Type: `string`
## `right`
Text for the right button. If unset, the right button does not show up.
Type: `string`
# View events
## `input`
Fires when the user presses on either of the three possible buttons. The item
contains one of the strings `"left"`, `"center"` or `"right"` depending on the
button.
Item type: `string`

View File

@@ -0,0 +1,22 @@
# js_gui__empty_screen {#js_gui__empty_screen}
# Empty Screen GUI View
Displays nothing.
<img src="empty.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let emptyView = require("gui/empty_screen");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the GUI example.
# View props
This view does not have any props.

View File

@@ -0,0 +1,23 @@
# js_gui__loading {#js_gui__loading}
# Loading GUI View
Displays an animated hourglass icon. Suppresses all `navigation` events, making
it impossible for the user to exit the view by pressing the back key.
<img src="loading.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let loadingView = require("gui/loading");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the GUI example.
# View props
This view does not have any props.

View File

@@ -0,0 +1,37 @@
# js_gui__submenu {#js_gui__submenu}
# Submenu GUI view
Displays a scrollable list of clickable textual entries.
<img src="submenu.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let submenuView = require("gui/submenu");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the GUI example.
# View props
## `header`
Single line of text that appears above the list
Type: `string`
## `items`
The list of options
Type: `string[]`
# View events
## `chosen`
Fires when an entry has been chosen by the user. The item contains the index of
the entry.
Item type: `number`

View File

@@ -0,0 +1,25 @@
# js_gui__text_box {#js_gui__text_box}
# Text box GUI view
Displays a scrollable read-only text field.
<img src="text_box.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let textBoxView = require("gui/text_box");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
# View props
## `text`
Text to show in the text box.
Type: `string`

View File

@@ -0,0 +1,44 @@
# js_gui__text_input {#js_gui__text_input}
# Text input GUI view
Displays a keyboard.
<img src="text_input.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let textInputView = require("gui/text_input");
```
This module depends on the `gui` module, which in turn depends on the
`event_loop` module, so they _must_ be imported in this order. It is also
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
# View props
## `minLength`
Smallest allowed text length
Type: `number`
## `maxLength`
Biggest allowed text length
Type: `number`
Default: `32`
## `header`
Single line of text that appears above the keyboard
Type: `string`
# View events
## `input`
Fires when the user selects the "save" button and the text matches the length
constrained by `minLength` and `maxLength`.
Item type: `string`

View File

@@ -1,48 +0,0 @@
# js_submenu {#js_submenu}
# Submenu module
```js
let submenu = require("submenu");
```
# Methods
## setHeader
Set the submenu header text.
### Parameters
- header (string): The submenu header text
### Example
```js
submenu.setHeader("Select an option:");
```
## addItem
Add a new submenu item.
### Parameters
- label (string): The submenu item label text
- id (number): The submenu item ID, must be a Uint32 number
### Example
```js
submenu.addItem("Option 1", 1);
submenu.addItem("Option 2", 2);
submenu.addItem("Option 3", 3);
```
## show
Show a submenu that was previously configured using `setHeader()` and `addItem()` methods.
### Returns
The ID of the submenu item that was selected, or `undefined` if the BACK button was pressed.
### Example
```js
let selected = submenu.show();
if (selected === undefined) {
// if BACK button was pressed
} else if (selected === 1) {
// if item with ID 1 was selected
}
```

View File

@@ -1,69 +0,0 @@
# js_textbox {#js_textbox}
# Textbox module
```js
let textbox = require("textbox");
```
# Methods
## setConfig
Set focus and font for the textbox.
### Parameters
- focus: "start" to focus on the beginning of the text, or "end" to focus on the end of the text
- font: "text" to use the default proportional font, or "hex" to use a monospaced font, which is convenient for aligned array output in HEX
### Example
```js
textbox.setConfig("start", "text");
textbox.addText("Hello world");
textbox.show();
```
## addText
Add text to the end of the textbox.
### Parameters
- text (string): The text to add to the end of the textbox
### Example
```js
textbox.addText("New text 1\nNew text 2");
```
## clearText
Clear the textbox.
### Example
```js
textbox.clearText();
```
## isOpen
Return true if the textbox is open.
### Returns
True if the textbox is open, false otherwise.
### Example
```js
let isOpen = textbox.isOpen();
```
## show
Show the textbox. You can add text to it using the `addText()` method before or after calling the `show()` method.
### Example
```js
textbox.show();
```
## close
Close the textbox.
### Example
```js
if (textbox.isOpen()) {
textbox.close();
}
```