diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..71ddcee --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: c18b54d0390621a1f11e87e80b29ed32 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/_images/summary.png b/_images/summary.png new file mode 100644 index 0000000..83e549f Binary files /dev/null and b/_images/summary.png differ diff --git a/_sources/api.rst.txt b/_sources/api.rst.txt new file mode 100644 index 0000000..f06b0e1 --- /dev/null +++ b/_sources/api.rst.txt @@ -0,0 +1,98 @@ + +RDFM Server API Reference +------------------------- + +API Authentication +~~~~~~~~~~~~~~~~~~ + +By default, the RDFM server expects all API requests to be authenticated. +Depending on the type of the API, this can be either: + +* Device Token +* Management Token + +In either case, the server expects the token to be passed as part of the request, in the HTTP Authorization header. +An example authenticated request is shown below: + +.. sourcecode:: http + + GET /api/v1/groups HTTP/1.1 + Host: rdfm-server:5000 + User-Agent: python-requests/2.31.0 + Accept-Encoding: gzip, deflate + Accept: */* + Connection: keep-alive + Authorization: Bearer token=eyJhbGciOiJSUzI1NiIsInR5cC<...truncated...>RpPonb7-IAsk89YpGayxg + +Any request that was not successfully authenticated (because of a missing or otherwise invalid token) will return the 401 Unauthorized status code. +Additionally, in the case of management tokens, if the given token does not provide sufficient access to the requested resource, the request will be rejected with a 403 Forbidden status code. +This can happen if the token does not claim all scopes required by the target endpoint (for example: trying to upload a package using a read-only token). + +Error Handling +~~~~~~~~~~~~~~ + +Should an error occur during the handling of an API request, either because of incorrect request data or other endpoint-specific scenarios, the server will return an error structure containing a user-friendly description of the error. +An example error response is shown below: + +.. sourcecode:: json + + { + "error": "delete failed, the package is assigned to at least one group" + } + + +Packages API +~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.packages + :undoc-static: + :order: path + +Group API +~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v2.groups + :undoc-static: + :order: path + +Group API (legacy) +~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.groups + :undoc-static: + :order: path + +Update API +~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.update + :undoc-static: + :order: path + +Device Management API +~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v2.devices + :undoc-static: + :order: path + +Device Management API (legacy) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.devices + :undoc-static: + :order: path + +Device Authorization API +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.auth + :undoc-static: + :order: path diff --git a/_sources/index.md.txt b/_sources/index.md.txt new file mode 100644 index 0000000..d04a157 --- /dev/null +++ b/_sources/index.md.txt @@ -0,0 +1,18 @@ +# {{project}} + +```{toctree} +:maxdepth: 2 + +introduction +system_overview +rdfm_linux_device_client +rdfm_android_device_client +rdfm_mcumgr_device_client +rdfm_artifact +rdfm_manager +rdfm_mgmt_server +rdfm_ota_manual +server_operation +rdfm_frontend +api +``` diff --git a/_sources/introduction.md.txt b/_sources/introduction.md.txt new file mode 100644 index 0000000..5bbba02 --- /dev/null +++ b/_sources/introduction.md.txt @@ -0,0 +1,16 @@ +# Introduction + +RDFM - Remote Device Fleet Manager - is an open-source ecosystem of tools that enable Over-The-Air (OTA) update delivery and fleet management for systems of embedded devices. + +This manual describes the main components of RDFM. It is divided into the following chapters: + +- System Architecture - a short overview of the system architecture, and how each component of the system interacts with the other +- RDFM Linux Device Client - build instructions and manual for the Linux RDFM Client, used for installing updates on a device +- RDFM Android Device Client - integration guide and user manual for the RDFM Android Client/app used for providing OTA updates via RDFM on embedded Android devices +- RDFM MCUmgr Device Client - build instructions and manual for the RDFM MCUmgr Client app, used for providing updates via RDFM on embedded devices running ZephyrRTOS +- RDFM Artifact utility - instruction manual for the `rdfm-artifact` utility used for generating update packages for use with the RDFM Linux device client +- RDFM Manager utility - instruction manual for the `rdfm-mgmt` utility, which allows management of devices connected to the RDFM server +- RDFM Management Server - build instructions and deployment manual for the RDFM Management Server +- RDFM Server API Reference - comprehensive reference of the HTTP APIs exposed by the RDFM Management Server +- RDFM OTA Manual - introduces key concepts of the RDFM OTA system and explains it's basic operation principles +- RDFM Frontend - build instructions for the RDFM Frontend application diff --git a/_sources/rdfm_android_device_client.md.txt b/_sources/rdfm_android_device_client.md.txt new file mode 100644 index 0000000..5392ffd --- /dev/null +++ b/_sources/rdfm_android_device_client.md.txt @@ -0,0 +1,172 @@ +# RDFM Android Device Client + +## Introduction + +The RDFM Android Device Client allows for integrating an Android-based device with the RDFM server. +Currently, only OTA update functionality is implemented. + +## Integrating the app + +This app is **not meant to be built separately** (i.e in Android Studio), but as part of the source tree for an existing device. +The app integrates with the Android UpdateEngine to perform the actual update installation, which requires it to be a system app. +Some configuration is required to the existing system sources. + +### Copying the sources + +First, copy the sources of the app to the root directory of the AOSP source tree. +After cloning this repository, run the following: +``` +mkdir -v -p /vendor/antmicro/rdfm +cd devices/android-client/ +cp -r app/src/main/* /vendor/antmicro/rdfm +``` + +### Configuring the device Makefile + +The [product Makefile](https://source.android.com/docs/setup/create/new-device#build-a-product) must be configured to build the RDFM app into the system image. +To do this, add `rdfm` to the `PRODUCT_PACKAGES` variable in the target device Makefile: +``` +PRODUCT_PACKAGES += rdfm +``` + +### Building the app + +Afterwards, [the usual Android build procedure](https://source.android.com/docs/setup/build/building) can be used to build just the app. +From an already configured build environment, run: +``` +mma rdfm +``` +The resulting signed APK is in `out/target/product//system/app/rdfm/rdfm.apk`. + +### Using HTTPS for server requests + +The default Android system CA certificates are used when validating the certificate presented by the server. +If the RDFM server that is configured in the app uses a certificate that is signed by a custom Certificate Authority, the CA certificate must be added to the system roots. + +## System versioning + +The app performs update check requests to the configured RDFM server. +The build version and device type are retrieved from the system properties: +- `ro.build.version.incremental` - the current software version (matches `rdfm.software.version`) +- `ro.build.product` - device type (matches `rdfm.hardware.devtype`) + +When uploading an OTA package to the RDFM server, currently these values must be **manually** extracted from the update package, and passed as arguments to `rdfm-mgmt`: +``` +rdfm-mgmt packages upload --path ota.zip --version --device +``` + +You can extract the values from the [package metadata file](https://source.android.com/docs/core/ota/tools#ota-package-metadata) by unzipping the OTA package. + +## Configuring the app + +The application will automatically start on system boot. +Available configuration options are shown below. + +### Build-time app configuration + +The default build-time configuration can be modified by providing a custom `conf.xml` file in the `app/src/main/res/values/` folder, similar to the one shown below: + +```xml + + + + +``` + +This build-time configuration is applied **only once, at first startup of the app**, as the main use case for this is first-time configuration for newly provisioned devices. +Modifying it afterwards (for example, via an update containing a new version of the RDFM app) will not result in the change of existing configuration. + +### Runtime app configuration + +It is possible to change the app's configuration at runtime by simply starting the RDFM app from the drawer and selecting `Settings` from the context menu. + +### Configuration options + +The following configuration options are available: +- RDFM server URL (`http`/`https` scheme) +- Update check interval (in seconds) +- Maximum amount of concurrent shell sessions (set to `0` to disable reverse shell functionality) + +## Available intents + +### Update check intent + +This intent allows an external app to force perform an update check outside of the usual automatic update check interval. +To do this, the app that wants to perform the update check must have the `com.antmicro.update.rdfm.permission.UPDATE_CHECK` permission defined in its `AndroidManifest.xml` file: + +```xml + +``` + +Afterwards, an update check can then be forced like so: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.startUpdate"); +mContext.sendBroadcast(configIntent); +``` + +### External configuration via intents + +The app settings can also be configured via intents, for example in order to change between different deployment environments. +To do this, the app that performs the configuring step must have the `com.antmicro.update.rdfm.permission.CONFIGURATION` permission defined in its `AndroidManifest.xml` file: +```xml + +``` + +To configure the app, use the `com.antmicro.update.rdfm.configurationSet` intent and set extra values on the intent to the settings you wish to change. +For example, to set the server address: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationSet"); +configIntent.putExtra("ota_server_address", "http://CUSTOM-OTA-ADDRESS/"); +mContext.sendBroadcast(configIntent); +``` + +The supported configuration key names can be found in the `res/values/strings.xml` file with the `preference_` prefix. + +Aside from setting the configuration, you can also fetch the current configuration of the app: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationGet"); +mContext.sendBroadcast(configIntent); + +// Now listen for `com.antmicro.update.rdfm.configurationResponse` broadcast intent +// The intent's extras bundle will contain the configuration keys and values +``` + +## Development + +The provided Gradle files can be used for development purposes, simply open the `devices/android-client` directory in Android Studio. +Missing references to the `UpdateEngine` class are expected, but they do not prevent regular use of the IDE. + +Do note however that **the app is not buildable from Android Studio**, as it requires integration with the aforementioned system API. +To test the app, an existing system source tree must be used. +Copy the modified sources to the AOSP tree, and re-run the [application build](#building-the-app). +The modified APK can then be uploaded to the device via ADB by running: +``` +adb install +``` + +### Restarting the app + +With the target device connected via ADB, run: +``` +adb shell am force-stop com.antmicro.update.rdfm +adb shell am start -n com.antmicro.update.rdfm/.MainActivity +``` + +### Fetching app logs + +To view the application logs, run: +``` +adb logcat --pid=`adb shell pidof -s com.antmicro.update.rdfm` +``` diff --git a/_sources/rdfm_artifact.md.txt b/_sources/rdfm_artifact.md.txt new file mode 100644 index 0000000..964c7e0 --- /dev/null +++ b/_sources/rdfm_artifact.md.txt @@ -0,0 +1,150 @@ +# RDFM Artifact utility + +## Introduction + +The RDFM Artifact tool (`rdfm-artifact`) allows for easy creation and modification of RDFM Linux client-compatible artifacts containing rootfs partition images. +A basic RDFM artifact consists of a rootfs image, as well as its checksum, metadata and compatibility with certain device types. + +Additionally, `rdfm-artifact` allows for the generation of delta updates, which contain only the differences between two versions of an artifact rather than the entire artifact itself. +This can be useful for reducing the size of updates and improving the efficiency of the deployment process. + +`rdfm-artifact` can also be used for generation of Zephyr MCUboot artifacts, which allows for updating embedded devices running Zephyr. +Additionally, multiple Zephyr images can be combined into one grouped artifact to allow multiple boards to act as one logical device. + +Single file updates are also supported. +This option allows for creating, or updating specific files on the device, without the need to update the whole partition. + +## Getting started + +In order to support robust updates and rollback, the RDFM Client requires proper partition layout and a bootloader that supports A/B update scheme. To make it easy to integrate the RDFM Client into your Yocto image-building project, it's recommended to use the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer when building the BSPs. + +## Building from source + +### Requirements + +* Go compiler +* C Compiler +* liblzma-dev and libglib2.0-dev packages + +### Steps + +To build `rdfm-artifact` on a device from source, clone the repository and build the binary using `make`: + +``` +git clone https://github.com/antmicro/rdfm.git && cd tools/rdfm-artifact/ +make +``` + +## Basic usage + +The basic functionality of writing an artifact is available with the `write` subcommand: + +``` +NAME: + rdfm-artifact write - Allows creation of RDFM-compatible artifacts + +USAGE: + rdfm-artifact write command [command options] [arguments...] + +COMMANDS: + rootfs-image Create a full rootfs image artifact + delta-rootfs-image Create a delta rootfs artifact + zephyr-image Create a full Zephyr MCUboot image artifact + zephyr-group-image Create a Zephyr MCUboot group image artifact + single-file Create a single file artifact + +OPTIONS: + --help, -h show help +``` + +### Creating a full-rootfs artifact + +For example, to create a simple rootfs artifact for a given system image: + +``` +rdfm-artifact write rootfs-image \ + --file "my-rootfs-image.img" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" +``` + +### Creating a delta rootfs artifact + +For creating a delta artifact, you should have already created two separate full-rootfs artifacts: + +- base artifact - the rootfs image that the deltas will be applied on top of, or in other words: the currently running rootfs on the device +- target artifact - the updated rootfs image that will be installed on the device + +Given these two artifacts, a delta artifact can be generated like this: + +``` +rdfm-artifact write delta-rootfs-image \ + --base-artifact "base.rdfm" \ + --target-artifact "target.rdfm" \ + --output-path "base-to-target.rdfm" +``` + +### Creating a Zephyr MCUboot artifact + +To create a Zephyr MCUboot artifact, you'll have to have already created a Zephyr image with MCUboot support enabled. +You should use the signed bin image (by default `zephyr.signed.bin`). +Artifact version will be extracted from provided image. + +With this image, you can generate an artifact like so: + +``` +rdfm-artifact write zephyr-image \ + --file "my-zephyr-image.signed.bin" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" +``` + +### Creating a Zephyr MCUboot group artifact + +To create a grouped Zephyr MCUboot artifact, you should have already created at least two Zephyr images with MCUboot support enabled. +The version of individual images in a grouped artifact must be identical. + +Given images `one.bin` and `two.bin` for group targets `one` and `two` respectively, an artifact can be generated with: + +``` +rdfm-artifact write zephyr-group-image \ + --group-type "my-group" \ + --target "one:one.bin" \ + --target "two:two.bin" \ + --ouptput-path "path-to-output.rdfm" +``` + +:::{note} +It's possible to create a grouped artifact with just one image, +however in cases like that you should create simple [zephyr-image](#creating-a-zephyr-mcuboot-artifact) instead. +::: + +### Creating a single file artifact + +Apart from updating a whole partition, it's also possible to update a single file on the device. +The usage is the same as for rootfs artifacts, but with the `single-file` subcommand and two new options: + +- `--dest-dir` - the destination directory on the device where the file should be placed +- `--rollback-support` - (optional) determines, whether a backup of the file should be created for rollback purposes. + The backup file is stored in the same directory as the original file, with the `.tmp` extension added to the name. + By default, the rollback support is disabled. + +``` +rdfm-artifact write single-file \ + --file "my-file.txt" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" \ + --dest-dir "/destination/device/directory" \ + --rollback-support +``` + +## Running tests + +To run `rdfm-artifact` tests, use the `test` Makefile target: + +``` +make test +``` diff --git a/_sources/rdfm_frontend.md.txt b/_sources/rdfm_frontend.md.txt new file mode 100644 index 0000000..1f15422 --- /dev/null +++ b/_sources/rdfm_frontend.md.txt @@ -0,0 +1,81 @@ +# RDFM Frontend + +## Introduction + +Repository contains code for a frontend application that is able to render data from and communicate with `rdfm-server` through HTTP requests. + +The application uses HTTP Polling to dynamically detect any changes in the data and update the UI accordingly, so multiple users can use the application simultaneously (as well as the `rdfm-mgmt` tool). + + +To use the frontend application, make sure that `rdfm-server` is up and running. +Details on how to run it can be found in [RDFM Management Server](./rdfm_mgmt_server.md). +To be able to send requests to `rdfm-server` its URL has to be defined in the `.env` file using `VITE_SERVER_URL` key. + +```{warning} +If no authentication is used in the frontend application make sure that the `RDFM_DISABLE_ENCRYPTION` and `RDFM_DISABLE_API_AUTH` values are set to `1`. +``` + +Before running any of the commands, make sure that you have `npm` installed. + +## Building the application + +To install dependencies and build the application for production run the following commands in the root directory of the project: + +```bash +npm install +npm run build +``` + +The built static files are located in the `dist` directory. +The frontend can be started alongside the RDFM API in the same [Docker image](rdfm_mgmt_server.md#setting-up-a-dockerized-development-environment). +The following changes must be applied: + +- `VITE_RDFM_BACKEND` in the `.env` file to `'true'`. +- `VITE_SERVER_URL` in the `.env` file to the URL of the backend server. +- `RDFM_INCLUDE_FRONTEND_ENDPOINT` in the docker-compose configuration. As a consequence, the frontend application will be served on `/api/static/frontend` endpoint once the HTTP server is started. + +The frontend may also be deployed independently of the RDFM API. +The following configuration settings must then be set: + +- `VITE_RDFM_BACKEND` in the `.env` file to `'false'`. +- `VITE_SERVER_URL` in the `.env` file to the URL of the backend server. +- `RDFM_ENABLE_CORS` in the docker-compose configuration to `1` to enable CORS requests. +- `RDFM_FRONTEND_APP_URL` in the docker-compose configuration to the URL of the frontend application server, as it is used for redirects. + +```{warning} +`RDFM_ENABLE_CORS` variable should not be set in production environment, as it allows for cross-origin requests. +``` + +## Running development server + +When developing the application it is recommended to use the `vite` development server, as features like Hot Module Replacement is enabled. +To install dependencies and start the development server run the following commands in the root directory of the project: + +```bash +npm install +npm run dev +``` + +To communicate with `rdfm-server` when using the development server, make sure to set all variables as described in the [Building](#building-the-application) section in the same as it is done for a separate server deployment. + + +## Configuration + +The frontend application can be configured using an `.env` file. +That file contains variables that can be set to change the behavior of the application. +Below there is a description of all available variables. + +* `VITE_SERVER_URL` - RDFM server URL +* `VITE_RDFM_BACKEND` - Indicates if the backend hosts the frontend application +* `VITE_LOGIN_URL` - OIDC login URL +* `VITE_LOGOUT_URL` - OIDC logout URL +* `VITE_OAUTH2_CLIENT` - OAUTH2 Client ID + +## Formatting + +To format the code using `prettier` run the following command: + +```bash +npm install +npm run format +``` diff --git a/_sources/rdfm_linux_device_client.md.txt b/_sources/rdfm_linux_device_client.md.txt new file mode 100644 index 0000000..61186a9 --- /dev/null +++ b/_sources/rdfm_linux_device_client.md.txt @@ -0,0 +1,190 @@ +# RDFM Linux Device Client + +## Introduction + +The RDFM Linux Device Client (`rdfm-client`) integrates an embedded Linux device with the RDFM Server. +This allows for performing robust Over-The-Air (OTA) updates of the running system and remote management of the device. + +`rdfm-client` runs on the target Linux device and handles the process of checking for updates in the background along with maintaining a connection to the RDFM Management Server. + +## Getting started + +In order to support robust updates and rollback, the RDFM Client requires proper partition layout and integration with the U-Boot bootloader. To make it easy to integrate the RDFM Client into your Yocto image-building project, it's recommended to use the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer when building the BSPs. + +## Installing from source + +### Requirements + +* C compiler +* Go compiler +* liblzma-dev, libssl-dev and libglib2.0-dev packages + +### Steps + +To install an RDFM client on a device from source, first clone the repository and build the binary: +``` +git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/ +make +``` + +Then run the install command: +``` +make install +``` + +### Installation notes + +Installing `rdfm` this way does not offer a complete system updater. +System updates require additional integration with the platform's bootloader and a dual-root partition setup for robust updates. +For this, it's recommended to build complete BSPs containing `rdfm` using the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer. + +## Building using Docker + +All build dependencies for compiling the RDFM Client are included in a dedicated Dockerfile. To build a development container image, you can use: + +``` +git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/ +sudo docker build -t rdfmbuilder . +``` + +This will create a Docker image that can be later used to compile the RDFM binary: + +``` +sudo docker run --rm -v :/data -it rdfmbuilder +cd data/devices/linux-client +make +``` + +## Configuring the client + +### RDFM default config + +The main config file contents are located in `/etc/rdfm/rdfm.conf`. It's JSON formatted and with the following keys of interest: + +#### RootfsPartA `string` + +Partition A for the A/B updating scheme. + +#### RootfsPartB `string` + +Partition B for the A/B updating scheme. + +### RDFM overlay config + +The file `/var/lib/rdfm/rdfm.conf` defines the high-level RDFM client configurations. They are overlaid over the configuration located in `/etc/rdfm/rdfm.conf` during client startup. + +#### DeviceTypeFile `string` + +Path to the device type file. + +#### UpdatePollIntervalSeconds `int` + +Poll interval for checking for new updates. + +#### RetryPollIntervalSeconds `int` + +Maximum number of seconds between each retry when authorizing. + +#### ServerCertificate `string` + +Path to a server SSL certificate. + +#### ServerURL `string` + +Management server URL. + +#### HttpCacheEnabled `bool` + +Describing if artifact caching is enabled. True by default. + +#### ReconnectRetryCount `int` + +HTTP reconnect retry count. + +#### ReconnectRetryTime `int` + +HTTP reconnect retry time. + +#### TelemetryEnable `bool` + +Describing if telemetry is enabled. False by default. + +#### TelemetryBatchSize `int` + +Number of log entries to be sent to a management server at a time. Fifty by default. + +### RDFM telemetry config + +The JSON structured `loggers.conf` file, laying under `/etc/rdfm/`, serves as a configuration file that defines a set of loggers to be executed once the client establishes a connection to the RDFM management server. Each logger can be any executable binary, which will be invoked by the client at predefined intervals. The client captures and processes the output generated by these loggers, providing a flexible mechanism for collecting and reporting system or application data during runtime. + +The `loggers.json` file contains an array of dictionaries, each of which describes a logger. + +Consider the following example: + +```json +[ + { + "name": "current date", + "path": "date", + "args": ["--rfc-email"], + "tick": 1000 + } +] +``` + +:::{note} Since the file gives the capacity to run arbitrary binaries, its permissions should be set to `-rw-r--r--`. +::: + +#### name `string` + +Denotes the name of the logger, each one should have a unique name. Loggers lower in the file will overwrite their counterparts that are above them. + +#### path `string` + +A path to an executable to be ran. + +#### args `[]string` + +A list of arguments for the given executable. + +#### tick `int` + +Number of milliseconds between each time a logger is ran. In the case of a logger taking more than `tick` to execute, it is killed and the client reports a timeout error. + +## Testing server-device integration with a demo Linux device client + +For development purposes, it's often necessary to test server integration with an existing device client. +To do this, it is possible to use the [RDFM Linux device client](rdfm_linux_device_client.md), without having to build a compatible system image utilizing the Yocto [meta-rdfm layer](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm). +First, build the demo container image: + +``` +cd devices/linux-client/ +make docker-demo-client +``` + +You can then start a demo Linux client by running the following: +``` +docker-compose -f docker-compose.demo.yml up +``` + +If required, the following environment variables can be changed in the above `docker-compose.demo.yml` file: + +- `RDFM_CLIENT_SERVER_URL` - URL to the RDFM Management Server, defaults to `http://127.0.0.1:5000/`. +- `RDFM_CLIENT_SERVER_CERT` **(optional)** - path (within the container) to the CA certificate to use for verification of the connection to the RDFM server. When this variable is set, the server URL must also be updated to use HTTPS instead of HTTP. +- `RDFM_CLIENT_DEVTYPE` - device type that will be advertised to the RDFM server; used for determining package compatibility, defaults to `x86_64`. +- `RDFM_CLIENT_PART_A`, `RDFM_CLIENT_PART_B` **(optional)** - specifies path (within the container) to the rootfs A/B partitions that updates will be installed to. They do not need to be specified for basic integration testing; any updates that are installed will go to `/dev/zero` by default. + +The demo client will automatically connect to the specified RDFM server and fetch any available packages. +To manage the device and update deployment, you can use the [RDFM Manager utility](rdfm_manager.md). + +## Developer Guide + +### Running tests + +Use the `test` make target to run the unit tests: + +``` +make test +``` + +Additionally, run the scripts available in the `scripts/test-docker` directory. These scripts test basic functionality of the RDFM client. diff --git a/_sources/rdfm_manager.md.txt b/_sources/rdfm_manager.md.txt new file mode 100644 index 0000000..f2663b7 --- /dev/null +++ b/_sources/rdfm_manager.md.txt @@ -0,0 +1,201 @@ +# RDFM Manager utility + +## Introduction + +The RDFM Manager (`rdfm-mgmt`) utility allows authorized users to manage resources exposed by the RDFM Management Server. + +## Installation + +Before proceeding, make sure that you have installed Python (at least version 3.11) and the `pipx` utility: +- **Debian (Bookworm)** - run `sudo apt update && sudo apt install pipx` +- **Arch** - `sudo pacman -S python-pipx` + +The prefered mode of installation for `rdfm-mgmt` is via `pipx`. +To install `rdfm-mgmt`, you must first clone the RDFM repository: + +``` +git clone https://github.com/antmicro/rdfm.git +cd rdfm/ +``` + +Afterwards, run the following commands: + +``` +cd manager/ +pipx install . +``` + +This will install the `rdfm-mgmt` utility and its dependencies for the current user within a virtual environment located at `/home//.local/pipx/venv`. +The `rdfm-mgmt` executable will be placed in `/home//.local/bin/` and should be immediately accessible from the shell. +Depending on the current system configuration, adding the above directory to the `PATH` may be required. + +## Configuration + +Additional RDFM Manager configuration is stored in the current user's `$HOME` directory, in the `$HOME/.config/rdfm-mgmt/config.json` file. +By default, RDFM Manager will add authentication data to all requests made to the RDFM server, which requires configuration of an authorization server and client credentials for use with the OAuth2 `Client Credentials` flow. +If authentication was disabled on the server-side, you can disable it in the manager as well by passing the `--no-api-auth` CLI flag like so: + +``` +rdfm-mgmt --no-api-auth groups list +``` + +An example configuration file is shown below. +In this case, the [Keycloak authorization server](https://www.keycloak.org/) was used: + +```json +{ + "auth_url": "http://keycloak:8080/realms/master/protocol/openid-connect/token", + "client_id": "rdfm-client", + "client_secret": "RDSwDyUMOT7UXxMqMmq2Y4vQ1ezxqobi" +} +``` + +Explanation of each required configuration field is shown below: +- `auth_url` - URL to the authorization server's [token endpoint](https://swagger.io/docs/specification/authentication/openid-connect-discovery/) +- `client_id` - Client ID to use for authentication using OAuth2 Client Credentials flow +- `client_secret` - Client secret to use for authentication using OAuth2 Client Credentials flow + +:::{note} +If you're also setting up the server, please note that the above client credentials are **NOT** the same as the server's Token Introspection credentials. +Each user of ``rdfm-mgmt`` should receive different credentials and be assigned scopes based on their allowed access level. +::: + + +## Building the wheel + +For installation instructions, see the [Installation section](#installation). +Building the wheel is not required in this case. + +To build the `rdfm-mgmt` wheel, you must have Python 3 installed, along with the `Poetry` dependency manager. + +Building the wheel can be done as follows: + +``` +cd manager/ +poetry build +``` + +## Usage + +For more detailed information, see the help messages associated with each subcommand: + +``` +$ rdfm-mgmt -h +usage: rdfm-mgmt + +RDFM Manager utility + +options: + -h, --help show this help message and exit + --url URL URL to the RDFM Management Server (default: http://127.0.0.1:5000/) + --cert CERT path to the server CA certificate used for establishing an HTTPS connection (default: ./certs/CA.crt) + --no-api-auth disable OAuth2 authentication for API requests (default: False) + +available commands: + {devices,packages,groups} + devices device management + packages package management + groups group management +``` + +### Listing available resources + +Listing devices: + +``` +rdfm-mgmt devices list +``` + +Listing registration requests: + +``` +rdfm-mgmt devices pending +``` + + +Listing packages: + +``` +rdfm-mgmt packages list +``` + +Listing groups: + +``` +rdfm-mgmt groups list +``` + +### Uploading packages + +``` +rdfm-mgmt packages upload \ + --path file.img \ + --version "v0" \ + --device "x86_64" +``` + +### Deleting packages + +``` +rdfm-mgmt packages delete --package-id +``` + +### Creating groups + +``` +rdfm-mgmt groups create --name "Group #1" --description "A very long description of the group" +``` + +### Deleting groups + +``` +rdfm-mgmt groups delete --group-id +``` + +### Assign package to a group + +Assigning one package: + +``` +rdfm-mgmt groups assign-package --group-id --package-id +``` + +Assigning many packages: + +``` +rdfm-mgmt groups assign-package --group-id --package-id --package-id +``` + +Clearing package assignments: + +``` +rdfm-mgmt groups assign-package --group-id +``` + +### Assign devices to a group + +Adding devices: + +``` +rdfm-mgmt groups modify-devices --group-id --add +``` + +Removing devices: + +``` +rdfm-mgmt groups modify-devices --group-id --remove +``` + +### Setting a group's target version + +``` +rdfm-mgmt groups target-version --group-id --version +``` + +### Authorizing a device + +``` +rdfm-mgmt devices auth +``` + +You can then select the registration for this device to authorize. diff --git a/_sources/rdfm_mcumgr_device_client.md.txt b/_sources/rdfm_mcumgr_device_client.md.txt new file mode 100644 index 0000000..8faee03 --- /dev/null +++ b/_sources/rdfm_mcumgr_device_client.md.txt @@ -0,0 +1,475 @@ +# RDFM MCUmgr Device Client + +## Introduction + +The RDFM MCUmgr Device Client (`rdfm-mcumgr-client`) allows for integrating an embedded device running ZephyrRTOS with the RDFM server via its MCUmgr SMP server implementation. +Currently, only the update functionality is implemented with support for serial, UDP and BLE transports. + +`rdfm-mcumgr-client` runs on a proxy device that's connected to the targets via one of the supported transports that handles the process of checking for updates, fetching update artifacts and pushing update images down to correct targets. + +## Getting started + +In order to properly function, both the Zephyr application and the `rdfm-mcumgr-client` have to be correctly configured in order for the update functionality to work. +Specifically: +* Zephyr applications must be built with MCUmgr support, with any transport method of your choice and with image management and reboot command groups enabled. +* The device running Zephyr must be connected to a proxy device running `rdfm-mcumgr-client` as the updates are coming from it. +* For reliable updates, the SMP server must be running alongside your application and be accessible at all times. + + +## Building client from source + +### Requirements + +* C compiler +* Go compiler (1.22+) +* liblzma-dev and libssl-dev packages + +### Steps + +To install the proxy client from source, first clone the repository and build the binary: +```sh +git clone https://github.com/antmicro/rdfm.git +cd rdfm/devices/mcumgr-client/ +make +``` + +Then run the install command: +```sh +make install +``` + +## Setting up target device + +### Setting up the bootloader + +To allow rollbacks and update verification, the MCUboot bootloader is used. +Images uploaded by `rdfm-mcumgr-client` are written to a secondary flash partition, while leaving the primary (currently running) image intact. +During update, the images are swapped by the bootloader. +If the update was successful, the new image is permanently set as the primary one, otherwise the images are swapped back to restore the previous version. +For more details on MCUboot, you can read the [official guide](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/readme-zephyr.html#building-and-using-mcuboot-with-zephyr) from MCUboot's website. + +#### Generating image signing key + +In order to enable updates, MCUboot requires all images to be signed. +During update, the bootloader will first validate the image using this key. + +MCUboot provides `imgtool.py` image tool script which can be used to generate appropriate signing key. +Below are the steps needed to generate a new key using this tool: + +Install additional packages required by the tool (replace `~/zephyrproject` with path to your Zephyr workspace): +```sh +cd ~/zephyrproject/bootloader/mcuboot +pip3 install --user -r ./scripts/requirements.txt +``` + +Generate new key: +```sh +cd ~/zephyrproject/bootloader/mcuboot/scripts +./imgtool.py keygen -k -t +``` +MCUboot currently supports `rsa-2048`, `rsa-3072`, `ecdsa-p256` or `ed25519` key types. +For more details on the image tool, please refer to its [official documentation](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/imgtool.html). + +#### Building the bootloader + +Besides the signing key, MCUboot also requires that the target board has specific flash partitions defined in its devicetree. +These partitions are: +* `boot_partition`: for MCUboot itself +* `slot0_partition`: the priamry slot of image 0 +* `slot1_partition`: the secondary slot of image 0 + +If you choose the *swap-using-scratch* update algorithm, one more partition has to be defined: +* `scratch_partition`: the scratch slot + +You can check whether your board has those partitions predefined by looking at its devicetree file (`boards///.dts`). +Look for `fixed-partitions` compatible entry. If your default board configuration doesn't specify those partitions (or you would like to modify them), +you can either modify the devicetree file directly or use [devicetree overlays](https://docs.zephyrproject.org/latest/build/dts/howtos.html#set-devicetree-overlays). + +Sample overlay file for the `stm32f746g_disco` board: + +```dts +#include + +/delete-node/ &quadspi; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 DT_SIZE_K(64)>; + }; + + slot0_partition: partition@40000 { + label = "image-0"; + reg = <0x00040000 DT_SIZE_K(256)>; + }; + + slot1_partition: partition@80000 { + label = "image-1"; + reg = <0x00080000 DT_SIZE_K(256)>; + }; + + scratch_partition: partition@c0000 { + label = "scratch"; + reg = <0x000c0000 DT_SIZE_K(256)>; + }; + }; +}; + +/ { + aliases { + /delete-property/ spi-flash0; + }; + + chosen { + zephyr,flash = &flash0; + zephyr,flash-controller = &flash; + zephyr,boot-partition = &boot_partition; + zephyr,code-partition = &slot0_partition; + }; +}; +``` + +:::{note} +If you do use devicetree overlay, make sure to add `app.overlay` as the last overlay file +since it's needed to correctly store the MCUboot image in `boot_partition`. +::: + +Besides the devicetree, you also have to specify: +* `BOOT_SIGNATURE_KEY_FILE`: path to the previously generate signing key +* `BOOT_SIGNATURE_TYPE`: signing key type: + * `BOOT_SIGNATURE_TYPE_RSA` and `BOOT_SIGNATURE_TYPE_RSA_LEN` + * `BOOT_SIGNATURE_TYPE_ECDSA_P256` + * `BOOT_SIGNATURE_TYPE_ED25519` +* `BOOT_IMAGE_UPGRADE_MODE`: the [update algorithm](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/design.html#image-slots) used for swapping images in primary and secondary slots: + * `BOOT_SWAP_USING_MOVE` + * `BOOT_SWAP_USING_SCRATCH` + +For example, if you wanted to build the bootloader for the `stm32f746g_disco` board with partitions defined in `stm32_disco.overlay`, +using *swap-using-scratch* update algorithm and using `rsa-2048` `key.pem` signing key, +you would run (replace `~/zephyrproject` with path to your Zephyr workspace): + +```sh + west build \ + -d mcuboot \ + -b stm32f746g_disco \ + ~/zephyrproject/bootloader/mcuboot/boot/zephyr \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay;app.overlay" \ + -DCONFIG_BOOT_SIGNATURE_KEYFILE='"key.pem"' \ + -DCONFIG_BOOT_SIGNATURE_TYPE_RSA=y \ + -DCONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN=2048 \ + -DCONFIG_BOOT_SWAP_USING_SCRATCH=y +``` + +The produced image can be flashed to your device. +For more details on building and using MCUboot with Zephyr, please refer to [official MCUboot guide](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/readme-zephyr.html#building-and-using-mcuboot-with-zephyr). + +### Setting up the Zephyr application + +#### Building the image + +To allow your application to be used with MCUmgr client, you will have to enable Zephyr's [device management subsystem](https://docs.zephyrproject.org/latest/services/device_mgmt/index.html#device-mgmt). +For the client to function properly, both [*image management*](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html) +and [*OS management*](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html) groups need to be enabled. +You will also have to enable and configure [SMP transport](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_transport.html) +(either serial, BLE or udp) that you wish to use. +To learn how to do that, you can reference Zephyr's [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) +which provides configuration for all of them. + +You will also have set `MCUBOOT_BOOTLOADER_MODE` setting to match the *swapping algorithm* you've configured for the [bootloader](#building-the-bootloader): + +:::{table} +:name: Swap algorithm configuration options + +MCUboot | Zephyr +--- | --- +`BOOT_SWAP_USING_MOVE` | `MCUBOOT_BOOTLOADER_MODE_SWAP_WITHOUT_SCRATCH` +`BOOT_SWAP_USING_SCRATCH` | `MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH` +::: + +:::{important} +#### Bluetooth specific + +Bluetooth transport additionally requires you to manually start SMB Bluetooth advertising. +Refer to the [`main.c`](https://github.com/zephyrproject-rtos/zephyr/blob/dbfc1aaec697b78573c18d83fd40ba66ff63c0b3/samples/subsys/mgmt/mcumgr/smp_svr/src/main.c#L70-L72) +and [`bluetooth.c`](https://github.com/zephyrproject-rtos/zephyr/blob/dbfc1aaec697b78573c18d83fd40ba66ff63c0b3/samples/subsys/mgmt/mcumgr/smp_svr/src/bluetooth.c) +from the [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) for details on that. +::: + +To build the [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) +for the `stm32f746g_disco` board with `stm32_disco.overlay` devicetree overlay, +configured to use serial transport with *swap-using-scratch* update algorithm, +you would run (replace `~/zephyrproject` with path to your Zephyr workspace): + +```sh + west build \ + -d build \ + -b stm32f746g_disco \ + "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay" \ + -DEXTRA_CONF_FILE="overlay-serial.conf" \ + -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y +``` + +For more information on the `smp_svr` sample, please refer to [Zephyr's documentation](https://docs.zephyrproject.org/latest/samples/subsys/mgmt/mcumgr/smp_svr/README.html#smp-svr). + +#### Signing the image + +By default MCUboot will only accept images that are properly signed with the same key as the bootloader itself. +Only `BIN` and `HEX` output types can be signed. +The recommended way for managing signing keys is using [MCUboot's image tool](#generating-image-signing-key), +which is shipped together with Zephyr's MCUboot implementation. +When signing an image, you also have to provide an image version, that's embedded in the signed image header. +This is also the value that will be reported by the MCUmgr client as the current running software version back to the [RDFM server](./rdfm_mgmt_server.md). +Image version is specified in `major.minor.revision+build` format. + +##### Automatically + +Zephyr build system can automatically sign the final image for you. +To enable this functionality, you will have to set: + +* `MCUBOOT_SIGNATURE_KEY_FILE`: path to the signing key +* `MCUBOOT_IMGTOOL_SIGN_VERSION`: version of the produced image +before building your application. +Here's a modification of the build command from [building the image](#building-the-image) with those settings applied: + +```sh + west build \ + -d build \ + -b stm32f746g_disco \ + "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay" \ + -DEXTRA_CONF_FILE="overlay-serial.conf" \ + -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y \ + -DCONFIG_MCUBOOT_SIGNATURE_KEY_FILE='"key.pem"' \ + -DCONFIG_IMGTOOL_SIGN_VERSION='"1.2.3+4"' +``` + +##### Manually + +You can also sign the produced images yourself using the [image tool](#generating-image-signing-key). +Below is a sample showing how to sign previously built image: + +```sh +west sign -d build -t imgtool -- --key --version +``` + +Either way, the signed images will be stored next to their unsigned counterparts. They will have `signed` inserted into the filename (e.g. unsigned `zephyr.bin` will produce `zephyr.signed.bin` signed image). + +### Self-confirmed updates + +By default, MCUmgr client will try to manually confirm a new image during an update. +While this works in simple cases, you might wish to run some additional test logic that should be used to determine if an update should be finalized. +For example, you might want to reject an update in case one of the drivers failed to start or if the network stack is misconfigured. +The client supports these kinds of use cases using self-confirming images. +Rather than confirming an update by itself, +the client will instead watch the primary image slot of the device to determine if an update was marked as permanent or if it was rejected. +In that case, the final decision falls on the updated device. + +For this feature to work correctly, you will have to modify your application to include the self-testing logic. + +```c +/* + * An example of self-test function. + * It will first check if this is a fresh update and run the testing logic. + * Based on results, it will either mark the update as permanent or reboot, + * causing MCUboot to revert to the previous version. + * + * This function should be called before the main application logic starts, + * preferably at the beginning of the `main` function. + */ + +#include +#include + +void run_self_tests() { + if (!boot_is_img_confirmed()) { + bool passed; + + /* Testing logic goes here */ + + if (!passed) { + sys_reboot(SYS_REBOOT_COLD); // (1) + return; + } + + boot_write_img_confirmed(); // (2) + } +} +``` +::::{code-annotations} +1. Tests failed - device reboots itself, returning to previous version +2. Tests passed - device confirms the update, marking it as permanent +:::: + +## Configuring MCUmgr client + +### Search locations + +The client is configured using `config.json` configuration file. +By default, the client will look for this file in: + +- current working directory +- `$HOME/.config/rdfm-mcumgr` +- `/etc/rdfm-mcumgr` + +stopping at first configuration file found. +You can override this by specifying path to a different configuration file with `-c/--config` flag: + +```sh +rdfm-mcumgr-client --config +``` + +All of the non-device specific options can also be overwritten by specifying their flag counterpart. +For a full list you can run: + +```sh +rdfm-mcumgr-client --help +``` + +### Configuration values + +- `server` - URL of the RDFM server the client should connect to +- `key_dir` - path (relative or absolute) to the directory where all device keys are stored +- `update_interval` - interval between each update poll to RDFM server (accepts time suffixes 's', 'm', 'h') +- `retries` - (optional) how many times should an update be attempted for a device in case of an error + (no value or value `0` means no limit) +- `devices` - an array containing configuration for each device the client should handle + - `name` - display name for device, used only for logging + - `id` - unique device identifier used when communicating with RDFM server + - `device_type` - device type reported to RDFM server used to specify compatible artifacts + - `key` - name of the file containing device private key in PEM format. Key should be stored in `key_dir` directory. + - `self_confirm` - (optional) bool indicating whether the device will confirm updates by itself. False by default + - `update_interval` - (optional) override global `update_interval` for this device + - `transport` - specifies the transport type for the device and it's specific options + +- `groups` - an array containing configuration for device groups + - `name` - display name for group, used for logging + - `id` - unique group identifier used when communicating with RDFM server + - `type` - type reported to RDFM server to specify compatible artifacts + - `key` - name of the file containing group private key in PEM format. Key should be stored in `key_dir` directory. + - `update_interval` - (optional) override global `update_interval` for this group + - `members` - an array containing configuration for each device that's a member of this group + - `name` - display name for device, used for logging + - `device` - name of target image to match from an artifact + - `self_confirm` - (optional) bool indicating whether the device will confirm updates by itself. False by default + - `transport` - specifies the transport type for the device and its specific options + +Transport specific: +- `type` - specific transport type for this device. Currently supported: `ble`, `serial`, `udp` + +* BLE transport: + - `device_index` - controller index to be used for connection (e.g. `hci0` -> `0`) + - `peer_name` - the name the target BLE device advertises. Should match with `CONFIG_BT_DEVICE_NAME` + +* Serial transport: + - `device` - device name used for communicating with device. OS specific (e.g. `"/dev/ttyUSB0"`, `"/dev/tty.usbserial"`) + - `baud` - communication speed; must match the baudrate of connected device + - `mtu` - Maximum Transmission Unit, maximum protocol packet size + +* UDP transport: + - `address`: IPv4 / IPv6 address and port in `IP`:`port` form + +### Device groups + +The client supports grouping multiple Zephyr MCUboot boards to act as one complete device from management server's perspective. +While each device in a group can be running different Zephyr application, +all devices are synchronized by the MCUmgr client to be running the exact same software version. +Group updates are performed using [zephyr group artifacts](./rdfm_artifact.md#creating-a-zephyr-mcuboot-group-artifact) +which contain update images for each member of the group and metadata on how to match image to device. + +During an update, the MCUmgr client matches each image to its target member and tries to apply it. +Group update is considered successful only if **all** members of the group went through the update process without errors. +Otherwise all members are rolled back by the client to the previous version. + +### Example configuration + +```json +{ + "server": "http://localhost:5000", + "key_dir": "keys", + "update_interval": "10s", + "retries": 3, + "devices": [ + { + "name": "zephyr-ble", + "id": "11:11:11:11:11:11", + "dev_type": "zeph-ble", + "update_interval": "15s", + "key": "ble.key", + "transport": { + "type": "ble", + "device_index": 0, + "peer_name": "test0" + } + }, + { + "name": "zephyr-serial", + "id": "22:22:22:22:22:22", + "dev_type": "zeph-ser", + "key": "serial.key", + "self_confirm": true, + "transport": { + "type": "serial", + "device": "/dev/ttyACM0", + "baud": 115200, + "mtu": 128 + } + } + ], + "groups": [ + { + "name": "group-one", + "id": "gr1", + "type": "group1", + "key": "group1.key", + "members": [ + { + "name": "udpl", + "device": "udp-left", + "transport": { + "type": "udp", + "address": "192.168.1.2:1337" + } + }, + { + "name": "udpr", + "device": "udp-right", + "transport": { + "type": "udp", + "address": "192.168.1.3:1337" + } + }, + { + "name": "bleh", + "device": "ble", + "self_confirm": true, + "transport": { + "type": "ble", + "device_index": 0, + "peer_name": "ble_head", + } + } + ] + } + ] +} +``` + +### Device keys + +Each device uses its own private key for authentication with `rdfm-server` as described in [device authentication](./server_operation.md#device-authentication). +Each key should be stored under `key_dir` specified in configuration. +If the client doesn't find corresponding device key for configured device, it will attempt to generate one itself. +The resulting key will be saved to the configured location with `0600` permissions. + +:::{note} +Device keys are different from the signing key used for signing the bootloader and application images! +::: diff --git a/_sources/rdfm_mgmt_server.md.txt b/_sources/rdfm_mgmt_server.md.txt new file mode 100644 index 0000000..8b13e29 --- /dev/null +++ b/_sources/rdfm_mgmt_server.md.txt @@ -0,0 +1,438 @@ +# RDFM Management Server + +## Introduction + +The RDFM Management Server is a core part of the RDFM ecosystem. The server manages incoming device connections and grants authorization only to those which are allowed to check-in with the server. +It also handles package upload and management, deploy group management and other crucial functionality required for robust and secure device Over-The-Air (OTA) updates along with allowing remote system management without exposing devices to the outside world. + +## REST API + +The server exposes a management and device API that is used by management software and end devices. A comprehensive list of all API endpoints is available in the [RDFM Server API Reference chapter](api.rst). + +## Setting up a Dockerized development environment + +The preferred method for running the RDFM server is by using a Docker container. +To set up a local development environment, first clone the RDFM repository: + +```bash +git clone https://github.com/antmicro/rdfm.git +cd rdfm/ +``` + +A `Dockerfile` is provided in the `server/deploy/` directory that builds a container suitable for running the server. +Currently, it is required to build the container image manually. +To do this, run the following from the **cloned RDFM repository root** folder: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +A simple `docker-compose` file that can be used to run the server is provided below, and in the `server/deploy/docker-compose.development.yml` file. + +```yaml +services: + rdfm-server: + image: antmicro/rdfm-server:latest + restart: unless-stopped + environment: + - RDFM_JWT_SECRET= + - RDFM_DB_CONNSTRING=sqlite:////database/development.db + - RDFM_HOSTNAME=rdfm-server + - RDFM_API_PORT=5000 + - RDFM_DISABLE_ENCRYPTION=1 + - RDFM_DISABLE_API_AUTH=1 + - RDFM_LOCAL_PACKAGE_DIR=/packages/ + - RDFM_WSGI_SERVER=werkzeug + ports: + - "5000:5000" + volumes: + - db:/database/ + - pkgs:/packages/ + +volumes: + db: + pkgs: +``` + +The server can then be started using the following command: + +```bash +docker-compose -f server/deploy/docker-compose.development.yml up +``` + +## Configuration via environment variables + +Configuration of the RDFM server can be changed by using the following environment variables: + +- `RDFM_JWT_SECRET` - secret key used by the server when issuing JWT tokens, this value must be kept secret and not easily guessable (for example, a random hexadecimal string). +- `RDFM_DB_CONNSTRING` - database connection string, for examples please refer to: [SQLAlchemy - Backend-specific URLs](https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls). Currently, only the SQLite and PostgreSQL engines were verified to work with RDFM (however: the PostgreSQL engine requires adding additional dependencies which are currently not part of the default server image, this may change in the future). + +Development configuration: + +- `RDFM_DISABLE_ENCRYPTION` - if set, disables the use of HTTPS, falling back to exposing the API over HTTP. This can only be used in production if an additional HTTPS reverse proxy is used in front of the RDFM server. +- `RDFM_DISABLE_API_AUTH` - if set, disables request authentication on the exposed API routes. **WARNING: This is a development flag only! Do not use in production!** This causes all API methods to be freely accessible, without any access control in place! +- `RDFM_ENABLE_CORS` - if set, disables CORS checks, which in consequence allows any origin to access the server. **WARNING: This is a development flag only! Do not use in production!** + +HTTP/WSGI configuration: + +- `RDFM_HOSTNAME` - hostname/IP address to listen on. This is additionally used for constructing package URLs when storing packages in a local directory. +- `RDFM_API_PORT` - API port. +- `RDFM_SERVER_CERT` - required when HTTPS is enabled; path to the server's certificate. The certificate can be stored on a Docker volume mounted to the container. For reference on generating the certificate/key pairs, see the `server/tests/certgen.sh` script. +- `RDFM_SERVER_KEY` - required when HTTPS is enabled; path to the server's private key. Additionally, the above also applies here. +- `RDFM_WSGI_SERVER` - WSGI server to use, this value should be left default. Accepted values: `gunicorn` (**default**, production-ready), `werkzeug` (recommended for development). +- `RDFM_WSGI_MAX_CONNECTIONS` - (when using Gunicorn) maximum amount of connections available to the server worker. This value must be set to at minimum the amount of devices that are expected to be maintaining a persistent (via WebSocket) connection with the server. Default: `4000`. +- `RDFM_INCLUDE_FRONTEND_ENDPOINT` - specifies whether the RDFM server should serve the frontend application. If set, the server will serve the frontend application from endpoint `/api/static/frontend`. Before setting this variable, the frontend application must be built and placed in the `frontend/dist` directory. +- `RDFM_FRONTEND_APP_URL` - specifies URL to the frontend application. This variable is required when `RDFM_INCLUDE_FRONTEND_ENDPOINT` is not set, as backend HTTP server has to know where to redirect the **user**. + +API OAuth2 configuration (must be present when `RDFM_DISABLE_API_AUTH` is omitted): + +- `RDFM_OAUTH_URL` - specifies the URL to an authorization server endpoint compatible with the RFC 7662 OAuth2 Token Introspection extension. This endpoint is used to authorize access to the RDFM server based on tokens provided in requests made by API users. +- `RDFM_LOGIN_URL` - specifies the URL to a login page of the authorization server. It is used to authorize users and generate an access token and start a session. +- `RDFM_LOGOUT_URL` - specified the URL to a logout page of the authorization server. It is used to end the session and revoke the access token. +- `RDFM_OAUTH_CLIENT_ID` - if the authorization server endpoint provided in `RDFM_OAUTH_URL` requires the RDFM server to authenticate, this variable defines the OAuth2 `client_id` used for authentication. +- `RDFM_OAUTH_CLIENT_SEC` - if the authorization server endpoint provided in `RDFM_OAUTH_URL` requires the RDFM server to authenticate, this variable defines the OAuth2 `client_secret` used for authentication. + +Package storage configuration: + +- `RDFM_STORAGE_DRIVER` - storage driver to use for storing artifacts. Accepted values: `local` (default), `s3`. +- `RDFM_LOCAL_PACKAGE_DIR` - specifies a path (local for the server) to a directory where the packages are stored. +- `RDFM_S3_BUCKET` - when using S3 storage, name of the bucket to upload the packages to. +- `RDFM_S3_ACCESS_KEY_ID` - when using S3 storage, Access Key ID to access the specified bucket. +- `RDFM_S3_ACCESS_SECRET_KEY` - when using S3 storage, Secret Access Key to access the specified bucket. + +## Configuring package storage location + +### Storing packages locally + +By default (when not using one of the above deployment setups), the server stores all uploaded packages to a temporary folder under `/tmp/.rdfm-local-storage/`. +To persist package data, configuration of an upload folder is required. +This can be done by using the `RDFM_LOCAL_PACKAGE_DIR` environment variable (in the Dockerized deployment), which should contain a path to the desired upload folder. + +:::{warning} +This storage method should NOT be used for production deployments! +The performance of the built-in file server is severely limited and provides NO caching, which will negatively affect the update speed for all devices even when a few of them try downloading an update package at the same time. +It is recommended to use a dedicated storage solution such as S3 to store packages. +::: + +### Storing packages on S3-compatible storage + +The RDFM server can also store package data on S3 and other S3 API-compatible object storage servers. +The following environment variables allow changing the configuration of the S3 integration: +- `RDFM_S3_BUCKET` - name of the bucket to upload the packages to +- `RDFM_S3_ACCESS_KEY_ID` - Access Key ID to access the specified bucket +- `RDFM_S3_ACCESS_SECRET_KEY` - Secret Access Key to access the specified bucket +Additionally, when using S3 storage, the environment variable `RDFM_STORAGE_DRIVER` must be set to `s3`. + +An example reference setup utilizing the MinIO Object Storage server is provided in the `server/deploy/docker-compose.minio.yml` file. +To run it, first build the RDFM server container like in the above setup guides: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +Then, run the following: + +``` +docker-compose -f server/deploy/docker-compose.minio.development.yml up +``` + +## Configuring API authentication + +### Basic configuration + +The above development setup does not provide any authentication for the RDFM API. +This is helpful for development or debugging purposes, however **under no circumstance should this be used in production deployments, as it exposes the entire API with no restrictions in place**. + +By default, the RDFM server requires configuration of an external authorization server to handle token creation and scope management. +To be compatible with RDFM Management Server, the authentication server **MUST** support the OAuth2 Token Introspection extension ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)). + +The authorization server is configured using the following environment variables: +- `RDFM_OAUTH_URL` - specifies the URL to the Token Introspection endpoint of the authorization server. +- `RDFM_OAUTH_CLIENT_ID` - specifies the client identifier to use for authenticating the RDFM server to the authorization server. +- `RDFM_OAUTH_CLIENT_SEC` - specifies the client secret to use for authenticating the RDFM server to the authorization server. + +For accessing the management API, the RDFM server does not issue any tokens itself. +This task is delegated to the authorization server that is used in conjunction with RDFM. +The following scopes are used for controlling access to different methods of the RDFM API: +- `rdfm_admin_ro` - read-only access to the API (fetching devices, groups, packages) +- `rdfm_admin_rw` - complete administrative access to the API with modification rights + +Additional rules are defined for package uploading route from [Packages API](api.rst#Packages_API). +- `rdfm_upload_single_file` - allows uploading an artifact of type `single-file`. +- `rdfm_upload_rootfs_image` - allows uploading artifacts `rootfs-image` and `delta-rootfs-image`. +Each package type requires its corresponding scope, or the complete admin access - `rdfm_admin_rw`. + +Refer to the [RDFM Server API Reference chapter](api.rst) for a breakdown of the scopes required for accessing each API method. + +### API authentication using Keycloak + +#### Running the services + +An example `docker-compose` file that can be used to run the RDFM server using [Keycloak Identity and Access Management server](https://www.keycloak.org/) as an authorization server is provided below, and in the `server/deploy/docker-compose.keycloak.development.yml` file. + +```yaml +services: + rdfm-server: + image: antmicro/rdfm-server:latest + restart: unless-stopped + environment: + - RDFM_JWT_SECRET= + - RDFM_DB_CONNSTRING=sqlite:////database/development.db + - RDFM_HOSTNAME=rdfm-server + - RDFM_API_PORT=5000 + - RDFM_DISABLE_ENCRYPTION=1 + - RDFM_LOCAL_PACKAGE_DIR=/packages/ + - RDFM_OAUTH_URL=http://keycloak:8080/realms/master/protocol/openid-connect/token/introspect + - RDFM_OAUTH_CLIENT_ID=rdfm-server-introspection + - RDFM_OAUTH_CLIENT_SEC= + networks: + - rdfm + ports: + - "5000:5000" + volumes: + - db:/database/ + - pkgs:/packages/ + + keycloak: + image: quay.io/keycloak/keycloak:22.0.1 + restart: unless-stopped + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + networks: + - rdfm + ports: + - "8080:8080" + command: + - start-dev + volumes: + - keycloak:/opt/keycloak/data/ + - ../keycloak-themes:/opt/keycloak/themes + +volumes: + db: + pkgs: + keycloak: + +networks: + rdfm: +``` + +Before running the above services, you must first build the RDFM server container by running the following from the RDFM repository root folder: + +``` +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +You can then run the services by running: + +``` +docker-compose -f server/deploy/docker-compose.keycloak.development.yml up +``` + +#### Keycloak configuration + +Further configuration on the Keycloak server is required before any requests are successfully authenticated. +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to **Clients** and press **Create client**. +This client is required for the RDFM server to perform token validation. +The following settings must be set when configuring the client: +- **Client ID** - must match `RDFM_OAUTH_CLIENT_ID` provided in the RDFM server configuration, can be anything (for example: `rdfm-server-introspection`) +- **Client Authentication** - set to `On` +- **Authentication flow** - select only `Service accounts roles` + +After saving the client, go to the `Credentials` tab found under the client details. +Make sure the authenticator used is `Client Id and Secret`, and copy the `Client secret`. +This secret must be configured in the RDFM server under the `RDFM_OAUTH_CLIENT_SEC` environment variable. + +:::{note} +After changing the `docker-compose` variables, remember to restart the services (by pressing `Ctrl+C` and re-running the `docker-compose up` command). +::: + +Additionally, you must create proper client scopes and user roles to define which users have access to the read-only and read-write parts of the RDFM API. +To create new scopes, navigate to the `Client scopes` tab and select `Create client scope`. +Create four separate scopes with the following names; the rest of the settings can be left as default (if required, you may also add a description to the scope): +- `rdfm_admin_ro` +- `rdfm_admin_rw` +- `rdfm_upload_single_file` +- `rdfm_upload_rootfs_image` + +To create new roles, navigate to the `Realm roles` tab and select `Create role`. +Create separate roles with the same names. The rest of the settings can be left as default (if required, you may also add a description to the role). + +After restarting the services, the RDFM server will now validate requests against the Keycloak server. +To further setup the `rdfm-mgmt` manager to use the Keycloak server, refer to the [RDFM manager manual](rdfm_manager.md). To add users with roles to the Keycloak server, which can then be used to access the RDFM API using the frontend application, refer to the [Adding a User](#adding-a-user) section below. + +#### Adding an API client + +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to **Clients** and press **Create client**. +This client will represent a user of the RDFM API. +The following settings must be set when configuring the client: +- **Client Authentication** - set to `On` +- **Authentication flow** - select only `Service accounts roles` + +After saving the client, go to the `Credentials` tab found under the client details. +Make sure the authenticator used is `Client Id and Secret`, and copy the `Client secret`. + +Finally, assign the required scope to the client: under the `Client scopes` tab, click `Add client scope` and select one of the two RDFM scopes: read-only `rdfm_admin_ro` or read-write `rdfm_admin_rw`. + +:::{note} +The newly-created client will now have access to the RDFM API. +To configure `rdfm-mgmt` to use this client, follow the [Configuration section](rdfm_manager.md#configuration) of the RDFM manager manual. +::: + +#### Adding a User + +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to `Users` tab and press **Add user**. This will open up a form to create a new user. +Fill in the **Username** field and press **Create**. + +Next, go to `Credentials` tab found under the user details and press **Set password**. +This form allows you to set a password for the user and determine whether creating a new one is required on the next login. + +After configuring the user, go to `Role mapping` tab under the user details. +There, appropriate roles can be assigned to the user using the **Assign role** button. + +:::{note} +The newly created users can now log in using the RDFM frontend application. +To configure and run the frontend application, refer to the [RDFM Frontend chapter](rdfm_frontend.md). +::: + +##### Configuring frontend application + +When using the frontend application, logging in functionality is provided by the Keycloak server. +To integrate the Keycloak server with the frontend application first go to the client details created in the [Keycloak configuration](#keycloak-configuration) section. + +Go to `Capability config` and make sure that **Implicit flow** and **Standard flow** are enabled. + +Open `Settings` panel and set **Valid redirect URIs** and **Valid post logout redirect URIs** values to the URL of the frontend application. +The value depends on the deployment method, if the `rdfm-server` is used to host the frontend application the value can be inferred from the `RDFM_HOSTNAME` and `RDFM_API_PORT` environment variables and will most likely be `http[s]://{RDFM_HOSTNAME}:{RDFM_API_PORT}`. +Otherwise, the value should be equal to `RDFM_FRONTEND_APP_URL` variable. + +Additionally, you can change the theme of the login page to match the frontend application. +To do this, go to `Login settings` section and `rdfm` in the **Login theme** dropdown. + +## Configuring HTTPS + +For simple deployments, the server can expose an HTTPS API directly without requiring an additional reverse proxy. +Configuration of the server's HTTPS can be done using the following environment variables: + +- `RDFM_SERVER_CERT` - path to the server's signed certificate +- `RDFM_SERVER_KEY` - path to the server's private key + +Both of these files must be accessible within the server Docker container. + +### HTTPS demo deployment + +:::{warning} +This demo deployment explicitly disables API authentication, and is only meant to be used as a reference on how to configure your particular deployment. +::: + +An example HTTPS deployment can be found in the `server/deploy/docker-compose.https.development.yml` file. +Before running it, you must execute the `tests/certgen.sh` in the `server/deploy/` directory: + +```bash +cd server/deploy/ +../tests/certgen.sh +``` + +This script generates a root CA and an associated signed certificate to be used for running the server. +The following files are generated: + +- `certs/CA.{crt,key}` - CA certificate/private key that is used as the root of trust +- `certs/SERVER.{crt,key}` - signed certificate/private key used by the server + +To run the deployment, you must first build the RDFM server container by running the following from the RDFM repository root folder: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +You can then start the deployment by running: +```bash +docker-compose -f server/deploy/docker-compose.https.development.yml up +``` + +To verify the connection to the server, you must provide the CA certificate. +For example, when using `curl` to access API methods: + +```bash +curl --cacert server/deploy/certs/CA.crt https://127.0.0.1:5000/api/v1/devices +``` + +When using `rdfm-mgmt`: + +```bash +rdfm-mgmt --url https://127.0.0.1:5000/ \ + --cert server/deploy/certs/CA.crt \ + --no-api-auth \ + devices list +``` + + +## Production deployments + +### Production considerations + +The following is a list of considerations when deploying the RDFM server: + +1. HTTPS **must** be enabled; `RDFM_DISABLE_ENCRYPTION` **must not** be set (or the server is behind a dedicated reverse proxy that adds HTTPS on the edge). +2. API authentication **must** be enabled; `RDFM_DISABLE_API_AUTH` **must not** be set. +3. RDFM **must** use a production WSGI server; `RDFM_WSGI_SERVER` **must not** be set to `werkzeug`. + When not provided, the server defaults to using a production-ready WSGI server (`gunicorn`). + The development server (`werkzeug`) does not provide sufficient performance to handle production workloads, and a high percentage of requests will be dropped under heavy load. +4. RDFM **must** use a dedicated (S3) package storage location; the local directory driver does not provide adequate performance when compared to dedicated object storage. + +Refer to the above configuration chapters for how to configure each aspect of the RDFM server: + +1. [Configuring HTTPS](#configuring-https) +2. [Configuring API authentication](#configuring-api-authentication) +3. [Configuring the WSGI server](#configuration-via-environment-variables) +4. [Configuring S3 package storage](#configuring-package-storage-location) + +A practical example of a deployment that includes all the above considerations can be found below, in the [Production example deployment](#production-example-deployment) section. + +### Production example deployment + +:::{warning} +For simplicity, this example deployment has static credentials pre-configured pretty much everywhere, and as such should never be used directly as a production setup. +At least the following secrets are pre-configured and would require changes: +- S3 Access Key ID/Access Secret Key +- rdfm-server JWT secret +- Keycloak Administrator username/password +- Keycloak Client: rdfm-server introspection Client ID/Secret +- Keycloak Client: rdfm-mgmt admin user Client ID/Secret + +Additionally, the Keycloak server requires further configuration for production deployments. +For more information, refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) page in Keycloak documentation. +::: + +A reference setup is provided in `server/deploy/docker-compose.production.yml` that can be used for customizing production server deployments. +Prior to starting the deployment, you must generate a signed server certificate that will be used for establishing the HTTPS connection to the server. +This can be done by either providing your own certificate, or by running the provided example certificate generation script: + +```bash +cd server/deploy/ +../tests/certgen.sh +``` + +When using the `certgen.sh` script, the CA certificate found at `server/deploy/certs/CA.crt` can be used for validating the connection made to the server. + +Similarly to previous example deployments, it can be started by running the following command from the **RDFM monorepository root folder**: + +```bash +docker-compose -f server/deploy/docker-compose.production.yml up +``` + +`rdfm-mgmt` configuration for this deployment can be found in `server/deploy/test-rdfm-mgmt-config.json`. +After copying the configuration to `$HOME/.config/rdfm-mgmt/config.json`, you can access the server by running: + +```bash +rdfm-mgmt --url https://127.0.0.1:5000/ --cert server/deploy/CA.crt \ + devices list +``` + diff --git a/_sources/rdfm_ota_manual.md.txt b/_sources/rdfm_ota_manual.md.txt new file mode 100644 index 0000000..e8aab16 --- /dev/null +++ b/_sources/rdfm_ota_manual.md.txt @@ -0,0 +1,113 @@ +# RDFM OTA Manual + +This chapter contains key information about the RDFM OTA update system. + +## Key concepts + +Below is a brief explanation of the key entities of the RDFM update system. + +### Devices + +From the server's point of view, a device is any system that is running an RDFM-compatible update client. +For example, see [RDFM Linux Device Client](./rdfm_linux_device_client.md). +Each device actively reports its metadata to the server: +- Currently running software version (`rdfm.software.version`) +- Device type (`rdfm.hardware.devtype`) +- Other client-specific metadata + +### Packages + +A package is any file that can be used by a compatible update client to update the running system. +From the server's point of view, update packages are simple binary blobs and no specific structure is enforced. +Each package has metadata assigned to it that indicates its contents. +The following metadata fields are mandatory for all packages: +- Software version (`rdfm.software.version`) - indicates the version of the contained software +- Device type (`rdfm.hardware.devtype`) - indicates the device type a package is compatible with + +The device type is used as the first filter when searching for a compatible update package. +Any package that does not match the device type reported by the update client will be considered incompatible. + +A package may also contain metadata with `requires:` clauses. +The `requires` clause is used to indicate dependencies on certain metadata properties of the device. +In its most basic form, it can be used to indicate a dependency on a certain system image to be installed for proper delta update installation. +For more complex use cases involving many intermediate update steps, it can also be used to enforce an order in which certain packages must be installed. + +### Groups + +A group consists of many assigned devices. Each group can also be assigned one or many packages. +The group itself also contains metadata about the group name, description, update policy, and other arbitrary information which can be used by custom frontends interacting with the server. + +### Update policy + +An update policy defines the target version the devices within a given group will be updated to. +The policy is a string with the syntax `,[arguments]`. +Required arguments depend on the specific policy being used. +Currently, the following policies are supported: +- `no_update` (**default**) - requires no arguments, the server will treat all devices within the group as up-to-date, and will not return any packages to devices requesting an update check. **This is the default update policy for all newly created groups**. +- `exact_match` - specifies that the server will attempt to install the target software version on each of the devices in the group. +Example usage: `exact_match,version1` - this specifies that the server will attempt to bring all of the devices to the software version `version1`. +This process may involve installing many intermediate packages, but the end result is a device that's running the specified version. +The server will use group-assigned packages when resolving the dependency graph required for reaching the target version. + +## Update resolution + +When resolving a path to the correct target version, the server utilizes only the group-assigned packages. +When a device is requesting an update check, a package dependency graph is created. +The edges of the graph correspond to different packages available during the update process (which are compatible with the device, as indicated by the `rdfm.hardware.devtype` field), while the nodes indicate the software versions (as indicated by the `rdfm.software.version` fields of each package). +Next, the group's update policy is queried, which indicates the target version/node each device should be attempting to reach. +The shortest path between the currently running node and the target node is used as instructions for how the server should lead the device to the specified version. + +## Example scenario: simple update assignment + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v1` +- P1 - `devtype=bar`, `version=v2` +- P2 - `devtype=baz`, `version=v3` + +The group is specified to update to version `v3` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v3` +- D1 - `devtype=bar`, `version=v3` +- D2 - `devtype=baz`, `version=v3` + +In this scenario, devices `D0` and `D1` shall receive the update packages `P0` and `P1` respectively. +The device `D2` is considered up-to-date, as its version matches the target specified in the group policy. + +## Example scenario: downgrades + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v4` + +The group is specified to update to version `v4` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v5` + +In this scenario, the device `D0` will receive the package `P0` to be installed next. + +## Example scenario: sequential updates + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v2`, `requires:version=v1` +- P1 - `devtype=foo`, `version=v3`, `requires:version=v2` + +The group is specified to update to version `v3` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v1` + +In this scenario, the device `D0` will first be updated to the package `P0`, as it's the only package that is compatible (matching device type and different version than the one running on the device). +The package's only `requires` clause also matches against the device's metadata. + +After successful installation, during the next update check on the newly installed version (`v1`), the device will receive the next available package. +As the device is now reporting a version field of `v1` and the package's `requires:` clause passes, package `P1` becomes the next candidate package available for installation. +After successful instalation of `P1`, no more packages are available and the device is considered to be up-to-date. + +## Example scenario: delta updatess + +Consider a group with the following packages assigned: +- P0 (delta) - `devtype=foo`, `version=v5`, `rootfs=e6e2531..`, `requires:version=v0`, `requires:rootfs=2f646ac..` +- P1 (delta) - `devtype=foo`, `version=v5`, `rootfs=e6e2531..`, `requires:version=v2`, `requires:rootfs=6d9aee4..` + +The group is specified to update to version `v5` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v0`, `rootfs=2f646ac..` +- D1 - `devtype=foo`, `version=v2`, `rootfs=6d9aee4..` + +In this scenario, devices `D0` and `D1` will receive packages `P0` and `P1` as updates respectively. +The packages themselves contain different binary contents, in this case a delta between a given base version's system partition (`v0` and `v2`) and the target (`v5`), but the end result is an identical system on both devices. +This way, many delta packages may be provided for updating a fleet consisting of a wide range of running versions. diff --git a/_sources/server_operation.md.txt b/_sources/server_operation.md.txt new file mode 100644 index 0000000..be124f5 --- /dev/null +++ b/_sources/server_operation.md.txt @@ -0,0 +1,111 @@ +# Server Integration flows + +This chapter describes the various integration flows between device clients and the RDFM Management server. + +## Device authentication + +At the start of their execution, all RDFM-compatible device clients shall authenticate with the server. +This shall be done by utilizing the `/api/v1/auth/device` endpoint. +For details on the request schema, refer to the [Server API Reference](api.rst) chapter. +An example request made to this endpoint is shown below: + +```json +{ + "metadata": { + "rdfm.hardware.devtype": "device-type", + "rdfm.software.version": "foo", + "rdfm.hardware.macaddr": "00:11:22:33:44:55", + } + "public_key": "", + "timestamp": 1694681536, +} +``` + +The JSON payload bytes must be signed by the device client with its securely stored RSA private key using PKCS #1 v1.5 signature with SHA-256 digest (function `RSASSA-PKCS1-V1_5-SIGN` defined in [RFC 8017](https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.1)) +The calculated signature must then be attached, encoded as base64, to the authorization request in the header `X-RDFM-Device-Signature`. +If the server successfully validates the attached signature, the device will be registered in the server's database, if it wasn't previously registered already. +The device-specified MAC address is used as a unique identifier for this specific device. + +Before the device is authorized to access the RDFM API, it must be accepted first by an administrative entity interacting via a separate API with the RDFM server. +If the device was not accepted, or its acceptation status was revoked, the above request shall fail with the `401 Unauthorized` HTTP status code. **The device client must handle this status code gracefully**, for example by retrying the attempted request after a certain time has passed. + +Once the device is accepted into the RDFM server, the above request shall return a device-specific app token, that can be used to interact with device-side API endpoints. +The app token is not permanent, and will expire after a certain time period. +The device client must not make any assumptions about the length of the usability period, and instead should take a defensive approach to any requests made to the device-side API and reauthenticate when a response with the `401` status code is received. + +## Device update check + +Once authorized, a device client will have access to the device-side API of the RDFM server. +The device client is expected to regularly poll for updates by utilizing the `/api/v1/update/check` endpoint. + +In the update check request, the device client must provide all of its local metadata. +The metadata, which consists of simple key/value pairs, uniquely describes the set of software and/or hardware present on the device, but may also represent other transient +properties not persisted in storage, such as temperature sensor values. + +When making the update check, the device client is advised to provide all of its metadata to the server in the update request. +At the time of writing, below three metadata properties are mandatory and must be present in all update checks: + +- `rdfm.software.version` - version identifier of the currently running software package +- `rdfm.hardware.devtype` - device type, used for limiting package compatibility only to a subset of devices +- `rdfm.hardware.macaddr` - MAC address of the device's main network interface + +For future compatibility, device clients are advised to provide all of their metadata, not only the mandatory keys, in the update check request. +For more details on the structure of an update check request, consult the [Update API Reference](api.rst#post--api-v1-update-check) + +When a new package is available, the response shall be as described in the API Reference, and a one-time download URL to the package is generated. +The device client shall use this URL to download and install, or in the case of clients capable of stream installation, directly install the package. +The device client **MUST** verify the hash of the package as described in the update check response. + +Additionally, the device client **MUST** verify whether the package contents look sane before attempting to install it. +The server shall never return a package that is not of the same device type as the one advertised by the client. +However, the server itself currently imposes **no limitations** on the binary contents of the packages themselves. + +## Management WebSocket + +If supported, the device may also connect to a device management WebSocket. +This provides additional management functionality of registered devices, such as reverse shell and file transfer. +To connect to the WebSocket, a device token is required to be provided in the `Authorization` header of the WebSocket handshake. +The format of the header is exactly the same as in other device routes and is described [in the API Reference chapter](api.rst#api-authentication). + +The general management flow is as follows: +1. Device connects to the management WebSocket: `/api/v1/devices/ws` +1. Device sends a `CapabilityReport` message indicating the capabilities it supports +1. Device reads incoming management messages from the server and handles them accordingly +1. Device may also send messages to the server to notify of certain situations + +### RDFM Management Protocol + +The management protocol is message-oriented and all messages are expected to be sent in WebSocket text mode. +Each message is a JSON object in the form: +```json +{ + "method": "", + "arg0": "...", + "arg1": {"...": "..."}, + "...": "..." +} +``` + +The type of message sent is identified by the `method` field. +The rest of the object fields are unspecified and depend on the specific message type. +Schema for messages used by the server can be found in `common/communication/src/request_models.py`. +On error during handling of a request, the server may return a custom WebSocket status code. +A list of status codes used by the server can be found in `common/communication/src/rdfm/ws.py`. + +### Capabilities + +A capability indicates what management functionality is supported by a device. +The device should report its capabilities using the `CapabilityReport` message immediately after connecting to the server. +By default it is assumed that the device does not provide any capabilities. + +#### Capability - `shell` + +This capability indicates that a device supports spawning a reverse shell. +The following methods must be supported by the device: +- `shell_attach` + +A device with the `shell` capability should react to `shell_attach` messages by connecting to a shell WebSocket at `/api/v1/devices//shell/attach/`. +This establishes a connection between the requesting manager and the device. +This WebSocket can then be used to stream the contents of the shell session and receive user input. +The format of messages sent over this endpoint is implementation defined. +However, generally the shell output/input are simply sent as binary WebSocket messages containing the standard output/input as raw bytes. diff --git a/_sources/system_overview.md.txt b/_sources/system_overview.md.txt new file mode 100644 index 0000000..13bd7f3 --- /dev/null +++ b/_sources/system_overview.md.txt @@ -0,0 +1,44 @@ +# System Architecture + +The reference architecture of an RDFM system consists of: + +- `RDFM Management Server` - handles device connections, packages, deployment, remote device management +- `Devices` - devices connect to a central management server and utilize the exposed `REST API` and device-server RDFM protocol for providing remote management functionality +- `Users` - individual users that are authenticated and allowed read-only/read-write access to resources exposed by the server + +The system architecture can be visualized as follows: + +:::{figure-md} summary +![Architecture summary](images/summary.png) + +Summary of the system architecture +::: + +## HTTP REST API + +For functionality not requiring a persistent connection, the server exposes an HTTP API. A complete list of available endpoints can be found +in the [RDFM Server API Reference](api.rst) chapter. The clients use this API to perform update checks. + +## Device-server RDFM Protocol + +The devices also maintain a persistent connection to the RDFM Management Server by utilizing JSON-based messages sent over a WebSocket route. +This is used to securely expose additional management functionality without directly exposing device ports to the Internet. + +Each message sent using the RDFM protocol is structured as follows: + +```text +0 h ++----------------------------+ +| utf-8 encoded JSON message | ++----------------------------+ +``` + +The message is a UTF-8 encoded JSON object, where each message is distinguished by the mandatory ``'method'`` field. + +An example request sent to the server may look like: + +``{'method': 'capability_report', 'capabilities': {'shell': True}}`` + +A response from the server may look like: + +``{'method': 'alert', 'alert': {'devices': ['d1', 'd2']}}`` diff --git a/_static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 b/_static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 new file mode 100644 index 0000000..ab30100 Binary files /dev/null and b/_static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 differ diff --git a/_static/fonts/029e176ad602329b4434892101db9cf3.woff2 b/_static/fonts/029e176ad602329b4434892101db9cf3.woff2 new file mode 100644 index 0000000..09e03c9 Binary files /dev/null and b/_static/fonts/029e176ad602329b4434892101db9cf3.woff2 differ diff --git a/_static/fonts/07ff82964967feebb9c96288e0e0df05.woff2 b/_static/fonts/07ff82964967feebb9c96288e0e0df05.woff2 new file mode 100644 index 0000000..d338178 Binary files /dev/null and b/_static/fonts/07ff82964967feebb9c96288e0e0df05.woff2 differ diff --git a/_static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 b/_static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 new file mode 100644 index 0000000..6b0b4af Binary files /dev/null and b/_static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 differ diff --git a/_static/fonts/0a0ad0eae50e549ecd713b9ad417f1a1.woff2 b/_static/fonts/0a0ad0eae50e549ecd713b9ad417f1a1.woff2 new file mode 100644 index 0000000..f477fda Binary files /dev/null and b/_static/fonts/0a0ad0eae50e549ecd713b9ad417f1a1.woff2 differ diff --git a/_static/fonts/0b68e8634c96265eb32a0c769416b5b0.woff2 b/_static/fonts/0b68e8634c96265eb32a0c769416b5b0.woff2 new file mode 100644 index 0000000..9a378af Binary files /dev/null and b/_static/fonts/0b68e8634c96265eb32a0c769416b5b0.woff2 differ diff --git a/_static/fonts/0d1b73eee266eabb2cff35dfa4ce25a3.woff2 b/_static/fonts/0d1b73eee266eabb2cff35dfa4ce25a3.woff2 new file mode 100644 index 0000000..48a8c10 Binary files /dev/null and b/_static/fonts/0d1b73eee266eabb2cff35dfa4ce25a3.woff2 differ diff --git a/_static/fonts/0e1f73c6737cdf273efb4b79504e4c0a.woff2 b/_static/fonts/0e1f73c6737cdf273efb4b79504e4c0a.woff2 new file mode 100644 index 0000000..6aa2845 Binary files /dev/null and b/_static/fonts/0e1f73c6737cdf273efb4b79504e4c0a.woff2 differ diff --git a/_static/fonts/0e326670106c8eb6a11a8c30734ecfc8.ttf b/_static/fonts/0e326670106c8eb6a11a8c30734ecfc8.ttf new file mode 100644 index 0000000..fbb5625 Binary files /dev/null and b/_static/fonts/0e326670106c8eb6a11a8c30734ecfc8.ttf differ diff --git a/_static/fonts/0ec3cc19652785204ea2e322330f0f1b.woff2 b/_static/fonts/0ec3cc19652785204ea2e322330f0f1b.woff2 new file mode 100644 index 0000000..5b0171c Binary files /dev/null and b/_static/fonts/0ec3cc19652785204ea2e322330f0f1b.woff2 differ diff --git a/_static/fonts/0f303f31706d39866cced9dcc17b61fb.woff2 b/_static/fonts/0f303f31706d39866cced9dcc17b61fb.woff2 new file mode 100644 index 0000000..71c9bb7 Binary files /dev/null and b/_static/fonts/0f303f31706d39866cced9dcc17b61fb.woff2 differ diff --git a/_static/fonts/101522bafe9c61c68698ecc784607772.woff2 b/_static/fonts/101522bafe9c61c68698ecc784607772.woff2 new file mode 100644 index 0000000..186d998 Binary files /dev/null and b/_static/fonts/101522bafe9c61c68698ecc784607772.woff2 differ diff --git a/_static/fonts/10b31f4cad9ea78d43449886bfbb88ac.woff2 b/_static/fonts/10b31f4cad9ea78d43449886bfbb88ac.woff2 new file mode 100644 index 0000000..02a27ea Binary files /dev/null and b/_static/fonts/10b31f4cad9ea78d43449886bfbb88ac.woff2 differ diff --git a/_static/fonts/1181a8e619707033241139715eca64c6.woff2 b/_static/fonts/1181a8e619707033241139715eca64c6.woff2 new file mode 100644 index 0000000..59ed385 Binary files /dev/null and b/_static/fonts/1181a8e619707033241139715eca64c6.woff2 differ diff --git a/_static/fonts/122802d03aed4bf8cd6a03997a97aca4.woff2 b/_static/fonts/122802d03aed4bf8cd6a03997a97aca4.woff2 new file mode 100644 index 0000000..40b9fda Binary files /dev/null and b/_static/fonts/122802d03aed4bf8cd6a03997a97aca4.woff2 differ diff --git a/_static/fonts/1383417807f7965daaf94e7c497dcddb.woff2 b/_static/fonts/1383417807f7965daaf94e7c497dcddb.woff2 new file mode 100644 index 0000000..d953103 Binary files /dev/null and b/_static/fonts/1383417807f7965daaf94e7c497dcddb.woff2 differ diff --git a/_static/fonts/144860ed1e48e186f08997e6388a9c3f.woff2 b/_static/fonts/144860ed1e48e186f08997e6388a9c3f.woff2 new file mode 100644 index 0000000..508baef Binary files /dev/null and b/_static/fonts/144860ed1e48e186f08997e6388a9c3f.woff2 differ diff --git a/_static/fonts/1488146d8b2e9859d6c90e6c2b48f7ef.woff2 b/_static/fonts/1488146d8b2e9859d6c90e6c2b48f7ef.woff2 new file mode 100644 index 0000000..bdad3df Binary files /dev/null and b/_static/fonts/1488146d8b2e9859d6c90e6c2b48f7ef.woff2 differ diff --git a/_static/fonts/1512b579343c6b61c7523cdd838d8328.ttf b/_static/fonts/1512b579343c6b61c7523cdd838d8328.ttf new file mode 100644 index 0000000..1a6895d Binary files /dev/null and b/_static/fonts/1512b579343c6b61c7523cdd838d8328.ttf differ diff --git a/_static/fonts/1c9cc76fd52238330f0aabac35acd2ca.woff2 b/_static/fonts/1c9cc76fd52238330f0aabac35acd2ca.woff2 new file mode 100644 index 0000000..cb9bfa7 Binary files /dev/null and b/_static/fonts/1c9cc76fd52238330f0aabac35acd2ca.woff2 differ diff --git a/_static/fonts/1f1481679a64a39f3427547aa1b13f0f.woff2 b/_static/fonts/1f1481679a64a39f3427547aa1b13f0f.woff2 new file mode 100644 index 0000000..6d458ad Binary files /dev/null and b/_static/fonts/1f1481679a64a39f3427547aa1b13f0f.woff2 differ diff --git a/_static/fonts/2096d27efc16cbdd79183bf295c8ebde.ttf b/_static/fonts/2096d27efc16cbdd79183bf295c8ebde.ttf new file mode 100644 index 0000000..2cf147e Binary files /dev/null and b/_static/fonts/2096d27efc16cbdd79183bf295c8ebde.ttf differ diff --git a/_static/fonts/20dc200cc43ab904876fb0c1697ebe39.woff2 b/_static/fonts/20dc200cc43ab904876fb0c1697ebe39.woff2 new file mode 100644 index 0000000..87711c0 Binary files /dev/null and b/_static/fonts/20dc200cc43ab904876fb0c1697ebe39.woff2 differ diff --git a/_static/fonts/214adfc289a2f2af8b0008c59ed0c7f2.woff2 b/_static/fonts/214adfc289a2f2af8b0008c59ed0c7f2.woff2 new file mode 100644 index 0000000..bc7e1b2 Binary files /dev/null and b/_static/fonts/214adfc289a2f2af8b0008c59ed0c7f2.woff2 differ diff --git a/_static/fonts/21953b998bab09c1f60c599caee56378.woff2 b/_static/fonts/21953b998bab09c1f60c599caee56378.woff2 new file mode 100644 index 0000000..d4ec189 Binary files /dev/null and b/_static/fonts/21953b998bab09c1f60c599caee56378.woff2 differ diff --git a/_static/fonts/22aadc77cafa07b2db9ed560d0320616.woff2 b/_static/fonts/22aadc77cafa07b2db9ed560d0320616.woff2 new file mode 100644 index 0000000..2950a77 Binary files /dev/null and b/_static/fonts/22aadc77cafa07b2db9ed560d0320616.woff2 differ diff --git a/_static/fonts/2325b97b584755067ea4f7f56ee05430.woff2 b/_static/fonts/2325b97b584755067ea4f7f56ee05430.woff2 new file mode 100644 index 0000000..51c88fd Binary files /dev/null and b/_static/fonts/2325b97b584755067ea4f7f56ee05430.woff2 differ diff --git a/_static/fonts/2550c2e2d8495c3ed2d4d52f824374f1.woff2 b/_static/fonts/2550c2e2d8495c3ed2d4d52f824374f1.woff2 new file mode 100644 index 0000000..81848e2 Binary files /dev/null and b/_static/fonts/2550c2e2d8495c3ed2d4d52f824374f1.woff2 differ diff --git a/_static/fonts/255cf41e0317d95e3992683a76ef28a8.woff2 b/_static/fonts/255cf41e0317d95e3992683a76ef28a8.woff2 new file mode 100644 index 0000000..7e93187 Binary files /dev/null and b/_static/fonts/255cf41e0317d95e3992683a76ef28a8.woff2 differ diff --git a/_static/fonts/25c52b9af13f0d1b10719f5289e8c803.woff2 b/_static/fonts/25c52b9af13f0d1b10719f5289e8c803.woff2 new file mode 100644 index 0000000..544eddc Binary files /dev/null and b/_static/fonts/25c52b9af13f0d1b10719f5289e8c803.woff2 differ diff --git a/_static/fonts/2781e9e7c3f369b8fc7965e679b17b60.woff2 b/_static/fonts/2781e9e7c3f369b8fc7965e679b17b60.woff2 new file mode 100644 index 0000000..cf61b88 Binary files /dev/null and b/_static/fonts/2781e9e7c3f369b8fc7965e679b17b60.woff2 differ diff --git a/_static/fonts/28e6b81b1bc1964707edd4179e4268f5.ttf b/_static/fonts/28e6b81b1bc1964707edd4179e4268f5.ttf new file mode 100644 index 0000000..d745504 Binary files /dev/null and b/_static/fonts/28e6b81b1bc1964707edd4179e4268f5.ttf differ diff --git a/_static/fonts/2a8c422bef4a7099e99dbf0e61ed5e49.woff2 b/_static/fonts/2a8c422bef4a7099e99dbf0e61ed5e49.woff2 new file mode 100644 index 0000000..c8091bc Binary files /dev/null and b/_static/fonts/2a8c422bef4a7099e99dbf0e61ed5e49.woff2 differ diff --git a/_static/fonts/2aadfad5aee7ceeaf4eb0924efabe5b4.ttf b/_static/fonts/2aadfad5aee7ceeaf4eb0924efabe5b4.ttf new file mode 100644 index 0000000..64fca94 Binary files /dev/null and b/_static/fonts/2aadfad5aee7ceeaf4eb0924efabe5b4.ttf differ diff --git a/_static/fonts/2c0f74be498d2da814c0a84dd6833f70.woff2 b/_static/fonts/2c0f74be498d2da814c0a84dd6833f70.woff2 new file mode 100644 index 0000000..8c63b51 Binary files /dev/null and b/_static/fonts/2c0f74be498d2da814c0a84dd6833f70.woff2 differ diff --git a/_static/fonts/2e10480d4154762bc7c8fbb40877e104.woff2 b/_static/fonts/2e10480d4154762bc7c8fbb40877e104.woff2 new file mode 100644 index 0000000..1f579aa Binary files /dev/null and b/_static/fonts/2e10480d4154762bc7c8fbb40877e104.woff2 differ diff --git a/_static/fonts/2ea7a97b7c976b121112a088eb398561.woff2 b/_static/fonts/2ea7a97b7c976b121112a088eb398561.woff2 new file mode 100644 index 0000000..e0d3c43 Binary files /dev/null and b/_static/fonts/2ea7a97b7c976b121112a088eb398561.woff2 differ diff --git a/_static/fonts/2f5c32f094829c0278bce28fe2bbe074.ttf b/_static/fonts/2f5c32f094829c0278bce28fe2bbe074.ttf new file mode 100644 index 0000000..ea5c8fe Binary files /dev/null and b/_static/fonts/2f5c32f094829c0278bce28fe2bbe074.ttf differ diff --git a/_static/fonts/2f7c3c315334a99574ee4ceb21af654d.woff2 b/_static/fonts/2f7c3c315334a99574ee4ceb21af654d.woff2 new file mode 100644 index 0000000..4b7a373 Binary files /dev/null and b/_static/fonts/2f7c3c315334a99574ee4ceb21af654d.woff2 differ diff --git a/_static/fonts/302b0425bf5ea66f37a822a61d723adc.ttf b/_static/fonts/302b0425bf5ea66f37a822a61d723adc.ttf new file mode 100644 index 0000000..d25425d Binary files /dev/null and b/_static/fonts/302b0425bf5ea66f37a822a61d723adc.ttf differ diff --git a/_static/fonts/3177dacffeac1eb4102852811ae4a2c7.woff2 b/_static/fonts/3177dacffeac1eb4102852811ae4a2c7.woff2 new file mode 100644 index 0000000..c5776a5 Binary files /dev/null and b/_static/fonts/3177dacffeac1eb4102852811ae4a2c7.woff2 differ diff --git a/_static/fonts/3254c528e2ab56454a9f22191035c5fe.ttf b/_static/fonts/3254c528e2ab56454a9f22191035c5fe.ttf new file mode 100644 index 0000000..9c48d22 Binary files /dev/null and b/_static/fonts/3254c528e2ab56454a9f22191035c5fe.ttf differ diff --git a/_static/fonts/32c8a74ac0816253d69a7cc68a60986d.woff2 b/_static/fonts/32c8a74ac0816253d69a7cc68a60986d.woff2 new file mode 100644 index 0000000..53d081f Binary files /dev/null and b/_static/fonts/32c8a74ac0816253d69a7cc68a60986d.woff2 differ diff --git a/_static/fonts/33c5d27ca0eaeb12ebe728ae2fc7106d.woff2 b/_static/fonts/33c5d27ca0eaeb12ebe728ae2fc7106d.woff2 new file mode 100644 index 0000000..45eae25 Binary files /dev/null and b/_static/fonts/33c5d27ca0eaeb12ebe728ae2fc7106d.woff2 differ diff --git a/_static/fonts/36e39c6463ae1c71c71e69c05e593e1b.woff2 b/_static/fonts/36e39c6463ae1c71c71e69c05e593e1b.woff2 new file mode 100644 index 0000000..f3c5f6a Binary files /dev/null and b/_static/fonts/36e39c6463ae1c71c71e69c05e593e1b.woff2 differ diff --git a/_static/fonts/3728fbdd191d75bad5b83a838dfe2fc1.woff2 b/_static/fonts/3728fbdd191d75bad5b83a838dfe2fc1.woff2 new file mode 100644 index 0000000..cb5834f Binary files /dev/null and b/_static/fonts/3728fbdd191d75bad5b83a838dfe2fc1.woff2 differ diff --git a/_static/fonts/38f3ee1f96b758f95672c632d8759594.ttf b/_static/fonts/38f3ee1f96b758f95672c632d8759594.ttf new file mode 100644 index 0000000..7c38f72 Binary files /dev/null and b/_static/fonts/38f3ee1f96b758f95672c632d8759594.ttf differ diff --git a/_static/fonts/392ff374142585f7b886ee1fe66e686e.woff2 b/_static/fonts/392ff374142585f7b886ee1fe66e686e.woff2 new file mode 100644 index 0000000..b1dc168 Binary files /dev/null and b/_static/fonts/392ff374142585f7b886ee1fe66e686e.woff2 differ diff --git a/_static/fonts/3a38c967413f7bce36d3baefc321aade.woff2 b/_static/fonts/3a38c967413f7bce36d3baefc321aade.woff2 new file mode 100644 index 0000000..53b8d0d Binary files /dev/null and b/_static/fonts/3a38c967413f7bce36d3baefc321aade.woff2 differ diff --git a/_static/fonts/3c23eb02de6b34e30f18cfb7167abd81.woff2 b/_static/fonts/3c23eb02de6b34e30f18cfb7167abd81.woff2 new file mode 100644 index 0000000..8a8de61 Binary files /dev/null and b/_static/fonts/3c23eb02de6b34e30f18cfb7167abd81.woff2 differ diff --git a/_static/fonts/3c505383d37d2078648e37868bbd1fad.woff2 b/_static/fonts/3c505383d37d2078648e37868bbd1fad.woff2 new file mode 100644 index 0000000..6399552 Binary files /dev/null and b/_static/fonts/3c505383d37d2078648e37868bbd1fad.woff2 differ diff --git a/_static/fonts/3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2 b/_static/fonts/3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2 new file mode 100644 index 0000000..59cab0f Binary files /dev/null and b/_static/fonts/3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2 differ diff --git a/_static/fonts/3f1918538864f9681d47a4538d48289c.woff2 b/_static/fonts/3f1918538864f9681d47a4538d48289c.woff2 new file mode 100644 index 0000000..99b1da1 Binary files /dev/null and b/_static/fonts/3f1918538864f9681d47a4538d48289c.woff2 differ diff --git a/_static/fonts/4039566f251699c4b421ed1a38a59b24.woff2 b/_static/fonts/4039566f251699c4b421ed1a38a59b24.woff2 new file mode 100644 index 0000000..45f222c Binary files /dev/null and b/_static/fonts/4039566f251699c4b421ed1a38a59b24.woff2 differ diff --git a/_static/fonts/4207cbc8cb7bc2cbd0bcce565298cbbc.woff2 b/_static/fonts/4207cbc8cb7bc2cbd0bcce565298cbbc.woff2 new file mode 100644 index 0000000..8ab9171 Binary files /dev/null and b/_static/fonts/4207cbc8cb7bc2cbd0bcce565298cbbc.woff2 differ diff --git a/_static/fonts/43358c04243de546caddd0898dbf0757.woff2 b/_static/fonts/43358c04243de546caddd0898dbf0757.woff2 new file mode 100644 index 0000000..1f38418 Binary files /dev/null and b/_static/fonts/43358c04243de546caddd0898dbf0757.woff2 differ diff --git a/_static/fonts/435e4b7f9f250d9d9243d4754799fc96.woff2 b/_static/fonts/435e4b7f9f250d9d9243d4754799fc96.woff2 new file mode 100644 index 0000000..0f6e60b Binary files /dev/null and b/_static/fonts/435e4b7f9f250d9d9243d4754799fc96.woff2 differ diff --git a/_static/fonts/437939342255944b82a49f916404c5fc.woff2 b/_static/fonts/437939342255944b82a49f916404c5fc.woff2 new file mode 100644 index 0000000..0bb2706 Binary files /dev/null and b/_static/fonts/437939342255944b82a49f916404c5fc.woff2 differ diff --git a/_static/fonts/455c2c1af0a2bf20047a1864d7d7c174.woff2 b/_static/fonts/455c2c1af0a2bf20047a1864d7d7c174.woff2 new file mode 100644 index 0000000..b289f00 Binary files /dev/null and b/_static/fonts/455c2c1af0a2bf20047a1864d7d7c174.woff2 differ diff --git a/_static/fonts/47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2 b/_static/fonts/47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2 new file mode 100644 index 0000000..9d7fb7f Binary files /dev/null and b/_static/fonts/47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2 differ diff --git a/_static/fonts/495d38d4b9741e8aa4204002414069e2.woff2 b/_static/fonts/495d38d4b9741e8aa4204002414069e2.woff2 new file mode 100644 index 0000000..47da362 Binary files /dev/null and b/_static/fonts/495d38d4b9741e8aa4204002414069e2.woff2 differ diff --git a/_static/fonts/4c815fdc869f885520f7c8eae6730edf.woff2 b/_static/fonts/4c815fdc869f885520f7c8eae6730edf.woff2 new file mode 100644 index 0000000..997a45c Binary files /dev/null and b/_static/fonts/4c815fdc869f885520f7c8eae6730edf.woff2 differ diff --git a/_static/fonts/4ec57f2a80b91090971b83970230ca09.woff2 b/_static/fonts/4ec57f2a80b91090971b83970230ca09.woff2 new file mode 100644 index 0000000..3c45011 Binary files /dev/null and b/_static/fonts/4ec57f2a80b91090971b83970230ca09.woff2 differ diff --git a/_static/fonts/4f17f22fc6bff4f3333ccf7ed7126e6d.woff2 b/_static/fonts/4f17f22fc6bff4f3333ccf7ed7126e6d.woff2 new file mode 100644 index 0000000..fb22fec Binary files /dev/null and b/_static/fonts/4f17f22fc6bff4f3333ccf7ed7126e6d.woff2 differ diff --git a/_static/fonts/4f93c2808e3b69e525c118074e5de31f.woff2 b/_static/fonts/4f93c2808e3b69e525c118074e5de31f.woff2 new file mode 100644 index 0000000..de10a3c Binary files /dev/null and b/_static/fonts/4f93c2808e3b69e525c118074e5de31f.woff2 differ diff --git a/_static/fonts/50aacf068f685be0dd903a91d5bab7d8.woff2 b/_static/fonts/50aacf068f685be0dd903a91d5bab7d8.woff2 new file mode 100644 index 0000000..2e71425 Binary files /dev/null and b/_static/fonts/50aacf068f685be0dd903a91d5bab7d8.woff2 differ diff --git a/_static/fonts/51f3f41805329fb8341beb56ded833ea.woff2 b/_static/fonts/51f3f41805329fb8341beb56ded833ea.woff2 new file mode 100644 index 0000000..c009987 Binary files /dev/null and b/_static/fonts/51f3f41805329fb8341beb56ded833ea.woff2 differ diff --git a/_static/fonts/52f28cb4d065b4adfa78df4f9559c639.woff2 b/_static/fonts/52f28cb4d065b4adfa78df4f9559c639.woff2 new file mode 100644 index 0000000..a0f4418 Binary files /dev/null and b/_static/fonts/52f28cb4d065b4adfa78df4f9559c639.woff2 differ diff --git a/_static/fonts/555ceea3a65ffbbecf8b7e6d04966c7f.woff2 b/_static/fonts/555ceea3a65ffbbecf8b7e6d04966c7f.woff2 new file mode 100644 index 0000000..3df1d8a Binary files /dev/null and b/_static/fonts/555ceea3a65ffbbecf8b7e6d04966c7f.woff2 differ diff --git a/_static/fonts/5989ef3a21d7f252337ab3326f78bde7.woff2 b/_static/fonts/5989ef3a21d7f252337ab3326f78bde7.woff2 new file mode 100644 index 0000000..100e81a Binary files /dev/null and b/_static/fonts/5989ef3a21d7f252337ab3326f78bde7.woff2 differ diff --git a/_static/fonts/5b6377da4c959db6d4b22738a27f1bee.woff2 b/_static/fonts/5b6377da4c959db6d4b22738a27f1bee.woff2 new file mode 100644 index 0000000..a0d68e2 Binary files /dev/null and b/_static/fonts/5b6377da4c959db6d4b22738a27f1bee.woff2 differ diff --git a/_static/fonts/5ce47d5195e59af38114d0b70217baf2.woff2 b/_static/fonts/5ce47d5195e59af38114d0b70217baf2.woff2 new file mode 100644 index 0000000..f496f9e Binary files /dev/null and b/_static/fonts/5ce47d5195e59af38114d0b70217baf2.woff2 differ diff --git a/_static/fonts/5d7ff31ac7bf945e8d61878f8a941239.woff2 b/_static/fonts/5d7ff31ac7bf945e8d61878f8a941239.woff2 new file mode 100644 index 0000000..dd5a4a2 Binary files /dev/null and b/_static/fonts/5d7ff31ac7bf945e8d61878f8a941239.woff2 differ diff --git a/_static/fonts/5dc0e4b14e903ba7f45c581df7402b3f.woff2 b/_static/fonts/5dc0e4b14e903ba7f45c581df7402b3f.woff2 new file mode 100644 index 0000000..c2862d7 Binary files /dev/null and b/_static/fonts/5dc0e4b14e903ba7f45c581df7402b3f.woff2 differ diff --git a/_static/fonts/60eb682678bbea5e8ad71f66f2f65536.woff2 b/_static/fonts/60eb682678bbea5e8ad71f66f2f65536.woff2 new file mode 100644 index 0000000..6a258ac Binary files /dev/null and b/_static/fonts/60eb682678bbea5e8ad71f66f2f65536.woff2 differ diff --git a/_static/fonts/63111d307c01b52ffccf7b0319cb7917.woff2 b/_static/fonts/63111d307c01b52ffccf7b0319cb7917.woff2 new file mode 100644 index 0000000..fecc185 Binary files /dev/null and b/_static/fonts/63111d307c01b52ffccf7b0319cb7917.woff2 differ diff --git a/_static/fonts/638764dc2513deb09c55fc025f6dd36c.woff2 b/_static/fonts/638764dc2513deb09c55fc025f6dd36c.woff2 new file mode 100644 index 0000000..122ac2c Binary files /dev/null and b/_static/fonts/638764dc2513deb09c55fc025f6dd36c.woff2 differ diff --git a/_static/fonts/63f4b74ebf127dbeb033126ea988f54e.woff2 b/_static/fonts/63f4b74ebf127dbeb033126ea988f54e.woff2 new file mode 100644 index 0000000..00a4a4b Binary files /dev/null and b/_static/fonts/63f4b74ebf127dbeb033126ea988f54e.woff2 differ diff --git a/_static/fonts/64a6b4e954cf84685cbf8de77eb47344.woff2 b/_static/fonts/64a6b4e954cf84685cbf8de77eb47344.woff2 new file mode 100644 index 0000000..edfc6cd Binary files /dev/null and b/_static/fonts/64a6b4e954cf84685cbf8de77eb47344.woff2 differ diff --git a/_static/fonts/661d4b208656c006e7aab58acf778485.woff2 b/_static/fonts/661d4b208656c006e7aab58acf778485.woff2 new file mode 100644 index 0000000..ae1933f Binary files /dev/null and b/_static/fonts/661d4b208656c006e7aab58acf778485.woff2 differ diff --git a/_static/fonts/6725a7e91680edd1cdc9ed5c26ac05fd.woff2 b/_static/fonts/6725a7e91680edd1cdc9ed5c26ac05fd.woff2 new file mode 100644 index 0000000..ece005f Binary files /dev/null and b/_static/fonts/6725a7e91680edd1cdc9ed5c26ac05fd.woff2 differ diff --git a/_static/fonts/6a84eeee6a25e7c9a8a03191007a6720.woff2 b/_static/fonts/6a84eeee6a25e7c9a8a03191007a6720.woff2 new file mode 100644 index 0000000..1bb7737 Binary files /dev/null and b/_static/fonts/6a84eeee6a25e7c9a8a03191007a6720.woff2 differ diff --git a/_static/fonts/6ac1ee292434fac2313c42b0dfb7897c.ttf b/_static/fonts/6ac1ee292434fac2313c42b0dfb7897c.ttf new file mode 100644 index 0000000..3e69acb Binary files /dev/null and b/_static/fonts/6ac1ee292434fac2313c42b0dfb7897c.ttf differ diff --git a/_static/fonts/6ad3f6bbe6220cc476a0d3c731d3fb04.ttf b/_static/fonts/6ad3f6bbe6220cc476a0d3c731d3fb04.ttf new file mode 100644 index 0000000..563f449 Binary files /dev/null and b/_static/fonts/6ad3f6bbe6220cc476a0d3c731d3fb04.ttf differ diff --git a/_static/fonts/6be97ca17228a69c406231d89c003194.woff2 b/_static/fonts/6be97ca17228a69c406231d89c003194.woff2 new file mode 100644 index 0000000..a56a6ed Binary files /dev/null and b/_static/fonts/6be97ca17228a69c406231d89c003194.woff2 differ diff --git a/_static/fonts/6de03a64aa8100032abc6e836b3ed803.ttf b/_static/fonts/6de03a64aa8100032abc6e836b3ed803.ttf new file mode 100644 index 0000000..604934a Binary files /dev/null and b/_static/fonts/6de03a64aa8100032abc6e836b3ed803.ttf differ diff --git a/_static/fonts/6deb20301c65a96db17c433ad0cf8158.woff2 b/_static/fonts/6deb20301c65a96db17c433ad0cf8158.woff2 new file mode 100644 index 0000000..cbe564b Binary files /dev/null and b/_static/fonts/6deb20301c65a96db17c433ad0cf8158.woff2 differ diff --git a/_static/fonts/6f8d857c5a8545e67de6b60aa0fe5c33.woff2 b/_static/fonts/6f8d857c5a8545e67de6b60aa0fe5c33.woff2 new file mode 100644 index 0000000..92fe38d Binary files /dev/null and b/_static/fonts/6f8d857c5a8545e67de6b60aa0fe5c33.woff2 differ diff --git a/_static/fonts/713780d8b30bda5583052ea847cdcb4f.woff2 b/_static/fonts/713780d8b30bda5583052ea847cdcb4f.woff2 new file mode 100644 index 0000000..0933dfe Binary files /dev/null and b/_static/fonts/713780d8b30bda5583052ea847cdcb4f.woff2 differ diff --git a/_static/fonts/71e06579279fba7436d58a1c49288909.ttf b/_static/fonts/71e06579279fba7436d58a1c49288909.ttf new file mode 100644 index 0000000..454e5d0 Binary files /dev/null and b/_static/fonts/71e06579279fba7436d58a1c49288909.ttf differ diff --git a/_static/fonts/765bd4a97597a4d7781193793477a6cd.ttf b/_static/fonts/765bd4a97597a4d7781193793477a6cd.ttf new file mode 100644 index 0000000..10f8720 Binary files /dev/null and b/_static/fonts/765bd4a97597a4d7781193793477a6cd.ttf differ diff --git a/_static/fonts/76945c7494c20515bb45d1dedab8f706.woff2 b/_static/fonts/76945c7494c20515bb45d1dedab8f706.woff2 new file mode 100644 index 0000000..943c5a0 Binary files /dev/null and b/_static/fonts/76945c7494c20515bb45d1dedab8f706.woff2 differ diff --git a/_static/fonts/76da333ab59c6d625cabfb0768f82b4a.woff2 b/_static/fonts/76da333ab59c6d625cabfb0768f82b4a.woff2 new file mode 100644 index 0000000..9eda94a Binary files /dev/null and b/_static/fonts/76da333ab59c6d625cabfb0768f82b4a.woff2 differ diff --git a/_static/fonts/770518db51bed1e082feecc532cfcbf8.woff2 b/_static/fonts/770518db51bed1e082feecc532cfcbf8.woff2 new file mode 100644 index 0000000..c0f0f73 Binary files /dev/null and b/_static/fonts/770518db51bed1e082feecc532cfcbf8.woff2 differ diff --git a/_static/fonts/77b24796a3d4ab521f66765651875338.woff2 b/_static/fonts/77b24796a3d4ab521f66765651875338.woff2 new file mode 100644 index 0000000..6284d2e Binary files /dev/null and b/_static/fonts/77b24796a3d4ab521f66765651875338.woff2 differ diff --git a/_static/fonts/77ff81100e5a1db3d925f713660700ad.woff2 b/_static/fonts/77ff81100e5a1db3d925f713660700ad.woff2 new file mode 100644 index 0000000..dd0851d Binary files /dev/null and b/_static/fonts/77ff81100e5a1db3d925f713660700ad.woff2 differ diff --git a/_static/fonts/78a9265759e7b861a1639a36f4c01d04.woff2 b/_static/fonts/78a9265759e7b861a1639a36f4c01d04.woff2 new file mode 100644 index 0000000..be81ddf Binary files /dev/null and b/_static/fonts/78a9265759e7b861a1639a36f4c01d04.woff2 differ diff --git a/_static/fonts/7af61b2367eba2b1852e837c46a75696.woff2 b/_static/fonts/7af61b2367eba2b1852e837c46a75696.woff2 new file mode 100644 index 0000000..e0d4123 Binary files /dev/null and b/_static/fonts/7af61b2367eba2b1852e837c46a75696.woff2 differ diff --git a/_static/fonts/7b63598dcc2a26583b82594bd0e36d5b.woff2 b/_static/fonts/7b63598dcc2a26583b82594bd0e36d5b.woff2 new file mode 100644 index 0000000..ff1f96d Binary files /dev/null and b/_static/fonts/7b63598dcc2a26583b82594bd0e36d5b.woff2 differ diff --git a/_static/fonts/7b8c2179b6b778308d2ff39bdb82e926.woff2 b/_static/fonts/7b8c2179b6b778308d2ff39bdb82e926.woff2 new file mode 100644 index 0000000..66efc25 Binary files /dev/null and b/_static/fonts/7b8c2179b6b778308d2ff39bdb82e926.woff2 differ diff --git a/_static/fonts/7e262106f82cc52663e403f5b73795bb.woff2 b/_static/fonts/7e262106f82cc52663e403f5b73795bb.woff2 new file mode 100644 index 0000000..067cb32 Binary files /dev/null and b/_static/fonts/7e262106f82cc52663e403f5b73795bb.woff2 differ diff --git a/_static/fonts/7f1c829b0c90fd664a03bb714a74f7d3.woff2 b/_static/fonts/7f1c829b0c90fd664a03bb714a74f7d3.woff2 new file mode 100644 index 0000000..68f094c Binary files /dev/null and b/_static/fonts/7f1c829b0c90fd664a03bb714a74f7d3.woff2 differ diff --git a/_static/fonts/7fa86b886bee5d6ab420a8e89b9f3052.ttf b/_static/fonts/7fa86b886bee5d6ab420a8e89b9f3052.ttf new file mode 100644 index 0000000..13a7f42 Binary files /dev/null and b/_static/fonts/7fa86b886bee5d6ab420a8e89b9f3052.ttf differ diff --git a/_static/fonts/8007dfe835cfb201b8caaa9651098588.woff2 b/_static/fonts/8007dfe835cfb201b8caaa9651098588.woff2 new file mode 100644 index 0000000..11c7018 Binary files /dev/null and b/_static/fonts/8007dfe835cfb201b8caaa9651098588.woff2 differ diff --git a/_static/fonts/83614c36460a4a9734968789cb535de7.woff2 b/_static/fonts/83614c36460a4a9734968789cb535de7.woff2 new file mode 100644 index 0000000..e836b51 Binary files /dev/null and b/_static/fonts/83614c36460a4a9734968789cb535de7.woff2 differ diff --git a/_static/fonts/84e959dd07f302392f0ffd86f87db888.ttf b/_static/fonts/84e959dd07f302392f0ffd86f87db888.ttf new file mode 100644 index 0000000..99c5795 Binary files /dev/null and b/_static/fonts/84e959dd07f302392f0ffd86f87db888.ttf differ diff --git a/_static/fonts/85a41b80c5fdc14e3dc48636a30d87dd.woff2 b/_static/fonts/85a41b80c5fdc14e3dc48636a30d87dd.woff2 new file mode 100644 index 0000000..8a81a2f Binary files /dev/null and b/_static/fonts/85a41b80c5fdc14e3dc48636a30d87dd.woff2 differ diff --git a/_static/fonts/870e5928dd14fcfe0ce9386107666774.woff2 b/_static/fonts/870e5928dd14fcfe0ce9386107666774.woff2 new file mode 100644 index 0000000..ef9a2bd Binary files /dev/null and b/_static/fonts/870e5928dd14fcfe0ce9386107666774.woff2 differ diff --git a/_static/fonts/8898c4b754d5d96c1a5e1b1d54100554.woff2 b/_static/fonts/8898c4b754d5d96c1a5e1b1d54100554.woff2 new file mode 100644 index 0000000..6b05020 Binary files /dev/null and b/_static/fonts/8898c4b754d5d96c1a5e1b1d54100554.woff2 differ diff --git a/_static/fonts/89b4f174a5a728d2d8c85b87990c9ab4.ttf b/_static/fonts/89b4f174a5a728d2d8c85b87990c9ab4.ttf new file mode 100644 index 0000000..57a6dc3 Binary files /dev/null and b/_static/fonts/89b4f174a5a728d2d8c85b87990c9ab4.ttf differ diff --git a/_static/fonts/8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2 b/_static/fonts/8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2 new file mode 100644 index 0000000..9756ba7 Binary files /dev/null and b/_static/fonts/8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2 differ diff --git a/_static/fonts/8aa562790559d61dd5178a88a296d70f.ttf b/_static/fonts/8aa562790559d61dd5178a88a296d70f.ttf new file mode 100644 index 0000000..68ff2a4 Binary files /dev/null and b/_static/fonts/8aa562790559d61dd5178a88a296d70f.ttf differ diff --git a/_static/fonts/8c3798e37724f71bc0c63c44a5307413.woff2 b/_static/fonts/8c3798e37724f71bc0c63c44a5307413.woff2 new file mode 100644 index 0000000..72fc021 Binary files /dev/null and b/_static/fonts/8c3798e37724f71bc0c63c44a5307413.woff2 differ diff --git a/_static/fonts/8c49ed8b472d38d3985ec9bbbccea601.ttf b/_static/fonts/8c49ed8b472d38d3985ec9bbbccea601.ttf new file mode 100644 index 0000000..9c039d6 Binary files /dev/null and b/_static/fonts/8c49ed8b472d38d3985ec9bbbccea601.ttf differ diff --git a/_static/fonts/8e48cf20cf9f9e5feb7197c79028132b.woff2 b/_static/fonts/8e48cf20cf9f9e5feb7197c79028132b.woff2 new file mode 100644 index 0000000..f8d7a0d Binary files /dev/null and b/_static/fonts/8e48cf20cf9f9e5feb7197c79028132b.woff2 differ diff --git a/_static/fonts/9095d663e4d450059bcc2260bb75cd62.woff2 b/_static/fonts/9095d663e4d450059bcc2260bb75cd62.woff2 new file mode 100644 index 0000000..472bf5a Binary files /dev/null and b/_static/fonts/9095d663e4d450059bcc2260bb75cd62.woff2 differ diff --git a/_static/fonts/90ebb29b5cffa197b184773983ba7e91.woff2 b/_static/fonts/90ebb29b5cffa197b184773983ba7e91.woff2 new file mode 100644 index 0000000..5fd1029 Binary files /dev/null and b/_static/fonts/90ebb29b5cffa197b184773983ba7e91.woff2 differ diff --git a/_static/fonts/93b6c99d936df38895a0d95e3ffea2fd.woff2 b/_static/fonts/93b6c99d936df38895a0d95e3ffea2fd.woff2 new file mode 100644 index 0000000..cb00b8b Binary files /dev/null and b/_static/fonts/93b6c99d936df38895a0d95e3ffea2fd.woff2 differ diff --git a/_static/fonts/9582ced8a675bf267cc7ac392a86413e.woff2 b/_static/fonts/9582ced8a675bf267cc7ac392a86413e.woff2 new file mode 100644 index 0000000..ef920e5 Binary files /dev/null and b/_static/fonts/9582ced8a675bf267cc7ac392a86413e.woff2 differ diff --git a/_static/fonts/99be4d68845d66c27c7f7d3a48687b66.woff2 b/_static/fonts/99be4d68845d66c27c7f7d3a48687b66.woff2 new file mode 100644 index 0000000..3f7f93a Binary files /dev/null and b/_static/fonts/99be4d68845d66c27c7f7d3a48687b66.woff2 differ diff --git a/_static/fonts/99cf36e763be9cce7b4c59b91841af58.woff2 b/_static/fonts/99cf36e763be9cce7b4c59b91841af58.woff2 new file mode 100644 index 0000000..663ec4c Binary files /dev/null and b/_static/fonts/99cf36e763be9cce7b4c59b91841af58.woff2 differ diff --git a/_static/fonts/9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2 b/_static/fonts/9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2 new file mode 100644 index 0000000..d0c5aa5 Binary files /dev/null and b/_static/fonts/9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2 differ diff --git a/_static/fonts/9bcbc88b33b2efc2aee821b831499f1c.woff2 b/_static/fonts/9bcbc88b33b2efc2aee821b831499f1c.woff2 new file mode 100644 index 0000000..3f060b3 Binary files /dev/null and b/_static/fonts/9bcbc88b33b2efc2aee821b831499f1c.woff2 differ diff --git a/_static/fonts/9c9be791a58af8a04c611ca1d13f51c6.woff2 b/_static/fonts/9c9be791a58af8a04c611ca1d13f51c6.woff2 new file mode 100644 index 0000000..2f2cacd Binary files /dev/null and b/_static/fonts/9c9be791a58af8a04c611ca1d13f51c6.woff2 differ diff --git a/_static/fonts/9fdb12ceee3a402d3a54afe354552459.woff2 b/_static/fonts/9fdb12ceee3a402d3a54afe354552459.woff2 new file mode 100644 index 0000000..1d173f2 Binary files /dev/null and b/_static/fonts/9fdb12ceee3a402d3a54afe354552459.woff2 differ diff --git a/_static/fonts/a6933e678530b263486fa7b185a449ca.woff2 b/_static/fonts/a6933e678530b263486fa7b185a449ca.woff2 new file mode 100644 index 0000000..dd587a2 Binary files /dev/null and b/_static/fonts/a6933e678530b263486fa7b185a449ca.woff2 differ diff --git a/_static/fonts/a6caf7b9888eb0c382948c1ca5e8bebb.woff2 b/_static/fonts/a6caf7b9888eb0c382948c1ca5e8bebb.woff2 new file mode 100644 index 0000000..9213da0 Binary files /dev/null and b/_static/fonts/a6caf7b9888eb0c382948c1ca5e8bebb.woff2 differ diff --git a/_static/fonts/a70ff2592da5e3453943f727633aff54.woff2 b/_static/fonts/a70ff2592da5e3453943f727633aff54.woff2 new file mode 100644 index 0000000..c932cd4 Binary files /dev/null and b/_static/fonts/a70ff2592da5e3453943f727633aff54.woff2 differ diff --git a/_static/fonts/aa28d99c7db60ad23f96a5c317615c42.woff2 b/_static/fonts/aa28d99c7db60ad23f96a5c317615c42.woff2 new file mode 100644 index 0000000..cce41ce Binary files /dev/null and b/_static/fonts/aa28d99c7db60ad23f96a5c317615c42.woff2 differ diff --git a/_static/fonts/aab05142e0e2dadf7df633e061e612ad.woff2 b/_static/fonts/aab05142e0e2dadf7df633e061e612ad.woff2 new file mode 100644 index 0000000..c8ab3b2 Binary files /dev/null and b/_static/fonts/aab05142e0e2dadf7df633e061e612ad.woff2 differ diff --git a/_static/fonts/ab03beb9091fa15ce4e783199e076bc6.woff2 b/_static/fonts/ab03beb9091fa15ce4e783199e076bc6.woff2 new file mode 100644 index 0000000..477887e Binary files /dev/null and b/_static/fonts/ab03beb9091fa15ce4e783199e076bc6.woff2 differ diff --git a/_static/fonts/ac848474638236e67a64bc654fb18de0.ttf b/_static/fonts/ac848474638236e67a64bc654fb18de0.ttf new file mode 100644 index 0000000..aa6a46d Binary files /dev/null and b/_static/fonts/ac848474638236e67a64bc654fb18de0.ttf differ diff --git a/_static/fonts/acaac043ca238f0e56e61864456777fa.woff2 b/_static/fonts/acaac043ca238f0e56e61864456777fa.woff2 new file mode 100644 index 0000000..c88b8ae Binary files /dev/null and b/_static/fonts/acaac043ca238f0e56e61864456777fa.woff2 differ diff --git a/_static/fonts/aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2 b/_static/fonts/aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2 new file mode 100644 index 0000000..8571683 Binary files /dev/null and b/_static/fonts/aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2 differ diff --git a/_static/fonts/b019538234514166ec7665359d097403.woff2 b/_static/fonts/b019538234514166ec7665359d097403.woff2 new file mode 100644 index 0000000..29342a8 Binary files /dev/null and b/_static/fonts/b019538234514166ec7665359d097403.woff2 differ diff --git a/_static/fonts/b076e86301cbee8c5c9aef51863a9c0a.woff2 b/_static/fonts/b076e86301cbee8c5c9aef51863a9c0a.woff2 new file mode 100644 index 0000000..91231c9 Binary files /dev/null and b/_static/fonts/b076e86301cbee8c5c9aef51863a9c0a.woff2 differ diff --git a/_static/fonts/b19ac4e57f2a56639eebd1c35319e5a7.woff2 b/_static/fonts/b19ac4e57f2a56639eebd1c35319e5a7.woff2 new file mode 100644 index 0000000..4ccadd1 Binary files /dev/null and b/_static/fonts/b19ac4e57f2a56639eebd1c35319e5a7.woff2 differ diff --git a/_static/fonts/b4d3c40a77fd9e35a881a79077957055.woff2 b/_static/fonts/b4d3c40a77fd9e35a881a79077957055.woff2 new file mode 100644 index 0000000..38d4c74 Binary files /dev/null and b/_static/fonts/b4d3c40a77fd9e35a881a79077957055.woff2 differ diff --git a/_static/fonts/b4e42731e8d667ae87c3450c345754ae.woff2 b/_static/fonts/b4e42731e8d667ae87c3450c345754ae.woff2 new file mode 100644 index 0000000..ee64c93 Binary files /dev/null and b/_static/fonts/b4e42731e8d667ae87c3450c345754ae.woff2 differ diff --git a/_static/fonts/b57a5ada789f195d5d42f4073a6cf313.woff2 b/_static/fonts/b57a5ada789f195d5d42f4073a6cf313.woff2 new file mode 100644 index 0000000..18c3f12 Binary files /dev/null and b/_static/fonts/b57a5ada789f195d5d42f4073a6cf313.woff2 differ diff --git a/_static/fonts/b5b4146d87e5d22d0a4e0d04f3ee5626.woff2 b/_static/fonts/b5b4146d87e5d22d0a4e0d04f3ee5626.woff2 new file mode 100644 index 0000000..a4699c7 Binary files /dev/null and b/_static/fonts/b5b4146d87e5d22d0a4e0d04f3ee5626.woff2 differ diff --git a/_static/fonts/b7ef2cd1159a8cbfd271ff2abe07f237.woff2 b/_static/fonts/b7ef2cd1159a8cbfd271ff2abe07f237.woff2 new file mode 100644 index 0000000..22ddee9 Binary files /dev/null and b/_static/fonts/b7ef2cd1159a8cbfd271ff2abe07f237.woff2 differ diff --git a/_static/fonts/b93199bb6f964f190f4da04ecdbaf5a4.woff2 b/_static/fonts/b93199bb6f964f190f4da04ecdbaf5a4.woff2 new file mode 100644 index 0000000..fa67bec Binary files /dev/null and b/_static/fonts/b93199bb6f964f190f4da04ecdbaf5a4.woff2 differ diff --git a/_static/fonts/bb8007225d94a099cddbade7ea904667.woff2 b/_static/fonts/bb8007225d94a099cddbade7ea904667.woff2 new file mode 100644 index 0000000..22c57b0 Binary files /dev/null and b/_static/fonts/bb8007225d94a099cddbade7ea904667.woff2 differ diff --git a/_static/fonts/bc67bba106323289ea3eda0826de1912.ttf b/_static/fonts/bc67bba106323289ea3eda0826de1912.ttf new file mode 100644 index 0000000..ce5a525 Binary files /dev/null and b/_static/fonts/bc67bba106323289ea3eda0826de1912.ttf differ diff --git a/_static/fonts/bcd47c2f3649cfcaa86a08fb741255d6.woff2 b/_static/fonts/bcd47c2f3649cfcaa86a08fb741255d6.woff2 new file mode 100644 index 0000000..20eb7ec Binary files /dev/null and b/_static/fonts/bcd47c2f3649cfcaa86a08fb741255d6.woff2 differ diff --git a/_static/fonts/bd0efe13f0d9d591b337ddc7f289f494.woff2 b/_static/fonts/bd0efe13f0d9d591b337ddc7f289f494.woff2 new file mode 100644 index 0000000..2900346 Binary files /dev/null and b/_static/fonts/bd0efe13f0d9d591b337ddc7f289f494.woff2 differ diff --git a/_static/fonts/bd51fb0ca67e64c809ffcf7e1370f969.woff2 b/_static/fonts/bd51fb0ca67e64c809ffcf7e1370f969.woff2 new file mode 100644 index 0000000..0f11176 Binary files /dev/null and b/_static/fonts/bd51fb0ca67e64c809ffcf7e1370f969.woff2 differ diff --git a/_static/fonts/bdbb6b52604c2451fdcba9cdfd44f4e1.woff2 b/_static/fonts/bdbb6b52604c2451fdcba9cdfd44f4e1.woff2 new file mode 100644 index 0000000..6363b1c Binary files /dev/null and b/_static/fonts/bdbb6b52604c2451fdcba9cdfd44f4e1.woff2 differ diff --git a/_static/fonts/bf2ad3287f13eb7076cccb516ec2986f.ttf b/_static/fonts/bf2ad3287f13eb7076cccb516ec2986f.ttf new file mode 100644 index 0000000..99e1e89 Binary files /dev/null and b/_static/fonts/bf2ad3287f13eb7076cccb516ec2986f.ttf differ diff --git a/_static/fonts/bfd1a0c9c783e84595589f33e1828a57.woff2 b/_static/fonts/bfd1a0c9c783e84595589f33e1828a57.woff2 new file mode 100644 index 0000000..27773c9 Binary files /dev/null and b/_static/fonts/bfd1a0c9c783e84595589f33e1828a57.woff2 differ diff --git a/_static/fonts/c13b34dd5b6a35b309944b61c91b2ace.woff2 b/_static/fonts/c13b34dd5b6a35b309944b61c91b2ace.woff2 new file mode 100644 index 0000000..b2ffb56 Binary files /dev/null and b/_static/fonts/c13b34dd5b6a35b309944b61c91b2ace.woff2 differ diff --git a/_static/fonts/c22066c14662d6c80415ae04c5dd9d51.woff2 b/_static/fonts/c22066c14662d6c80415ae04c5dd9d51.woff2 new file mode 100644 index 0000000..7148fac Binary files /dev/null and b/_static/fonts/c22066c14662d6c80415ae04c5dd9d51.woff2 differ diff --git a/_static/fonts/c28a41f656599f6694528b5463c6a445.woff2 b/_static/fonts/c28a41f656599f6694528b5463c6a445.woff2 new file mode 100644 index 0000000..fcadc91 Binary files /dev/null and b/_static/fonts/c28a41f656599f6694528b5463c6a445.woff2 differ diff --git a/_static/fonts/c6dc61b627bbc5af9130518297bd4f17.ttf b/_static/fonts/c6dc61b627bbc5af9130518297bd4f17.ttf new file mode 100644 index 0000000..7a8b630 Binary files /dev/null and b/_static/fonts/c6dc61b627bbc5af9130518297bd4f17.ttf differ diff --git a/_static/fonts/c8a9fd4eab4e83382cc66fde70911b41.woff2 b/_static/fonts/c8a9fd4eab4e83382cc66fde70911b41.woff2 new file mode 100644 index 0000000..d2f30b5 Binary files /dev/null and b/_static/fonts/c8a9fd4eab4e83382cc66fde70911b41.woff2 differ diff --git a/_static/fonts/ca7eea0cf248d6e8442c01074765bd33.woff2 b/_static/fonts/ca7eea0cf248d6e8442c01074765bd33.woff2 new file mode 100644 index 0000000..24a1bfd Binary files /dev/null and b/_static/fonts/ca7eea0cf248d6e8442c01074765bd33.woff2 differ diff --git a/_static/fonts/cadfb311297a9362b07fab73934b432a.ttf b/_static/fonts/cadfb311297a9362b07fab73934b432a.ttf new file mode 100644 index 0000000..4242da4 Binary files /dev/null and b/_static/fonts/cadfb311297a9362b07fab73934b432a.ttf differ diff --git a/_static/fonts/cbfd26d5bcf084ee407a0b2b7599e84b.woff2 b/_static/fonts/cbfd26d5bcf084ee407a0b2b7599e84b.woff2 new file mode 100644 index 0000000..65687e7 Binary files /dev/null and b/_static/fonts/cbfd26d5bcf084ee407a0b2b7599e84b.woff2 differ diff --git a/_static/fonts/ccdebed88064e470c15f37c432922e57.woff2 b/_static/fonts/ccdebed88064e470c15f37c432922e57.woff2 new file mode 100644 index 0000000..6abf54d Binary files /dev/null and b/_static/fonts/ccdebed88064e470c15f37c432922e57.woff2 differ diff --git a/_static/fonts/cce2217cc8323fe49789adefb3596291.woff2 b/_static/fonts/cce2217cc8323fe49789adefb3596291.woff2 new file mode 100644 index 0000000..b8dff97 Binary files /dev/null and b/_static/fonts/cce2217cc8323fe49789adefb3596291.woff2 differ diff --git a/_static/fonts/cd3d1f17e048e2116f438bd7157baccf.woff2 b/_static/fonts/cd3d1f17e048e2116f438bd7157baccf.woff2 new file mode 100644 index 0000000..93cd525 Binary files /dev/null and b/_static/fonts/cd3d1f17e048e2116f438bd7157baccf.woff2 differ diff --git a/_static/fonts/d07f561ba87d93460742b060727d9e0d.woff2 b/_static/fonts/d07f561ba87d93460742b060727d9e0d.woff2 new file mode 100644 index 0000000..bfa05a0 Binary files /dev/null and b/_static/fonts/d07f561ba87d93460742b060727d9e0d.woff2 differ diff --git a/_static/fonts/d368cf5bed7856dbafa2af36b51acb9c.woff2 b/_static/fonts/d368cf5bed7856dbafa2af36b51acb9c.woff2 new file mode 100644 index 0000000..72ce0e9 Binary files /dev/null and b/_static/fonts/d368cf5bed7856dbafa2af36b51acb9c.woff2 differ diff --git a/_static/fonts/d422317033deb87342a5e56c7be67458.ttf b/_static/fonts/d422317033deb87342a5e56c7be67458.ttf new file mode 100644 index 0000000..6a9fc93 Binary files /dev/null and b/_static/fonts/d422317033deb87342a5e56c7be67458.ttf differ diff --git a/_static/fonts/d6f9cdf1a40893111566fcdee3bbe5a9.woff2 b/_static/fonts/d6f9cdf1a40893111566fcdee3bbe5a9.woff2 new file mode 100644 index 0000000..b9cee29 Binary files /dev/null and b/_static/fonts/d6f9cdf1a40893111566fcdee3bbe5a9.woff2 differ diff --git a/_static/fonts/d98f35e926c11f3d5c0c8e3205d43907.ttf b/_static/fonts/d98f35e926c11f3d5c0c8e3205d43907.ttf new file mode 100644 index 0000000..11a1e9f Binary files /dev/null and b/_static/fonts/d98f35e926c11f3d5c0c8e3205d43907.ttf differ diff --git a/_static/fonts/d9e6a498dac7e9e91f6e0b4f8930eba0.woff2 b/_static/fonts/d9e6a498dac7e9e91f6e0b4f8930eba0.woff2 new file mode 100644 index 0000000..0b792b0 Binary files /dev/null and b/_static/fonts/d9e6a498dac7e9e91f6e0b4f8930eba0.woff2 differ diff --git a/_static/fonts/da6cd48e6dad1888fccc91735e7522f7.woff2 b/_static/fonts/da6cd48e6dad1888fccc91735e7522f7.woff2 new file mode 100644 index 0000000..fe8fcec Binary files /dev/null and b/_static/fonts/da6cd48e6dad1888fccc91735e7522f7.woff2 differ diff --git a/_static/fonts/daf12b5f1889502004bba85ad71f9fa4.woff2 b/_static/fonts/daf12b5f1889502004bba85ad71f9fa4.woff2 new file mode 100644 index 0000000..3d0f604 Binary files /dev/null and b/_static/fonts/daf12b5f1889502004bba85ad71f9fa4.woff2 differ diff --git a/_static/fonts/daf51ab540602b2d0b87646621637bac.woff2 b/_static/fonts/daf51ab540602b2d0b87646621637bac.woff2 new file mode 100644 index 0000000..fc71d94 Binary files /dev/null and b/_static/fonts/daf51ab540602b2d0b87646621637bac.woff2 differ diff --git a/_static/fonts/db0424fb67fb52e7e538490240cc7fb9.woff2 b/_static/fonts/db0424fb67fb52e7e538490240cc7fb9.woff2 new file mode 100644 index 0000000..e1b7a79 Binary files /dev/null and b/_static/fonts/db0424fb67fb52e7e538490240cc7fb9.woff2 differ diff --git a/_static/fonts/dc25cbf4baaf778bd8ae78fbc0e79479.woff2 b/_static/fonts/dc25cbf4baaf778bd8ae78fbc0e79479.woff2 new file mode 100644 index 0000000..75d29cf Binary files /dev/null and b/_static/fonts/dc25cbf4baaf778bd8ae78fbc0e79479.woff2 differ diff --git a/_static/fonts/dd719f1662079ce6a61260f9af972379.woff2 b/_static/fonts/dd719f1662079ce6a61260f9af972379.woff2 new file mode 100644 index 0000000..4481927 Binary files /dev/null and b/_static/fonts/dd719f1662079ce6a61260f9af972379.woff2 differ diff --git a/_static/fonts/de018865c95896bb57265fc97c48ebd7.woff2 b/_static/fonts/de018865c95896bb57265fc97c48ebd7.woff2 new file mode 100644 index 0000000..a181dfe Binary files /dev/null and b/_static/fonts/de018865c95896bb57265fc97c48ebd7.woff2 differ diff --git a/_static/fonts/e33716333704ab19fdf9989e072ad49a.woff2 b/_static/fonts/e33716333704ab19fdf9989e072ad49a.woff2 new file mode 100644 index 0000000..b2391b9 Binary files /dev/null and b/_static/fonts/e33716333704ab19fdf9989e072ad49a.woff2 differ diff --git a/_static/fonts/e56cc9fb5272752b78f144b4be43175d.woff2 b/_static/fonts/e56cc9fb5272752b78f144b4be43175d.woff2 new file mode 100644 index 0000000..9997e98 Binary files /dev/null and b/_static/fonts/e56cc9fb5272752b78f144b4be43175d.woff2 differ diff --git a/_static/fonts/e704ef18719c08839bc99a32437ef0f8.woff2 b/_static/fonts/e704ef18719c08839bc99a32437ef0f8.woff2 new file mode 100644 index 0000000..bfcc76f Binary files /dev/null and b/_static/fonts/e704ef18719c08839bc99a32437ef0f8.woff2 differ diff --git a/_static/fonts/e99627cd27de169d23ece4573006af2a.woff2 b/_static/fonts/e99627cd27de169d23ece4573006af2a.woff2 new file mode 100644 index 0000000..677de8b Binary files /dev/null and b/_static/fonts/e99627cd27de169d23ece4573006af2a.woff2 differ diff --git a/_static/fonts/ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2 b/_static/fonts/ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2 new file mode 100644 index 0000000..064e94b Binary files /dev/null and b/_static/fonts/ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2 differ diff --git a/_static/fonts/f154d62b4879af7a22895af7a4ef03f0.woff2 b/_static/fonts/f154d62b4879af7a22895af7a4ef03f0.woff2 new file mode 100644 index 0000000..074504d Binary files /dev/null and b/_static/fonts/f154d62b4879af7a22895af7a4ef03f0.woff2 differ diff --git a/_static/fonts/f17ee050ada0453f3bd07bc466c2dde2.woff2 b/_static/fonts/f17ee050ada0453f3bd07bc466c2dde2.woff2 new file mode 100644 index 0000000..0bfb07d Binary files /dev/null and b/_static/fonts/f17ee050ada0453f3bd07bc466c2dde2.woff2 differ diff --git a/_static/fonts/f265cee675c0e5b2d6ab263d0edcc754.woff2 b/_static/fonts/f265cee675c0e5b2d6ab263d0edcc754.woff2 new file mode 100644 index 0000000..f041fde Binary files /dev/null and b/_static/fonts/f265cee675c0e5b2d6ab263d0edcc754.woff2 differ diff --git a/_static/fonts/f2f69e8cd15fdd15a4244c95ec8a8514.woff2 b/_static/fonts/f2f69e8cd15fdd15a4244c95ec8a8514.woff2 new file mode 100644 index 0000000..5b19d60 Binary files /dev/null and b/_static/fonts/f2f69e8cd15fdd15a4244c95ec8a8514.woff2 differ diff --git a/_static/fonts/f534242dea2255c25b9d05c2371986e3.woff2 b/_static/fonts/f534242dea2255c25b9d05c2371986e3.woff2 new file mode 100644 index 0000000..23fcdf3 Binary files /dev/null and b/_static/fonts/f534242dea2255c25b9d05c2371986e3.woff2 differ diff --git a/_static/fonts/f53f3b5a15d717b6d21d7885285e90ed.woff2 b/_static/fonts/f53f3b5a15d717b6d21d7885285e90ed.woff2 new file mode 100644 index 0000000..2bfc2ce Binary files /dev/null and b/_static/fonts/f53f3b5a15d717b6d21d7885285e90ed.woff2 differ diff --git a/_static/fonts/f55dac651a40fce74a5cf5728d9f8ffc.woff2 b/_static/fonts/f55dac651a40fce74a5cf5728d9f8ffc.woff2 new file mode 100644 index 0000000..481279c Binary files /dev/null and b/_static/fonts/f55dac651a40fce74a5cf5728d9f8ffc.woff2 differ diff --git a/_static/fonts/f5aebdfea35d1e7656ef4acc5db1f243.woff2 b/_static/fonts/f5aebdfea35d1e7656ef4acc5db1f243.woff2 new file mode 100644 index 0000000..771fbec Binary files /dev/null and b/_static/fonts/f5aebdfea35d1e7656ef4acc5db1f243.woff2 differ diff --git a/_static/fonts/f5f971e9640a9eb86ef553a7e7e999c7.woff2 b/_static/fonts/f5f971e9640a9eb86ef553a7e7e999c7.woff2 new file mode 100644 index 0000000..d87fe26 Binary files /dev/null and b/_static/fonts/f5f971e9640a9eb86ef553a7e7e999c7.woff2 differ diff --git a/_static/fonts/f6734f8177112c0839b961f96d813fcb.woff2 b/_static/fonts/f6734f8177112c0839b961f96d813fcb.woff2 new file mode 100644 index 0000000..020729e Binary files /dev/null and b/_static/fonts/f6734f8177112c0839b961f96d813fcb.woff2 differ diff --git a/_static/fonts/f75911313e1c7802c23345ab57e754d8.woff2 b/_static/fonts/f75911313e1c7802c23345ab57e754d8.woff2 new file mode 100644 index 0000000..6068138 Binary files /dev/null and b/_static/fonts/f75911313e1c7802c23345ab57e754d8.woff2 differ diff --git a/_static/fonts/fb17f56622e45dd4ecee00bb5c63cd2b.woff2 b/_static/fonts/fb17f56622e45dd4ecee00bb5c63cd2b.woff2 new file mode 100644 index 0000000..4487ab7 Binary files /dev/null and b/_static/fonts/fb17f56622e45dd4ecee00bb5c63cd2b.woff2 differ diff --git a/_static/fonts/fb1aaa90783b8cb9375265abeb91b153.woff2 b/_static/fonts/fb1aaa90783b8cb9375265abeb91b153.woff2 new file mode 100644 index 0000000..1351aad Binary files /dev/null and b/_static/fonts/fb1aaa90783b8cb9375265abeb91b153.woff2 differ diff --git a/_static/fonts/fc66f942651a9fe1a598770d3d896529.woff2 b/_static/fonts/fc66f942651a9fe1a598770d3d896529.woff2 new file mode 100644 index 0000000..94ab5fb Binary files /dev/null and b/_static/fonts/fc66f942651a9fe1a598770d3d896529.woff2 differ diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 0000000..250f566 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/sphinx_immaterial_theme.1b5b7a2d5891aec19.min.js b/_static/sphinx_immaterial_theme.1b5b7a2d5891aec19.min.js new file mode 100644 index 0000000..ec77f5d --- /dev/null +++ b/_static/sphinx_immaterial_theme.1b5b7a2d5891aec19.min.js @@ -0,0 +1,27 @@ +"use strict";(()=>{var aa=Object.create;var wr=Object.defineProperty;var sa=Object.getOwnPropertyDescriptor;var ca=Object.getOwnPropertyNames,Rt=Object.getOwnPropertySymbols,fa=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,dn=Object.prototype.propertyIsEnumerable;var mn=(e,t,r)=>t in e?wr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,V=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&mn(e,r,t[r]);if(Rt)for(var r of Rt(t))dn.call(t,r)&&mn(e,r,t[r]);return e};var hn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Rt)for(var n of Rt(e))t.indexOf(n)<0&&dn.call(e,n)&&(r[n]=e[n]);return r};var vt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var la=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ca(t))!Er.call(e,o)&&o!==r&&wr(e,o,{get:()=>t[o],enumerable:!(n=sa(t,o))||n.enumerable});return e};var Je=(e,t,r)=>(r=e!=null?aa(fa(e)):{},la(t||!e||!e.__esModule?wr(r,"default",{value:e,enumerable:!0}):r,e));var ze=(e,t,r)=>new Promise((n,o)=>{var i=s=>{try{c(r.next(s))}catch(f){o(f)}},a=s=>{try{c(r.throw(s))}catch(f){o(f)}},c=s=>s.done?n(s.value):Promise.resolve(s.value).then(i,a);c((r=r.apply(e,t)).next())});var vn=vt((Sr,bn)=>{(function(e,t){typeof Sr=="object"&&typeof bn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Sr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function c(y){return!!(y&&y!==document&&y.nodeName!=="HTML"&&y.nodeName!=="BODY"&&"classList"in y&&"contains"in y.classList)}function s(y){var Te=y.type,Le=y.tagName;return!!(Le==="INPUT"&&a[Te]&&!y.readOnly||Le==="TEXTAREA"&&!y.readOnly||y.isContentEditable)}function f(y){y.classList.contains("focus-visible")||(y.classList.add("focus-visible"),y.setAttribute("data-focus-visible-added",""))}function u(y){!y.hasAttribute("data-focus-visible-added")||(y.classList.remove("focus-visible"),y.removeAttribute("data-focus-visible-added"))}function l(y){y.metaKey||y.altKey||y.ctrlKey||(c(r.activeElement)&&f(r.activeElement),n=!0)}function p(y){n=!1}function m(y){!c(y.target)||(n||s(y.target))&&f(y.target)}function h(y){!c(y.target)||(y.target.classList.contains("focus-visible")||y.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(y.target))}function b(y){document.visibilityState==="hidden"&&(o&&(n=!0),w())}function w(){document.addEventListener("mousemove",$),document.addEventListener("mousedown",$),document.addEventListener("mouseup",$),document.addEventListener("pointermove",$),document.addEventListener("pointerdown",$),document.addEventListener("pointerup",$),document.addEventListener("touchmove",$),document.addEventListener("touchstart",$),document.addEventListener("touchend",$)}function q(){document.removeEventListener("mousemove",$),document.removeEventListener("mousedown",$),document.removeEventListener("mouseup",$),document.removeEventListener("pointermove",$),document.removeEventListener("pointerdown",$),document.removeEventListener("pointerup",$),document.removeEventListener("touchmove",$),document.removeEventListener("touchstart",$),document.removeEventListener("touchend",$)}function $(y){y.target.nodeName&&y.target.nodeName.toLowerCase()==="html"||(n=!1,q())}document.addEventListener("keydown",l,!0),document.addEventListener("mousedown",p,!0),document.addEventListener("pointerdown",p,!0),document.addEventListener("touchstart",p,!0),document.addEventListener("visibilitychange",b,!0),w(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var gn=vt(Or=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(f){return!1}},r=t(),n=function(f){var u={next:function(){var l=f.shift();return{done:l===void 0,value:l}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(f){return encodeURIComponent(f).replace(/%20/g,"+")},i=function(f){return decodeURIComponent(String(f).replace(/\+/g," "))},a=function(){var f=function(l){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var p=typeof l;if(p!=="undefined")if(p==="string")l!==""&&this._fromString(l);else if(l instanceof f){var m=this;l.forEach(function(q,$){m.append($,q)})}else if(l!==null&&p==="object")if(Object.prototype.toString.call(l)==="[object Array]")for(var h=0;hm[0]?1:0}),f._entries&&(f._entries={});for(var l=0;l1?i(m[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Or);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(s,f){typeof s!="string"&&(s=String(s)),f&&typeof f!="string"&&(f=String(f));var u=document,l;if(f&&(e.location===void 0||f!==e.location.href)){f=f.toLowerCase(),u=document.implementation.createHTMLDocument(""),l=u.createElement("base"),l.href=f,u.head.appendChild(l);try{if(l.href.indexOf(f)!==0)throw new Error(l.href)}catch(y){throw new Error("URL unable to set base "+f+" due to "+y)}}var p=u.createElement("a");p.href=s,l&&(u.body.appendChild(p),p.href=p.href);var m=u.createElement("input");if(m.type="url",m.value=s,p.protocol===":"||!/:/.test(p.href)||!m.checkValidity()&&!f)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:p});var h=new e.URLSearchParams(this.search),b=!0,w=!0,q=this;["append","delete","set"].forEach(function(y){var Te=h[y];h[y]=function(){Te.apply(h,arguments),b&&(w=!1,q.search=h.toString(),w=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var $=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==$&&($=this.search,w&&(b=!1,this.searchParams._fromString(this.search),b=!0))}})},a=i.prototype,c=function(s){Object.defineProperty(a,s,{get:function(){return this._anchorElement[s]},set:function(f){this._anchorElement[s]=f},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(s){c(s)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(s){this._anchorElement.search=s,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var s=this;return function(){return s.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(s){this._anchorElement.href=s,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(s){this._anchorElement.pathname=s},enumerable:!0},origin:{get:function(){var s={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],f=this._anchorElement.port!=s&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(f?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(s){},enumerable:!0},username:{get:function(){return""},set:function(s){},enumerable:!0}}),i.createObjectURL=function(s){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(s){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Or)});var Un=vt((tc,Pt)=>{/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var yn,xn,wn,En,Sn,On,_n,Tn,Ln,Ht,_r,Mn,An,Cn,it,Rn,Hn,kn,Pn,$n,In,jn,Fn,kt;(function(e){var t=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(n){e(r(t,r(n)))}):typeof Pt=="object"&&typeof Pt.exports=="object"?e(r(t,r(Pt.exports))):e(r(t));function r(n,o){return n!==t&&(typeof Object.create=="function"?Object.defineProperty(n,"__esModule",{value:!0}):n.__esModule=!0),function(i,a){return n[i]=o?o(i,a):a}}})(function(e){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,o){n.__proto__=o}||function(n,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(n[i]=o[i])};yn=function(n,o){if(typeof o!="function"&&o!==null)throw new TypeError("Class extends value "+String(o)+" is not a constructor or null");t(n,o);function i(){this.constructor=n}n.prototype=o===null?Object.create(o):(i.prototype=o.prototype,new i)},xn=Object.assign||function(n){for(var o,i=1,a=arguments.length;i=0;u--)(f=n[u])&&(s=(c<3?f(s):c>3?f(o,i,s):f(o,i))||s);return c>3&&s&&Object.defineProperty(o,i,s),s},Sn=function(n,o){return function(i,a){o(i,a,n)}},On=function(n,o){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(n,o)},_n=function(n,o,i,a){function c(s){return s instanceof i?s:new i(function(f){f(s)})}return new(i||(i=Promise))(function(s,f){function u(m){try{p(a.next(m))}catch(h){f(h)}}function l(m){try{p(a.throw(m))}catch(h){f(h)}}function p(m){m.done?s(m.value):c(m.value).then(u,l)}p((a=a.apply(n,o||[])).next())})},Tn=function(n,o){var i={label:0,sent:function(){if(s[0]&1)throw s[1];return s[1]},trys:[],ops:[]},a,c,s,f;return f={next:u(0),throw:u(1),return:u(2)},typeof Symbol=="function"&&(f[Symbol.iterator]=function(){return this}),f;function u(p){return function(m){return l([p,m])}}function l(p){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,c&&(s=p[0]&2?c.return:p[0]?c.throw||((s=c.return)&&s.call(c),0):c.next)&&!(s=s.call(c,p[1])).done)return s;switch(c=0,s&&(p=[p[0]&2,s.value]),p[0]){case 0:case 1:s=p;break;case 4:return i.label++,{value:p[1],done:!1};case 5:i.label++,c=p[1],p=[0];continue;case 7:p=i.ops.pop(),i.trys.pop();continue;default:if(s=i.trys,!(s=s.length>0&&s[s.length-1])&&(p[0]===6||p[0]===2)){i=0;continue}if(p[0]===3&&(!s||p[1]>s[0]&&p[1]=n.length&&(n=void 0),{value:n&&n[a++],done:!n}}};throw new TypeError(o?"Object is not iterable.":"Symbol.iterator is not defined.")},_r=function(n,o){var i=typeof Symbol=="function"&&n[Symbol.iterator];if(!i)return n;var a=i.call(n),c,s=[],f;try{for(;(o===void 0||o-- >0)&&!(c=a.next()).done;)s.push(c.value)}catch(u){f={error:u}}finally{try{c&&!c.done&&(i=a.return)&&i.call(a)}finally{if(f)throw f.error}}return s},Mn=function(){for(var n=[],o=0;o1||u(b,w)})})}function u(b,w){try{l(a[b](w))}catch(q){h(s[0][3],q)}}function l(b){b.value instanceof it?Promise.resolve(b.value.v).then(p,m):h(s[0][2],b)}function p(b){u("next",b)}function m(b){u("throw",b)}function h(b,w){b(w),s.shift(),s.length&&u(s[0][0],s[0][1])}},Hn=function(n){var o,i;return o={},a("next"),a("throw",function(c){throw c}),a("return"),o[Symbol.iterator]=function(){return this},o;function a(c,s){o[c]=n[c]?function(f){return(i=!i)?{value:it(n[c](f)),done:c==="return"}:s?s(f):f}:s}},kn=function(n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var o=n[Symbol.asyncIterator],i;return o?o.call(n):(n=typeof Ht=="function"?Ht(n):n[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(s){i[s]=n[s]&&function(f){return new Promise(function(u,l){f=n[s](f),c(u,l,f.done,f.value)})}}function c(s,f,u,l){Promise.resolve(l).then(function(p){s({value:p,done:u})},f)}},Pn=function(n,o){return Object.defineProperty?Object.defineProperty(n,"raw",{value:o}):n.raw=o,n};var r=Object.create?function(n,o){Object.defineProperty(n,"default",{enumerable:!0,value:o})}:function(n,o){n.default=o};$n=function(n){if(n&&n.__esModule)return n;var o={};if(n!=null)for(var i in n)i!=="default"&&Object.prototype.hasOwnProperty.call(n,i)&&kt(o,n,i);return r(o,n),o},In=function(n){return n&&n.__esModule?n:{default:n}},jn=function(n,o){if(!o.has(n))throw new TypeError("attempted to get private field on non-instance");return o.get(n)},Fn=function(n,o,i){if(!o.has(n))throw new TypeError("attempted to set private field on non-instance");return o.set(n,i),i},e("__extends",yn),e("__assign",xn),e("__rest",wn),e("__decorate",En),e("__param",Sn),e("__metadata",On),e("__awaiter",_n),e("__generator",Tn),e("__exportStar",Ln),e("__createBinding",kt),e("__values",Ht),e("__read",_r),e("__spread",Mn),e("__spreadArrays",An),e("__spreadArray",Cn),e("__await",it),e("__asyncGenerator",Rn),e("__asyncDelegator",Hn),e("__asyncValues",kn),e("__makeTemplateObject",Pn),e("__importStar",$n),e("__importDefault",In),e("__classPrivateFieldGet",jn),e("__classPrivateFieldSet",Fn)})});var Qr=vt((Lt,Jr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Jr=="object"?Jr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ia}});var a=i(279),c=i.n(a),s=i(370),f=i.n(s),u=i(817),l=i.n(u);function p(F){try{return document.execCommand(F)}catch(S){return!1}}var m=function(S){var E=l()(S);return p("cut"),E},h=m;function b(F){var S=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[S?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=F,E}var w=function(S,E){var H=b(S);E.container.appendChild(H);var j=l()(H);return p("copy"),H.remove(),j},q=function(S){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof S=="string"?H=w(S,E):S instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(S==null?void 0:S.type)?H=w(S.value,E):(H=l()(S),p("copy")),H},$=q;function y(F){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?y=function(E){return typeof E}:y=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},y(F)}var Te=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=S.action,H=E===void 0?"copy":E,j=S.container,K=S.target,Me=S.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(K!==void 0)if(K&&y(K)==="object"&&K.nodeType===1){if(H==="copy"&&K.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(K.hasAttribute("readonly")||K.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return $(Me,{container:j});if(K)return H==="cut"?h(K):$(K,{container:j})},Le=Te;function we(F){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?we=function(E){return typeof E}:we=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},we(F)}function Mt(F,S){if(!(F instanceof S))throw new TypeError("Cannot call a class as a function")}function ot(F,S){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof j.action=="function"?j.action:this.defaultAction,this.target=typeof j.target=="function"?j.target:this.defaultTarget,this.text=typeof j.text=="function"?j.text:this.defaultText,this.container=we(j.container)==="object"?j.container:document.body}},{key:"listenClick",value:function(j){var K=this;this.listener=f()(j,"click",function(Me){return K.onClick(Me)})}},{key:"onClick",value:function(j){var K=j.delegateTarget||j.currentTarget,Me=this.action(K)||"copy",Ct=Le({action:Me,container:this.container,target:this.target(K),text:this.text(K)});this.emit(Ct?"success":"error",{action:Me,text:Ct,trigger:K,clearSelection:function(){K&&K.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(j){return xr("action",j)}},{key:"defaultTarget",value:function(j){var K=xr("target",j);if(K)return document.querySelector(K)}},{key:"defaultText",value:function(j){return xr("text",j)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(j){var K=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return $(j,K)}},{key:"cut",value:function(j){return h(j)}},{key:"isSupported",value:function(){var j=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],K=typeof j=="string"?[j]:j,Me=!!document.queryCommandSupported;return K.forEach(function(Ct){Me=Me&&!!document.queryCommandSupported(Ct)}),Me}}]),E}(c()),ia=oa},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(c,s){for(;c&&c.nodeType!==o;){if(typeof c.matches=="function"&&c.matches(s))return c;c=c.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function c(u,l,p,m,h){var b=f.apply(this,arguments);return u.addEventListener(p,b,h),{destroy:function(){u.removeEventListener(p,b,h)}}}function s(u,l,p,m,h){return typeof u.addEventListener=="function"?c.apply(null,arguments):typeof p=="function"?c.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(b){return c(b,l,p,m,h)}))}function f(u,l,p,m){return function(h){h.delegateTarget=a(h.target,l),h.delegateTarget&&m.call(u,h)}}n.exports=s},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),c=i(438);function s(p,m,h){if(!p&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(p))return f(p,m,h);if(a.nodeList(p))return u(p,m,h);if(a.string(p))return l(p,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function f(p,m,h){return p.addEventListener(m,h),{destroy:function(){p.removeEventListener(m,h)}}}function u(p,m,h){return Array.prototype.forEach.call(p,function(b){b.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(p,function(b){b.removeEventListener(m,h)})}}}function l(p,m,h){return c(document.body,p,m,h)}n.exports=s},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var c=i.hasAttribute("readonly");c||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),c||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var s=window.getSelection(),f=document.createRange();f.selectNodeContents(i),s.removeAllRanges(),s.addRange(f),a=s.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,c){var s=this.e||(this.e={});return(s[i]||(s[i]=[])).push({fn:a,ctx:c}),this},once:function(i,a,c){var s=this;function f(){s.off(i,f),a.apply(c,arguments)}return f._=a,this.on(i,f,c)},emit:function(i){var a=[].slice.call(arguments,1),c=((this.e||(this.e={}))[i]||[]).slice(),s=0,f=c.length;for(s;s{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var Ls=/["'&<>]/;Ti.exports=Ms;function Ms(e){var t=""+e,r=Ls.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,c=o.observers;return i||a?Tr:(this.currentObservers=null,c.push(r),new $e(function(){n.currentObservers=null,Ve(c,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Xn(r,n)},t}(U);var Xn=function(e){re(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Tr},t}(L);var yt={now:function(){return(yt.delegate||Date).now()},delegate:void 0};var xt=function(e){re(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=yt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,c=n._timestampProvider,s=n._windowTime;o||(i.push(r),!a&&i.push(c.now()+s)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,c=a.slice(),s=0;s0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ft.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ft.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Dt);var to=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Wt);var fe=new to(eo);var k=new U(function(e){return e.complete()});function zt(e){return e&&T(e.schedule)}function kr(e){return e[e.length-1]}function qe(e){return T(kr(e))?e.pop():void 0}function Ee(e){return zt(kr(e))?e.pop():void 0}function qt(e,t){return typeof kr(e)=="number"?e.pop():t}var lt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Kt(e){return T(e==null?void 0:e.then)}function Bt(e){return T(e[ct])}function Yt(e){return Symbol.asyncIterator&&T(e==null?void 0:e[Symbol.asyncIterator])}function Gt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function ya(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Jt=ya();function Qt(e){return T(e==null?void 0:e[Jt])}function Xt(e){return Dn(this,arguments,function(){var r,n,o,i;return $t(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,It(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,It(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,It(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Zt(e){return T(e==null?void 0:e.getReader)}function N(e){if(e instanceof U)return e;if(e!=null){if(Bt(e))return xa(e);if(lt(e))return wa(e);if(Kt(e))return Ea(e);if(Yt(e))return ro(e);if(Qt(e))return Sa(e);if(Zt(e))return Oa(e)}throw Gt(e)}function xa(e){return new U(function(t){var r=e[ct]();if(T(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function wa(e){return new U(function(t){for(var r=0;r=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new L}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,c=e.resetOnRefCountZero,s=c===void 0?!0:c;return function(f){var u,l,p,m=0,h=!1,b=!1,w=function(){l==null||l.unsubscribe(),l=void 0},q=function(){w(),u=p=void 0,h=b=!1},$=function(){var y=u;q(),y==null||y.unsubscribe()};return g(function(y,Te){m++,!b&&!h&&w();var Le=p=p!=null?p:r();Te.add(function(){m--,m===0&&!b&&!h&&(l=Dr($,s))}),Le.subscribe(Te),!u&&m>0&&(u=new Xe({next:function(we){return Le.next(we)},error:function(we){b=!0,w(),l=Dr(q,o,we),Le.error(we)},complete:function(){h=!0,w(),l=Dr(q,a),Le.complete()}}),N(y).subscribe(u))})(f)}}function Dr(e,t){for(var r=[],n=2;ne.next(document)),e}function D(e,t=document){return Array.from(t.querySelectorAll(e))}function B(e,t=document){let r=ie(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ie(e,t=document){return t.querySelector(e)||void 0}function Fe(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function nr(e){return P(x(document.body,"focusin"),x(document.body,"focusout")).pipe(He(1),d(()=>{let t=Fe();return typeof t!="undefined"?e.contains(t):!1}),z(e===Fe()),Q())}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function Oo(e){return P(x(window,"load"),x(window,"resize")).pipe(Re(0,fe),d(()=>Be(e)),z(Be(e)))}function or(e){return{x:e.scrollLeft,y:e.scrollTop}}function _t(e){return P(x(e,"scroll"),x(window,"resize")).pipe(Re(0,fe),d(()=>or(e)),z(or(e)))}var To=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Br||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),Ka?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Br||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=qa.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Lo=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Ao=typeof WeakMap!="undefined"?new WeakMap:new To,Co=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ba.getInstance(),n=new ns(t,r,this);Ao.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Co.prototype[e]=function(){var t;return(t=Ao.get(this))[e].apply(t,arguments)}});var os=function(){return typeof ir.ResizeObserver!="undefined"?ir.ResizeObserver:Co}(),Ro=os;var Ho=new L,is=I(()=>C(new Ro(e=>{for(let t of e)Ho.next(t)}))).pipe(_(e=>P(je,C(e)).pipe(R(()=>e.disconnect()))),X(1));function Se(e){return{width:e.offsetWidth,height:e.offsetHeight}}function de(e){return is.pipe(M(t=>t.observe(e)),_(t=>Ho.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),d(()=>Se(e)))),z(Se(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ko(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var Po=new L,as=I(()=>C(new IntersectionObserver(e=>{for(let t of e)Po.next(t)},{threshold:0}))).pipe(_(e=>P(je,C(e)).pipe(R(()=>e.disconnect()))),X(1));function cr(e){return as.pipe(M(t=>t.observe(e)),_(t=>Po.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),d(({isIntersecting:r})=>r))))}var fr={drawer:B("[data-md-toggle=drawer]"),search:B("[data-md-toggle=search]")};function $o(e){return fr[e].checked}function Ye(e,t){fr[e].checked!==t&&fr[e].click()}function ht(e){let t=fr[e];return x(t,"change").pipe(d(()=>t.checked),z(t.checked))}function ss(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Io(){return x(window,"keydown").pipe(A(e=>!(e.metaKey||e.ctrlKey)),d(e=>({mode:$o("search")?"search":"global",type:e.key,claim(){e.preventDefault(),e.stopPropagation()}})),A(({mode:e,type:t})=>{if(e==="global"){let r=Fe();if(typeof r!="undefined")return!ss(r,t)}return!0}),pe())}function Oe(){return new URL(location.href)}function lr(e){location.href=e.href}function jo(){return new L}function Fo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Fo(e,r)}function O(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Fo(n,o);return n}function Uo(e,t){let r=t;if(e.length>r){for(;e[r]!==" "&&--r>0;);return`${e.substring(0,r)}...`}return e}function ur(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Vo(){return location.hash.substring(1)}function No(e){let t=O("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function cs(){return x(window,"hashchange").pipe(d(Vo),z(Vo()),A(e=>e.length>0),X(1))}function Do(){return cs().pipe(d(e=>ie(`[id="${e}"]`)),A(e=>typeof e!="undefined"))}function Yr(e){let t=matchMedia(e);return tr(r=>t.addListener(()=>r(t.matches))).pipe(z(t.matches))}function Wo(){let e=matchMedia("print");return P(x(window,"beforeprint").pipe(d(()=>!0)),x(window,"afterprint").pipe(d(()=>!1))).pipe(z(e.matches))}function Gr(e,t){return e.pipe(_(r=>r?t():k))}function pr(e,t={credentials:"same-origin"}){return be(fetch(`${e}`,t)).pipe(le(()=>k),_(r=>r.status!==200?Et(()=>new Error(r.statusText)):C(r)))}function Ge(e,t){return pr(e,t).pipe(_(r=>r.json()),X(1))}function zo(e,t){let r=new DOMParser;return pr(e,t).pipe(_(n=>n.text()),d(n=>r.parseFromString(n,"text/xml")),X(1))}function qo(e){let t=O("script",{src:e});return I(()=>(document.head.appendChild(t),P(x(t,"load"),x(t,"error").pipe(_(()=>Et(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(d(()=>{}),R(()=>document.head.removeChild(t)),ve(1))))}function Ko(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Bo(){return P(x(window,"scroll",{passive:!0}),x(window,"resize",{passive:!0})).pipe(d(Ko),z(Ko()))}function Yo(){return{width:innerWidth,height:innerHeight}}function Go(){return x(window,"resize",{passive:!0}).pipe(d(Yo),z(Yo()))}function Jo(){return G([Bo(),Go()]).pipe(d(([e,t])=>({offset:e,size:t})),X(1))}function mr(e,{viewport$:t,header$:r}){let n=t.pipe(J("size")),o=G([n,r]).pipe(d(()=>Be(e)));return G([r,t,o]).pipe(d(([{height:i},{offset:a,size:c},{x:s,y:f}])=>({offset:{x:a.x-s,y:a.y-f+i},size:c})))}var fs=B("#__config"),bt=JSON.parse(fs.textContent);bt.base=`${new URL(bt.base,Oe())}`;function ce(){return bt}function Z(e){return bt.features.includes(e)}function ae(e,t){return typeof t!="undefined"?bt.translations[e].replace("#",t.toString()):bt.translations[e]}function _e(e,t=document){return B(`[data-md-component=${e}]`,t)}function ee(e,t=document){return D(`[data-md-component=${e}]`,t)}function ls(e){let t=B(".md-typeset > :first-child",e);return x(t,"click",{once:!0}).pipe(d(()=>B(".md-typeset",e)),d(r=>({hash:__md_hash(r.innerHTML)})))}function Qo(e){return!Z("announce.dismiss")||!e.childElementCount?k:I(()=>{let t=new L;return t.pipe(z({hash:__md_get("__announce")})).subscribe(({hash:r})=>{var n;r&&r===((n=__md_get("__announce"))!=null?n:r)&&(e.hidden=!0,__md_set("__announce",r))}),ls(e).pipe(M(r=>t.next(r)),R(()=>t.complete()),d(r=>V({ref:e},r)))})}function us(e,{target$:t}){return t.pipe(d(r=>({hidden:r!==e})))}function Xo(e,t){let r=new L;return r.subscribe(({hidden:n})=>{e.hidden=n}),us(e,t).pipe(M(n=>r.next(n)),R(()=>r.complete()),d(n=>V({ref:e},n)))}var fi=Je(Qr());function Xr(e){return O("div",{class:"md-tooltip",id:e},O("div",{class:"md-tooltip__inner md-typeset"}))}function Zo(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return O("aside",{class:"md-annotation",tabIndex:0},Xr(t),O("a",{href:r,class:"md-annotation__index",tabIndex:-1},O("span",{"data-md-annotation-id":e})))}else return O("aside",{class:"md-annotation",tabIndex:0},Xr(t),O("span",{class:"md-annotation__index",tabIndex:-1},O("span",{"data-md-annotation-id":e})))}function ei(e){return O("button",{class:"md-clipboard md-icon",title:ae("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function Zr(e,t){let r=t&2,n=t&1,o=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,s)=>[...c,O("del",null,s)," "],[]).slice(0,-1),i=new URL(e.location);Z("search.highlight")&&i.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[s])=>`${c} ${s}`.trim(),""));let{tags:a}=ce();return O("a",{href:`${i}`,class:"md-search-result__link",tabIndex:-1},O("article",{class:["md-search-result__article",...r?["md-search-result__article--document"]:[]].join(" "),"data-md-score":e.score.toFixed(2)},r>0&&O("div",{class:"md-search-result__icon md-icon"}),O("h1",{class:"md-search-result__title"},e.title),n>0&&e.text.length>0&&O("p",{class:"md-search-result__teaser"},Uo(e.text,320)),e.tags&&O("div",{class:"md-typeset"},e.tags.map(c=>{let s=c.replace(/<[^>]+>/g,""),f=a?s in a?`md-tag-icon md-tag-icon--${a[s]}`:"md-tag-icon":"";return O("span",{class:`md-tag ${f}`},c)})),n>0&&o.length>0&&O("p",{class:"md-search-result__terms"},ae("search.result.term.missing"),": ",...o)))}function ti(e){let t=e[0].score,r=[...e],n=r.findIndex(f=>!f.location.includes("#")),[o]=r.splice(n===-1?0:n,1),i=r.findIndex(f=>f.scoreZr(f,1)),...c.length?[O("details",{class:"md-search-result__more"},O("summary",{tabIndex:-1},c.length>0&&c.length===1?ae("search.result.more.one"):ae("search.result.more.other",c.length)),...c.map(f=>Zr(f,1)))]:[]];return O("li",{class:"md-search-result__item"},s)}function ri(e){return O("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>O("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?ur(r):r)))}function en(e){let t=`tabbed-control tabbed-control--${e}`;return O("div",{class:t,hidden:!0},O("button",{class:"tabbed-button",tabIndex:-1}))}function ni(e){return O("div",{class:"md-typeset__scrollwrap"},O("div",{class:"md-typeset__table"},e))}function ps(e){let t=ce(),r=new URL(`${e.version}/`,new URL("../",t.base));return O("li",{class:"md-version__item"},O("a",{href:`${r}`,class:"md-version__link"},e.title))}function oi(e,t){return O("div",{class:"md-version"},O("button",{class:"md-version__current","aria-label":ae("select.version.title")},t.title),O("ul",{class:"md-version__list"},e.map(ps)))}function ms(e,t){let r=I(()=>G([Oo(e),_t(t)])).pipe(d(([{x:n,y:o},i])=>{let{width:a,height:c}=Se(e);return{x:n-i.x+a/2,y:o-i.y+c/2}}));return nr(e).pipe(_(n=>r.pipe(d(o=>({active:n,offset:o})),ve(+!n||1/0))))}function ii(e,t,{target$:r}){let[n,o]=Array.from(e.children);return I(()=>{let i=new L,a=i.pipe(ue(1));return i.subscribe({next({offset:c}){e.style.setProperty("--md-tooltip-x",`${c.x}px`),e.style.setProperty("--md-tooltip-y",`${c.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),cr(e).pipe(te(a)).subscribe(c=>{e.toggleAttribute("data-md-visible",c)}),P(i.pipe(A(({active:c})=>c)),i.pipe(He(250),A(({active:c})=>!c))).subscribe({next({active:c}){c?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Re(16,fe)).subscribe(({active:c})=>{n.classList.toggle("md-tooltip--active",c)}),i.pipe(Kr(125,fe),A(()=>!!e.offsetParent),d(()=>e.offsetParent.getBoundingClientRect()),d(({x:c})=>c)).subscribe({next(c){c?e.style.setProperty("--md-tooltip-0",`${-c}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),x(o,"click").pipe(te(a),A(c=>!(c.metaKey||c.ctrlKey))).subscribe(c=>c.preventDefault()),x(o,"mousedown").pipe(te(a),me(i)).subscribe(([c,{active:s}])=>{var f;if(c.button!==0||c.metaKey||c.ctrlKey)c.preventDefault();else if(s){c.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(f=Fe())==null||f.blur()}}),r.pipe(te(a),A(c=>c===n),ke(125)).subscribe(()=>e.focus()),ms(e,t).pipe(M(c=>i.next(c)),R(()=>i.complete()),d(c=>V({ref:e},c)))})}function ds(e){let t=[];for(let r of D(".c, .c1, .cm",e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,c,s]=a;if(typeof s=="undefined"){let f=i.splitText(a.index);i=f.splitText(c.length),t.push(f)}else{i.textContent=c,t.push(i);break}}}}return t}function ai(e,t){t.append(...Array.from(e.childNodes))}function si(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let c of ds(t)){let[,s]=c.textContent.match(/\((\d+)\)/);ie(`li:nth-child(${s})`,e)&&(a.set(s,Zo(s,i)),c.replaceWith(a.get(s)))}return a.size===0?k:I(()=>{let c=new L,s=[];for(let[f,u]of a)s.push([B(".md-typeset",u),B(`li:nth-child(${f})`,e)]);return n.pipe(te(c.pipe(ue(1)))).subscribe(f=>{e.hidden=!f;for(let[u,l]of s)f?ai(u,l):ai(l,u)}),P(...[...a].map(([,f])=>ii(f,t,{target$:r}))).pipe(R(()=>c.complete()),pe())})}var hs=0;function li(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return li(t)}}function ci(e){return de(e).pipe(d(({width:t})=>({scrollable:Tt(e).width>t})),J("scrollable"))}function ui(e,t){let{matches:r}=matchMedia("(hover)"),n=I(()=>{let o=new L;if(o.subscribe(({scrollable:a})=>{a&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}),fi.default.isSupported()){let a=e.closest("pre");a.id=`__code_${++hs}`,a.insertBefore(ei(a.id),e)}let i=e.closest(".highlight");if(i instanceof HTMLElement){let a=li(i);if(typeof a!="undefined"&&(i.classList.contains("annotate")||Z("content.code.annotate"))){let c=si(a,e,t);return ci(e).pipe(M(s=>o.next(s)),R(()=>o.complete()),d(s=>V({ref:e},s)),tt(de(i).pipe(d(({width:s,height:f})=>s&&f),Q(),_(s=>s?c:k))))}}return ci(e).pipe(M(a=>o.next(a)),R(()=>o.complete()),d(a=>V({ref:e},a)))});return Z("content.lazy")?cr(e).pipe(A(o=>o),ve(1),_(()=>n)):n}var pi=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:transparent}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}defs #flowchart-circleEnd,defs #flowchart-circleStart,defs #flowchart-crossEnd,defs #flowchart-crossStart,defs #flowchart-pointEnd,defs #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}.actor,defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{stroke:var(--md-mermaid-node-fg-color)}text.actor>tspan{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-default-fg-color--lighter)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-edge-color)}.loopText>tspan,.messageText,.noteText>tspan{font-family:var(--md-mermaid-font-family)!important}#arrowhead path,.loopText>tspan,.messageText,.noteText>tspan{fill:var(--md-mermaid-edge-color);stroke:none}.loopLine{stroke:var(--md-mermaid-node-fg-color)}.labelBox,.loopLine{fill:var(--md-mermaid-node-bg-color)}.labelBox{stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-node-fg-color);font-family:var(--md-mermaid-font-family)}";var tn,vs=0,gs=ce();function ys(){return typeof mermaid=="undefined"||mermaid instanceof Element?qo(`${gs.base}_static/mermaid/mermaid.min.js`):C(void 0)}function mi(e){return e.classList.remove("mermaid"),tn||(tn=ys().pipe(M(()=>mermaid.initialize({startOnLoad:!1,themeCSS:pi,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),d(()=>{}),X(1))),tn.subscribe(()=>{e.classList.add("mermaid");let t=`__mermaid_${vs++}`,r=O("div",{class:"mermaid"});mermaid.mermaidAPI.render(t,e.textContent,n=>{let o=r.attachShadow({mode:"closed"});o.innerHTML=n,e.replaceWith(r)})}),tn.pipe(d(()=>({ref:e})))}function xs(e,{target$:t,print$:r}){let n=!0;return P(t.pipe(d(o=>o.closest("details:not([open])")),A(o=>e===o),d(()=>({action:"open",reveal:!0}))),r.pipe(A(o=>o||!n),M(()=>n=e.open),d(o=>({action:o?"open":"close"}))))}function di(e,t){return I(()=>{let r=new L;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),xs(e,t).pipe(M(n=>r.next(n)),R(()=>r.complete()),d(n=>V({ref:e},n)))})}var hi=O("table");function bi(e){return e.replaceWith(hi),hi.replaceWith(ni(e)),C({ref:e})}function ws(e){let t=D(":scope > input",e),r=t.find(n=>n.checked)||t[0];return P(...t.map(n=>x(n,"change").pipe(d(()=>B(`label[for="${n.id}"]`))))).pipe(z(B(`label[for="${r.id}"]`)),d(n=>({active:n})))}function vi(e,{viewport$:t}){let r=en("prev");e.append(r);let n=en("next");e.append(n);let o=B(".tabbed-labels",e);return I(()=>{let i=new L,a=i.pipe(ue(1));return G([i,de(e)]).pipe(Re(1,fe),te(a)).subscribe({next([{active:c},s]){let f=Be(c),{width:u}=Se(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let l=or(o);(f.xl.x+s.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),G([_t(o),de(o)]).pipe(te(a)).subscribe(([c,s])=>{let f=Tt(o);r.hidden=c.x<16,n.hidden=c.x>f.width-s.width-16}),P(x(r,"click").pipe(d(()=>-1)),x(n,"click").pipe(d(()=>1))).pipe(te(a)).subscribe(c=>{let{width:s}=Se(o);o.scrollBy({left:s*c,behavior:"smooth"})}),Z("content.tabs.link")&&i.pipe(Pe(1),me(t)).subscribe(([{active:c},{offset:s}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-s.y;for(let p of D("[data-tabs]"))for(let m of D(":scope > input",p)){let h=B(`label[for="${m.id}"]`);if(h!==c&&h.innerText.trim()===f){h.setAttribute("data-md-switching",""),m.click();break}}window.scrollTo({top:e.offsetTop-u});let l=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...l])])}}),ws(e).pipe(M(c=>i.next(c)),R(()=>i.complete()),d(c=>V({ref:e},c)))}).pipe(Ze(se))}function gi(e,{viewport$:t,target$:r,print$:n}){return P(...D("pre:not(.mermaid) > code",e).map(o=>ui(o,{target$:r,print$:n})),...D("pre.mermaid",e).map(o=>mi(o)),...D("table:not([class])",e).map(o=>bi(o)),...D("details",e).map(o=>di(o,{target$:r,print$:n})),...D("[data-tabs]",e).map(o=>vi(o,{viewport$:t})))}function Es(e,{alert$:t}){return t.pipe(_(r=>P(C(!0),C(!1).pipe(ke(2e3))).pipe(d(n=>({message:r,active:n})))))}function yi(e,t){let r=B(".md-typeset",e);return I(()=>{let n=new L;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),Es(e,t).pipe(M(o=>n.next(o)),R(()=>n.complete()),d(o=>V({ref:e},o)))})}function Ss({viewport$:e}){if(!Z("header.autohide"))return C(!1);let t=e.pipe(d(({offset:{y:o}})=>o),Ne(2,1),d(([o,i])=>[oMath.abs(i-o.y)>100),d(([,[o]])=>o),Q()),n=ht("search");return G([e,n]).pipe(d(([{offset:o},i])=>o.y>400&&!i),Q(),_(o=>o?r:C(!1)),z(!1))}function xi(e,t){return I(()=>G([de(e),Ss(t)])).pipe(d(([{height:r},n])=>({height:r,hidden:n})),Q((r,n)=>r.height===n.height&&r.hidden===n.hidden),X(1))}function wi(e,{header$:t,main$:r}){return I(()=>{let n=new L,o=n.pipe(ue(1));return n.pipe(J("active"),St(t)).subscribe(([{active:i},{hidden:a}])=>{e.classList.toggle("md-header--shadow",i&&!a),e.hidden=a}),r.subscribe(n),t.pipe(te(o),d(i=>V({ref:e},i)))})}function Os(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(d(({offset:{y:n}})=>{let{height:o}=Se(e);return{active:n>=o}}),J("active"))}function Ei(e,t){return I(()=>{let r=new L;r.subscribe(({active:o})=>{e.classList.toggle("md-header__title--active",o)});let n=ie("article h1, .objdesc > dt .descname");return typeof n=="undefined"?k:Os(n,t).pipe(M(o=>r.next(o)),R(()=>r.complete()),d(o=>V({ref:e},o)))})}function Si(e,{viewport$:t,header$:r}){let n=r.pipe(d(({height:i})=>i),Q()),o=n.pipe(_(()=>de(e).pipe(d(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),J("bottom"))));return G([n,o,t]).pipe(d(([i,{top:a,bottom:c},{offset:{y:s},size:{height:f}}])=>(f=Math.max(0,f-Math.max(0,a-s,i)-Math.max(0,f+s-c)),{offset:a-i,height:f,active:a-i<=s})),Q((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function _s(e){let t=__md_get("__palette")||{index:e.findIndex(r=>matchMedia(r.getAttribute("data-md-color-media")).matches)};return C(...e).pipe(oe(r=>x(r,"change").pipe(d(()=>r))),z(e[Math.max(0,t.index)]),d(r=>({index:e.indexOf(r),color:{scheme:r.getAttribute("data-md-color-scheme"),primary:r.getAttribute("data-md-color-primary"),accent:r.getAttribute("data-md-color-accent")}})),X(1))}function Oi(e){return I(()=>{let t=new L;t.subscribe(n=>{document.body.setAttribute("data-md-color-switching","");for(let[o,i]of Object.entries(n.color))document.body.setAttribute(`data-md-color-${o}`,i);for(let o=0;o{document.body.removeAttribute("data-md-color-switching")});let r=D("input",e);return _s(r).pipe(M(n=>t.next(n)),R(()=>t.complete()),d(n=>V({ref:e},n)))})}function Ts(e){let t=a=>a.trim(),{searchParams:r}=Oe(),n;if(r.has("q")){Ye("search",!0);let a=r.get("q");n=C(a)}else n=C();ht("search").pipe(A(a=>!a),ve(1)).subscribe(()=>{let a=new URL(location.href);a.searchParams.delete("q"),history.replaceState({},"",`${a}`)}),n.subscribe(a=>{a&&(e.value=a,e.focus())});let o=nr(e),i=P(x(e,"keyup"),x(e,"focus").pipe(ke(1)),n).pipe(d(()=>t(e.value)),z(""),Q());return G([i,o]).pipe(d(([a,c])=>({value:a,focus:c})),X(1))}function _i(e){let t=new L,r=t.pipe(ue(1));return t.pipe(J("focus")).subscribe(({focus:n})=>{n?(Ye("search",n),e.placeholder=""):e.placeholder=ae("search.placeholder")}),x(e.form,"reset").pipe(te(r)).subscribe(()=>e.focus()),Ts(e).pipe(M(n=>t.next(n)),R(()=>t.complete()),d(n=>V({ref:e},n)),pe())}var on=Je(rn());var As=ce();function nn(e){return`${As.base}${e}`}var dr;function Li(e){return new Promise((t,r)=>{let n=document.createElement("script"),o=nn(e);n.src=o,n.addEventListener("load",()=>t()),n.addEventListener("error",()=>{console.error(`Failed to load search data: ${o}`),r()}),document.body.appendChild(n)})}function Cs(){return dr!==void 0||(dr=Promise.all([Li("_static/language_data.js"),Li("searchindex.js")]).then(()=>{})),dr}var Ue={objNameMatch:11,objPartialMatch:6,objPrio:{0:15,1:5,2:-5},objPrioDefault:0,title:15,partialTitle:7,term:5,partialTerm:2},an;window.Search={setIndex:e=>{an=e}};var Rs=!1;function Hs(e,t){let{docurls:r,objects:n,objnames:o,titles:i}=an,a=[];function c(s,f,u,l,p,m,h){var q;let b=(s?`${s}.`:"")+m,w=b.toLowerCase();if(w.indexOf(e)>-1){let $=0,y=w.split(".");w===e||y[y.length-1]===e?$+=Ue.objNameMatch:y[y.length-1].indexOf(e)>-1&&($+=Ue.objPartialMatch);let Te=o[u][2],Le=i[f];if(t.length>0){let we=`${s} ${m} ${Te} ${Le} ${h!=null?h:""}`.toLowerCase(),Mt=!0;for(let ot=0;ot2){let m=sn(u);if(!o[u])for(let h in o)h.match(m)&&p.push({files:o[h],score:Ue.partialTerm});if(!i[u])for(let h in i)h.match(m)&&p.push({files:i[h],score:Ue.partialTitle})}if(p.every(m=>m.files===void 0))break;p.forEach(m=>{let h=m.files;if(h!==void 0){Array.isArray(h)||(h=[h]),l.push(...h);for(let b=0;bm.length>2).length;if(!(a[u].length!==e.length&&a[u].length!==p)){for(let m=0;mc[f][h]));s.push({docurl:r[f],title:n[f],anchor:"",objectLabel:null,synopsis:null,score:m})}}}return s}function $s(e){let t=new DOMParser().parseFromString(e,"text/html");t.querySelectorAll(".headerlink").forEach(s=>{var f;(f=s.parentNode)==null||f.removeChild(s)});let r=t.querySelector("[role=main]");if(r===null)return console.warn("Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."),[];let n=r.querySelectorAll("h1, h2, h3, h4, h5, h6"),o,i=[],a=t.createRange(),c=(s,f)=>{var m;s!==void 0?a.setStartAfter(s):a.setStartBefore(r),f!==void 0?a.setEndBefore(f):a.setEndAfter(r);let u=a.toString().trim(),l=(m=s==null?void 0:s.textContent)==null?void 0:m.trim();if(!l&&!u)return;let p=s!==void 0?`#${s.id}`:"";i.push({title:l!=null?l:"",anchor:p,text:u})};return n.forEach(s=>{if(!s.id)return;let u=o;o=s,c(u,s)}),c(o,void 0),i}function Is(e,t){let r=$s(e),n=t.map(i=>new RegExp(sn(i),"im")),o=[];for(let i=0;ii.score!==a.score?a.score-i.score:i.sectionIndex-a.sectionIndex),o.length!==0)return o.map(i=>{let c=r[i.sectionIndex],s=Math.max(i.snippetIndex-240/2,0);return{snippet:(s>0?"\u2026":"")+c.text.substr(s,240).trim()+(s+240${(0,on.default)(e.objectLabel)}`,text:r(e.synopsis)}];let a=nn(e.docurl),c;if(window.location.protocol!=="file:")try{let l=yield(yield fetch(a)).text();c=Is(l,t)}catch(u){console.warn("Failed to retrieve search result document: ",u)}c===void 0&&(c=[{score:-1,title:"",anchor:"",snippet:"",terms:i}]);let s=[];c[0].score!==-1&&s.push({location:n,score:e.score,terms:i,title:r(o),text:""});let f;for(let u of c)f===void 0&&(f=u.score),s.push({location:n+u.anchor,score:u.score===f?e.score:0,terms:u.terms,title:r(u.title||o),text:r(u.snippet)});return s})}function Fs(e){return new DOMParser().parseFromString(e,"text/html").body.textContent||""}function Mi(e){return ze(this,null,function*(){yield Cs();let t=new Stemmer,r=[],n=[],o=[],i=[];for(let u of ks(e)){let l=u.toLowerCase();if(l.length===0||(i.push(l),stopwords.indexOf(l)!==-1))continue;let p=t.stemWord(l);p.length<3&&l.length>=3&&(p=l);let m;p[0]==="-"?(m=n,p=p.substr(1)):(m=r,o.push(l)),m.indexOf(p)===-1&&m.push(p)}let a=[];for(let u=0;u{let p=u.score,m=l.score;if(p!==m)return m-p;let h=u.title.toLowerCase(),b=l.title.toLowerCase();return h>b?1:h`${u}`,f=u=>(0,on.default)(u).replace(c,s).replace(/<\/mark>(\s+)]*>/gim,"$1");return{count:a.length,get:u=>js(a[u],o,f)}})}function Ai(e,{query$:t}){let r=B(":scope > :first-child",e),n=B(":scope > :last-child",e),o,i,a=e.parentElement,c=16,s=()=>a.scrollTop+a.clientHeight+c>a.scrollHeight,f=()=>{i!==void 0&&s()&&(i(),i=void 0)};a.addEventListener("scroll",f,{passive:!0}),window.addEventListener("resize",f,{passive:!0});let u=l=>ze(this,null,function*(){o=l;let p=4,m=p;for(let h=0;h{i=()=>w(void 0)})),m+=p),o!==l)return;let b=yield l.get(h);if(o!==l)return;n.appendChild(ti(b))}});return t.pipe(J("value"),Fr(()=>$r(250)),mt(l=>ze(this,null,function*(){if(!!l.value)return Mi(l.value)})),Ce(fe)).subscribe(l=>{if(n.innerHTML="",l){switch(l.count){case 0:r.textContent=ae("search.result.none");break;case 1:r.textContent=ae("search.result.one");break;default:r.textContent=ae("search.result.other",ur(l.count))}u(l)}else r.textContent=ae("search.result.placeholder")}),C()}function Us(e,{query$:t}){return t.pipe(d(({value:r})=>{let n=Oe();return n.hash="",n.searchParams.delete("h"),n.searchParams.set("q",r),{url:n}}))}function Ci(e,t){let r=new L;return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),x(e,"click").subscribe(n=>n.preventDefault()),Us(e,t).pipe(M(n=>r.next(n)),R(()=>r.complete()),d(n=>V({ref:e},n)))}function Ri(e,{keyboard$:t}){let r=_e("search-query");return t.pipe(A(({mode:n})=>n==="search")).subscribe(n=>{switch(n.type){case"ArrowRight":e.innerText.length&&r.selectionStart===r.value.length&&(r.value=e.innerText);break}}),C()}function Hi(e,{keyboard$:t}){try{let r=_e("search-query",e),n=_e("search-result",e);t.pipe(A(({mode:a})=>a==="search")).subscribe(a=>{let c=Fe();switch(a.type){case"Enter":if(c===r){let s=new Map;for(let f of D(":first-child [href]",n)){let u=f.firstElementChild;s.set(f,parseFloat(u.getAttribute("data-md-score")))}if(s.size){let[[f]]=[...s].sort(([,u],[,l])=>l-u);f.click()}a.claim()}break;case"Escape":case"Tab":Ye("search",!1),r.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")r.focus();else{let s=[r,...D(":not(details) > [href], summary, details[open] [href]",n)],f=Math.max(0,(Math.max(0,s.indexOf(c))+s.length+(a.type==="ArrowUp"?-1:1))%s.length);s[f].focus()}a.claim();break;default:r!==Fe()&&r.focus()}}),t.pipe(A(({mode:a})=>a==="global")).subscribe(a=>{switch(a.type){case"f":case"s":case"/":r.focus(),r.select(),a.claim();break}});let o=_i(r),i=Ai(n,{query$:o});return P(o,i).pipe(tt(...ee("search-share",e).map(a=>Ci(a,{query$:o})),...ee("search-suggest",e).map(a=>Ri(a,{keyboard$:t}))))}catch(r){return e.hidden=!0,je}}var cn=Je(Qr());function Vs(e){e.setAttribute("data-md-copying","");let t=e.innerText;return e.removeAttribute("data-md-copying"),t}function ki({alert$:e}){cn.default.isSupported()&&new U(t=>{new cn.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Vs(B(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(M(t=>{t.trigger.focus()}),d(()=>ae("clipboard.copied"))).subscribe(e)}function Ns(e){if(e.length<2)return[""];let[t,r]=[...e].sort((o,i)=>o.length-i.length).map(o=>o.replace(/[^/]+$/,"")),n=0;if(t===r)n=t.length;else for(;t.charCodeAt(n)===r.charCodeAt(n);)n++;return e.map(o=>o.replace(t.slice(0,n),""))}function hr(e){let t=__md_get("__sitemap",sessionStorage,e);if(t)return C(t);{let r=ce();return zo(new URL("sitemap.xml",e||r.base)).pipe(d(n=>Ns(D("loc",n).map(o=>o.textContent))),le(()=>k),De([]),M(n=>__md_set("__sitemap",n,sessionStorage,e)))}}function Pi({document$:e,location$:t,viewport$:r}){let n=ce();if(location.protocol==="file:")return;"scrollRestoration"in history&&(history.scrollRestoration="manual",x(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}));let o=ie("link[rel=icon]");typeof o!="undefined"&&(o.href=o.href);let i=hr().pipe(d(l=>l.map(p=>`${new URL(p,n.base)}`)),_(l=>x(document.body,"click").pipe(A(p=>!p.metaKey&&!p.ctrlKey),_(p=>{if(p.target instanceof Element){let m=p.target.closest("a");if(m&&!m.target){let h=new URL(m.href);if(h.search="",h.hash="",h.pathname!==location.pathname&&l.includes(h.toString()))return p.preventDefault(),C({url:new URL(m.href)})}}return je}))),pe()),a=x(window,"popstate").pipe(A(l=>l.state!==null),d(l=>({url:new URL(location.href),offset:l.state})),pe());P(i,a).pipe(Q((l,p)=>l.url.href===p.url.href),d(({url:l})=>l)).subscribe(t);let c=t.pipe(J("pathname"),_(l=>pr(l.href).pipe(le(()=>(lr(l),je)))),pe());i.pipe(rr(c)).subscribe(({url:l})=>{history.pushState({},"",`${l}`)});let s=new DOMParser;c.pipe(_(l=>l.text()),d(l=>s.parseFromString(l,"text/html"))).subscribe(e);let f=new Set,u=new Set;for(let l of D("script",document))l.src?f.add(new URL(l.src,document.baseURI).toString()):u.add(l.outerHTML);e.pipe(Pe(1),mt(l=>ze(this,null,function*(){var p;for(let m of["title","link[rel=canonical]","meta[name=author]","meta[name=description]","[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...Z("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let h=ie(m),b=ie(m,l);typeof h!="undefined"&&typeof b!="undefined"&&h.replaceWith(b)}((p=window.MathJax)==null?void 0:p.typesetPromise)!==void 0&&(yield window.MathJax.typesetPromise());for(let m of D("script",l))if(m.src){let h=new URL(m.src,document.baseURI).toString();if(!f.has(h)){let b=document.createElement("script");for(let q of m.getAttributeNames())b.setAttribute(q,m.getAttribute(q));let w;b.src=h,b.async||(w=new Promise(q=>b.addEventListener("load",()=>q()))),document.body.appendChild(b),f.add(h),w!==void 0&&(yield w)}}else{let h=m.outerHTML;if(!u.has(h)){let b=document.createElement("script");for(let w of m.getAttributeNames())b.setAttribute(w,m.getAttribute(w));b.textContent=m.textContent,document.body.appendChild(b),u.add(h)}}}))).subscribe(),e.pipe(Pe(1),d(()=>_e("container")),_(l=>D("script",l)),mt(l=>{let p=O("script");if(l.src){for(let m of l.getAttributeNames())p.setAttribute(m,l.getAttribute(m));return l.replaceWith(p),new U(m=>{p.onload=()=>m.complete()})}else return p.textContent=l.textContent,l.replaceWith(p),k})).subscribe(),P(i,a).pipe(rr(e)).subscribe(({url:l,offset:p})=>{l.hash&&!p?No(l.hash):window.scrollTo(0,(p==null?void 0:p.y)||0)}),r.pipe(Wr(i),He(250),J("offset")).subscribe(({offset:l})=>{history.replaceState(l,"")}),P(i,a).pipe(Ne(2,1),A(([l,p])=>l.url.pathname===p.url.pathname),d(([,l])=>l)).subscribe(({offset:l})=>{window.scrollTo(0,(l==null?void 0:l.y)||0)})}var $i=Je(rn());function Ii(e,t){let r=new RegExp(e.separator,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator})(${o.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(t?(0,$i.default)(a):a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function ji({document$:e}){var a;let t=ce(),r=t.version.staticVersions?C(t.version.staticVersions):Ge(new URL((a=t.version.versionPath)!=null?a:"../versions.json",t.base)),n=new URL("..",t.base),o=c=>new URL(c,n).toString().replace(/\/*$/,""),i=r.pipe(d(c=>{let s=t.base.toString().replace(/\/*$/,"");return c.find(({version:f,aliases:u})=>o(f)===s||u.find(l=>o(l)===s))||c[0]}));r.pipe(d(c=>new Map(c.map(s=>[`${new URL(`../${s.version}/`,t.base)}`,s]))),_(c=>x(document.body,"click").pipe(A(s=>!s.metaKey&&!s.ctrlKey),me(i),_(([s,f])=>{if(s.target instanceof Element){let u=s.target.closest("a");if(u&&!u.target&&c.has(u.href)){let l=u.href;return!s.target.closest(".md-version")&&c.get(l)===f?k:(s.preventDefault(),C(l))}}return k}),_(s=>{let{version:f}=c.get(s);return hr(new URL(s)).pipe(d(u=>{let p=Oe().href.replace(t.base,"");return u.includes(p.split("#")[0])?new URL(`../${f}/${p}`,t.base):new URL(s)}))})))).subscribe(c=>lr(c)),G([r,i]).subscribe(([c,s])=>{B(".md-header__topic").appendChild(oi(c,s))}),e.pipe(_(()=>i)).subscribe(c=>{var f;let s=__md_get("__outdated",sessionStorage);if(s===null){let u=((f=t.version)==null?void 0:f.default)||"latest";s=!c.aliases.includes(u),__md_set("__outdated",s,sessionStorage)}if(s)for(let u of ee("outdated"))u.hidden=!1})}function Fi(e,{location$:t}){let r={lang:[],separator:"\\s+"};return G([t.pipe(z(Oe()),A(n=>!!n.searchParams.get("h")))]).pipe(d(([n])=>Ii(r,!0)(n.searchParams.get("h"))),d(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let c=i.nextNode();c;c=i.nextNode())if((a=c.parentElement)!=null&&a.offsetHeight){let s=c.textContent,f=n(s);f.length>s.length&&o.set(c,f)}for(let[c,s]of o){let{childNodes:f}=O("span",null,s);c.replaceWith(...Array.from(f))}return{ref:e,nodes:o}}))}function Ds(e,{viewport$:t,main$:r}){let n=e.parentElement,o=n.offsetTop-n.parentElement.offsetTop;return G([r,t]).pipe(d(([{offset:i,height:a},{offset:{y:c}}])=>(a=a+Math.min(o,Math.max(0,c-i))-o,{height:a,locked:c>=i+o})),Q((i,a)=>i.height===a.height&&i.locked===a.locked))}function fn(e,n){var o=n,{header$:t}=o,r=hn(o,["header$"]);let i=B(".md-sidebar__scrollwrap",e),{y:a}=Be(i);return I(()=>{let c=new L;return c.pipe(Re(0,fe),me(t)).subscribe({next([{height:s},{height:f}]){i.style.height=`${s-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ce(fe),ve(1)).subscribe(()=>{for(let s of D(".md-nav__link--active[href]",e)){let f=ko(s);if(typeof f!="undefined"){let u=s.offsetTop-f.offsetTop,{height:l}=Se(f);f.scrollTo({top:u-l/2})}}}),Ds(e,r).pipe(M(s=>c.next(s)),R(()=>c.complete()),d(s=>V({ref:e},s)))})}function Ui(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ir(Ge(`${r}/releases/latest`).pipe(le(()=>k),d(n=>({version:n.tag_name})),De({})),Ge(r).pipe(le(()=>k),d(n=>({stars:n.stargazers_count,forks:n.forks_count})),De({}))).pipe(d(([n,o])=>V(V({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return Ge(r).pipe(d(n=>({repositories:n.public_repos})),De({}))}}function Vi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ge(r).pipe(le(()=>k),d(({star_count:n,forks_count:o})=>({stars:n,forks:o})),De({}))}function Ni(e){let[t]=e.match(/(git(?:hub|lab))/i)||[];switch(t.toLowerCase()){case"github":let[,r,n]=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);return Ui(r,n);case"gitlab":let[,o,i]=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i);return Vi(o,i);default:return k}}var Ws;function zs(e){return Ws||(Ws=I(()=>{let t=__md_get("__source",sessionStorage);if(t)return C(t);if(ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return k}return Ni(e.href).pipe(M(n=>__md_set("__source",n,sessionStorage)))}).pipe(le(()=>k),A(t=>Object.keys(t).length>0),d(t=>({facts:t})),X(1)))}function Di(e){let t=B(":scope > :last-child",e);return I(()=>{let r=new L;return r.subscribe(({facts:n})=>{t.appendChild(ri(n)),t.classList.add("md-source__repository--active")}),zs(e).pipe(M(n=>r.next(n)),R(()=>r.complete()),d(n=>V({ref:e},n)))})}function qs(e,{viewport$:t,header$:r}){return de(document.body).pipe(_(()=>mr(e,{header$:r,viewport$:t})),d(({offset:{y:n}})=>({hidden:n>=10})),J("hidden"))}function Wi(e,t){return I(()=>{let r=new L;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(Z("navigation.tabs.sticky")?C({hidden:!1}):qs(e,t)).pipe(M(n=>r.next(n)),R(()=>r.complete()),d(n=>V({ref:e},n)))})}function Ks(e,{viewport$:t,header$:r,excludedLinks:n}){let o=new Map,i=D("a[href]",e);for(let s of i){if(n!=null&&n.has(s))continue;let f=s.getAttribute("href"),u;if(f.startsWith("#")){let l=decodeURIComponent(s.hash.substring(1));u=ie(`[id="${l}"]`)}else u=ie(`a.pseudo-toc-entry[href=${CSS.escape(f)}]`);if(typeof u!="undefined"){let l=s.closest(".md-nav__link");l!==null&&o.set(l,u)}}let a=r.pipe(J("height"),d(({height:s})=>{let f=_e("main"),u=B(":scope > :first-child",f);return s+.8*(u.offsetTop-f.offsetTop)}),pe());return de(document.body).pipe(J("height"),_(s=>I(()=>{let f=[];return C([...o].reduce((u,[l,p])=>{for(;f.length&&o.get(f[f.length-1]).tagName>=p.tagName;)f.pop();let m=p.offsetTop;for(;!m&&p.parentElement;)p=p.parentElement,m=p.offsetTop;return u.set([...f=[...f,l]].reverse(),m)},new Map))}).pipe(d(f=>new Map([...f].sort(([,u],[,l])=>u-l))),St(a),_(([f,u])=>t.pipe(Nr(([l,p],{offset:{y:m},size:h})=>{let b=m+h.height>=Math.floor(s.height);for(;p.length;){let[,w]=p[0];if(w-u=m&&!b)p=[l.pop(),...p];else break}return[l,p]},[[],[...f]]),Q((l,p)=>l[0]===p[0]&&l[1]===p[1])))))).pipe(d(([s,f])=>({prev:s.map(([u])=>u),next:f.map(([u])=>u)})),z({prev:[],next:[]}),Ne(2,1),d(([s,f])=>s.prev.length{let i=new L,a=o?"md-nav__link--active":"md-nav__link--in-viewport";if(i.subscribe(({prev:s,next:f})=>{for(let[u]of f)u.classList.remove("md-nav__link--passed"),u.classList.remove(a);for(let[u,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle(a,u===s.length-1)}),Z("toc.follow")&&(o||!Z("toc.integrate"))){let s=!o||Z("toc.integrate");i.pipe(He(1)).subscribe(({prev:f})=>{var m;let u;if(f.length===0&&s&&(u=(m=e.querySelector("a[href='#']"))!=null?m:e),s=!1,f.length!==0&&(u=f[f.length-1][0]),u===void 0||!u.offsetHeight)return;let l=u.parentElement,p=5;for(;l!==null&&l.scrollHeight-p<=l.clientHeight;)l=l.parentElement;if(l!==null&&l!==document.body&&l!==document.documentElement){let h=u.getBoundingClientRect(),b=l.getBoundingClientRect();l.scrollTo({top:l.scrollTop+(h.y-b.height/2-b.y)})}})}o&&Z("navigation.tracking")&&t.pipe(te(i.pipe(ue(1))),J("offset"),He(250),Pe(1),te(n.pipe(Pe(1))),Ot({delay:250}),me(i)).subscribe(([,{prev:s}])=>{let f=Oe(),u=s[s.length-1];if(u&&u.length){let[l]=u,{hash:p}=new URL(l.href);f.hash!==p&&(f.hash=p,history.replaceState({},"",`${f}`))}else f.hash="",history.replaceState({},"",`${f}`)}),Z("toc.sticky")&&de(document.body).pipe(J("width"),He(0)).subscribe(()=>{let s=new Map,f="--md-nav__header-height";for(let u of D(".md-nav__link",e)){let l=u.nextElementSibling;if(!(l instanceof HTMLElement)||l.tagName!=="NAV")continue;let p="",m=NaN,h=l.parentElement.closest("nav");if(h!==null){let b=s.get(h);b!==void 0&&(p=`${b.height} + `,m=b.zindex-1)}isNaN(m)&&(m=100),p+=`${u.offsetHeight}px + 0.625em`,u.classList.add("md-nav__sticky"),u.style.setProperty("--md-nav__sticky-zindex",m.toString()),l.style.setProperty(f,`calc(${p})`),s.set(l,{height:p,zindex:m})}});let c=o?void 0:new Set(D("[data-md-component='toc'] a[href]",e));return Ks(e,{viewport$:t,header$:r,excludedLinks:c}).pipe(M(s=>i.next(s)),R(()=>i.complete()),d(s=>V({ref:e},s)))})}function Bs(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(d(({offset:{y:a}})=>a),Ne(2,1),d(([a,c])=>a>c&&c>0),Q()),i=r.pipe(d(({active:a})=>a));return G([i,o]).pipe(d(([a,c])=>!(a&&c)),Q(),te(n.pipe(Pe(1))),Vr(!0),Ot({delay:250}),d(a=>({hidden:a})))}function zi(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new L,a=i.pipe(ue(1));return i.subscribe({next({hidden:c}){e.hidden=c,c?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(te(a),J("height")).subscribe(({height:c})=>{e.style.top=`${c+16}px`}),Bs(e,{viewport$:t,main$:n,target$:o}).pipe(M(c=>i.next(c)),R(()=>i.complete()),d(c=>V({ref:e},c)))}function qi({document$:e,tablet$:t}){e.pipe(_(()=>D(".md-toggle--indeterminate, [data-md-state=indeterminate]")),M(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>x(r,"change").pipe(zr(()=>r.classList.contains("md-toggle--indeterminate")),d(()=>r))),me(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Ys(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ki({document$:e}){e.pipe(_(()=>D("[data-md-scrollfix]")),M(t=>t.removeAttribute("data-md-scrollfix")),A(Ys),oe(t=>x(t,"touchstart").pipe(d(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Bi({viewport$:e,tablet$:t}){G([ht("search"),t]).pipe(d(([r,n])=>r&&!n),_(r=>C(r).pipe(ke(r?400:100))),me(e)).subscribe(([r,{offset:{y:n}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${n}px`;else{let o=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",o&&window.scrollTo(0,o)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n!="object"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var nt=So(),vr=jo(),rt=Do(),un=Io(),ge=Jo(),gr=Yr("(min-width: 960px)"),Gi=Yr("(min-width: 1220px)"),Ji=Wo(),Gs=ce(),pn=new L;ki({alert$:pn});Z("navigation.instant")&&Pi({document$:nt,location$:vr,viewport$:ge});var Yi;((Yi=Gs.version)==null?void 0:Yi.provider)==="mike"&&ji({document$:nt});P(vr,rt).pipe(ke(125)).subscribe(()=>{Ye("drawer",!1),Ye("search",!1)});un.pipe(A(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ie("[href][rel=prev]");typeof t!="undefined"&&t.click();break;case"n":case".":let r=ie("[href][rel=next]");typeof r!="undefined"&&r.click();break}});qi({document$:nt,tablet$:gr});Ki({document$:nt});Bi({viewport$:ge,tablet$:gr});var We=xi(_e("header"),{viewport$:ge}),br=nt.pipe(d(()=>_e("main")),_(e=>Si(e,{viewport$:ge,header$:We})),X(1)),Js=P(...ee("consent").map(e=>Xo(e,{target$:rt})),...ee("dialog").map(e=>yi(e,{alert$:pn})),...ee("header").map(e=>wi(e,{viewport$:ge,header$:We,main$:br})),...ee("palette").map(e=>Oi(e)),...ee("search").map(e=>Hi(e,{keyboard$:un})),...ee("source").map(e=>Di(e))),Qs=I(()=>P(...ee("announce").map(e=>Qo(e)),...ee("content").map(e=>gi(e,{viewport$:ge,target$:rt,print$:Ji})),...ee("content").map(e=>Z("search.highlight")?Fi(e,{location$:vr}):k),...ee("header-title").map(e=>Ei(e,{viewport$:ge,header$:We})),...ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Gr(Gi,()=>fn(e,{viewport$:ge,header$:We,main$:br})):Gr(gr,()=>fn(e,{viewport$:ge,header$:We,main$:br}))),...ee("tabs").map(e=>Wi(e,{viewport$:ge,header$:We})),...ee("toc").map(e=>ln(e,{viewport$:ge,header$:We,target$:rt,localToc:!0})),...ee("sidebar").filter(e=>e.getAttribute("data-md-type")==="navigation").map(e=>ln(e,{viewport$:ge,header$:We,target$:rt,localToc:!1})),...ee("top").map(e=>zi(e,{viewport$:ge,header$:We,main$:br,target$:rt})))),Qi=nt.pipe(_(()=>Qs),tt(Js),X(1));Qi.subscribe();window.document$=nt;window.location$=vr;window.target$=rt;window.keyboard$=un;window.viewport$=ge;window.tablet$=gr;window.screen$=Gi;window.print$=Ji;window.alert$=pn;window.component$=Qi;})(); diff --git a/_static/sphinx_immaterial_theme.af531f03affe68837.min.css b/_static/sphinx_immaterial_theme.af531f03affe68837.min.css new file mode 100644 index 0000000..abac097 --- /dev/null +++ b/_static/sphinx_immaterial_theme.af531f03affe68837.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:content-box;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:separate;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:transparent;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root,[data-md-color-scheme=default]{--md-default-fg-color:rgba(0,0,0,.87);--md-default-fg-color--light:rgba(0,0,0,.54);--md-default-fg-color--lighter:rgba(0,0,0,.32);--md-default-fg-color--lightest:rgba(0,0,0,.07);--md-default-bg-color:#fff;--md-default-bg-color--light:hsla(0,0%,100%,.7);--md-default-bg-color--lighter:hsla(0,0%,100%,.3);--md-default-bg-color--lightest:hsla(0,0%,100%,.12);--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7);--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:rgba(255,255,0,.5);--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(255,255,0,.5);--md-typeset-del-color:rgba(245,80,61,.15);--md-typeset-ins-color:rgba(11,213,112,.15);--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-table-color:rgba(0,0,0,.12);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-fg-color:#fff;--md-footer-fg-color--light:hsla(0,0%,100%,.7);--md-footer-fg-color--lighter:hsla(0,0%,100%,.3);--md-footer-bg-color:rgba(0,0,0,.87);--md-footer-bg-color--dark:rgba(0,0,0,.32);--md-shadow-z1:0 0.2rem 0.5rem rgba(0,0,0,.05),0 0 0.05rem rgba(0,0,0,.1);--md-shadow-z2:0 0.2rem 0.5rem rgba(0,0,0,.1),0 0 0.05rem rgba(0,0,0,.25);--md-shadow-z3:0 0.2rem 0.5rem rgba(0,0,0,.2),0 0 0.05rem rgba(0,0,0,.35)}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}.si-icon-inline:before{background-color:var(--md-default-fg-color);content:"";display:inline-flex;height:1.125em;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;vertical-align:text-top;width:1.125em}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}body,input{font-feature-settings:"kern","liga";font-family:var(--md-text-font-family)}body,code,input,kbd,pre{color:var(--md-typeset-color)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset .code-block-caption+.notranslate .highlighttable,.md-typeset .code-block-caption+.notranslate pre{margin-top:0}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}@media (hover:none){.md-typeset abbr{position:relative}.md-typeset abbr[title]:-webkit-any(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}.md-typeset abbr[title]:-moz-any(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:-moz-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}[dir=ltr] .md-typeset abbr[title]:-webkit-any(:focus,:hover):after{left:0}[dir=ltr] .md-typeset abbr[title]:-moz-any(:focus,:hover):after{left:0}[dir=ltr] .md-typeset abbr[title]:is(:focus,:hover):after{left:0}[dir=rtl] .md-typeset abbr[title]:-webkit-any(:focus,:hover):after{right:0}[dir=rtl] .md-typeset abbr[title]:-moz-any(:focus,:hover):after{right:0}[dir=rtl] .md-typeset abbr[title]:is(:focus,:hover):after{right:0}.md-typeset abbr[title]:is(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:-moz-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}.md-typeset ol li :-webkit-any(ul,ol),.md-typeset ul li :-webkit-any(ul,ol){margin-bottom:.5em;margin-top:.5em}.md-typeset ol li :-moz-any(ul,ol),.md-typeset ul li :-moz-any(ul,ol){margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset ol li :-webkit-any(ul,ol),[dir=ltr] .md-typeset ul li :-webkit-any(ul,ol){margin-left:.625em}[dir=ltr] .md-typeset ol li :-moz-any(ul,ol),[dir=ltr] .md-typeset ul li :-moz-any(ul,ol){margin-left:.625em}[dir=ltr] .md-typeset ol li :is(ul,ol),[dir=ltr] .md-typeset ul li :is(ul,ol){margin-left:.625em}[dir=rtl] .md-typeset ol li :-webkit-any(ul,ol),[dir=rtl] .md-typeset ul li :-webkit-any(ul,ol){margin-right:.625em}[dir=rtl] .md-typeset ol li :-moz-any(ul,ol),[dir=rtl] .md-typeset ul li :-moz-any(ul,ol){margin-right:.625em}[dir=rtl] .md-typeset ol li :is(ul,ol),[dir=rtl] .md-typeset ul li :is(ul,ol){margin-right:.625em}.md-typeset ol li :is(ul,ol),.md-typeset ul li :is(ul,ol){margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset img[src$="#gh-dark-mode-only"],.md-typeset img[src$="#only-dark"]{display:none}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table.data:not(.plain){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto;width:-moz-max-content;width:max-content}@media print{.md-typeset table.data:not(.plain){display:table}}.md-typeset table.data:not(.plain)+*{margin-top:1.5em}.md-typeset table.data:not(.plain) :-webkit-any(th,td)>:first-child{margin-top:0}.md-typeset table.data:not(.plain) :-moz-any(th,td)>:first-child{margin-top:0}.md-typeset table.data:not(.plain) :is(th,td)>:first-child{margin-top:0}.md-typeset table.data:not(.plain) :-webkit-any(th,td)>:last-child{margin-bottom:0}.md-typeset table.data:not(.plain) :-moz-any(th,td)>:last-child{margin-bottom:0}.md-typeset table.data:not(.plain) :is(th,td)>:last-child{margin-bottom:0}.md-typeset table.data:not(.plain) :-webkit-any(th,td):not([align],.align-center,.align-left,.align-right){text-align:left}.md-typeset table.data:not(.plain) :-moz-any(th,td):not([align],.align-center,.align-left,.align-right){text-align:left}.md-typeset table.data:not(.plain) :is(th,td):not([align],.align-center,.align-left,.align-right){text-align:left}[dir=rtl] .md-typeset table.data:not(.plain) :-webkit-any(th,td):not([align],.align-center,.align-left,.align-right){text-align:right}[dir=rtl] .md-typeset table.data:not(.plain) :-moz-any(th,td):not([align],.align-center,.align-left,.align-right){text-align:right}[dir=rtl] .md-typeset table.data:not(.plain) :is(th,td):not([align],.align-center,.align-left,.align-right){text-align:right}.md-typeset table.data:not(.plain) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table.data:not(.plain) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table.data:not(.plain) tbody tr{transition:background-color 125ms}.md-typeset table.data:not(.plain) tbody tr:hover{background-color:rgba(0,0,0,.035);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table.data:not(.plain) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.9375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background:var(--md-typeset-mark-color);color:var(--md-default-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.md-banner__button:hover{opacity:.7}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.9375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:-webkit-any(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-clipboard--inline:-moz-any(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-clipboard--inline:is(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:rgba(0,0,0,.54);height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.9375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{align-content:baseline;display:flex;flex-wrap:wrap;justify-content:center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{display:flex;flex-grow:0.01;outline-color:var(--md-accent-fg-color);overflow:hidden;padding-bottom:.4rem;padding-top:1.4rem;transition:opacity .25s}.md-footer__link:-webkit-any(:focus,:hover){opacity:.7}.md-footer__link:-moz-any(:focus,:hover){opacity:.7}.md-footer__link:is(:focus,:hover){opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.9375em){.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;line-height:2.4rem;max-width:calc(100% - 2.4rem);padding:0 1rem;position:relative;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;left:0;margin-top:-1rem;opacity:.7;padding:0 1rem;position:absolute;right:0}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:-webkit-any(:focus,:hover){color:var(--md-footer-fg-color)}html .md-footer-meta.md-typeset a:-moz-any(:focus,:hover){color:var(--md-footer-fg-color)}html .md-footer-meta.md-typeset a:is(:focus,:hover){color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-typeset .md-button:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-typeset .md-button:is(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:-webkit-any(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input:-moz-any(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input:is(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem transparent,0 .2rem .4rem transparent;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.1875em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo :-webkit-any(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo :-moz-any(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo :is(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem}[dir=ltr] .md-header__title{margin-left:1rem}[dir=rtl] .md-header__title{margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-hero{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-size:1rem;overflow:hidden;transition:background .25s}.md-hero__inner{margin-top:1rem;padding:.8rem .8rem .4rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s;transition-delay:.1s}@media screen and (max-width:76.1875em){.md-hero__inner{margin-bottom:1.2rem;margin-top:2.4rem}}[data-md-state=hidden] .md-hero__inner{opacity:0;pointer-events:none;transform:translateY(.625rem);transition:transform 0ms .4s,opacity .1s 0ms}.md-hero--expand .md-hero__inner{margin-bottom:1.2rem}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{align-items:center;display:flex;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo :-webkit-any(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__title .md-nav__button.md-logo :-moz-any(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__title .md-nav__button.md-logo :is(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;-o-object-fit:contain;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__item{padding:0 .6rem}[dir=ltr] .md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-left:0}.md-nav__link{align-items:center;cursor:pointer;display:flex;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link.md-nav__sticky{box-shadow:0 -.625em var(--md-default-bg-color),0 .625em var(--md-default-bg-color)}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{color:var(--md-typeset-a-color)}.md-nav__link--in-viewport{position:relative}.md-nav__link--in-viewport:before{background-color:var(--md-primary-fg-color);bottom:0;content:"";height:100%;position:absolute;right:calc(100% + .3rem);top:0;width:.05rem}.md-nav__item .md-nav__link--index [href]{width:100%}.md-nav__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__link>*{cursor:pointer;display:flex}.md-nav__sticky{background-color:var(--md-default-bg-color);position:sticky;top:var(--md-nav__header-height,0);z-index:var(--md-nav__sticky-zindex)}.md-nav .md-ellipsis{display:block;flex-grow:1;white-space:normal}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.1875em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__sticky{background-color:transparent;box-shadow:none;position:static;z-index:auto}.md-nav--primary :-webkit-any(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary :-moz-any(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary :is(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;line-height:2.4rem;min-height:5.6rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest);padding:0}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:transparent;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:transparent}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}.md-nav .md-nav__title .md-ellipsis{white-space:nowrap}.md-nav .md-nav__title .md-ellipsis wbr{display:none}}@media screen and (max-width:59.9375em){.md-nav__current-nested{display:none}.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav__current-toc{display:none}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}}@media screen and (min-width:76.25em){.md-nav{transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon,.md-nav__toggle~.md-nav{display:none}.md-nav__toggle:-webkit-any(:checked,:indeterminate)~.md-nav{display:block}.md-nav__toggle:-moz-any(:checked,:indeterminate)~.md-nav{display:block}.md-nav__toggle:is(:checked,:indeterminate)~.md-nav{display:block}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700;pointer-events:none}.md-nav__item--section>.md-nav__link--index [href]{pointer-events:auto}.md-nav__item--section>.md-nav__link .md-nav__icon{display:none}.md-nav__item--section>.md-nav{display:block}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s,transform .25s;width:.9rem}[dir=rtl] .md-nav__icon{transform:rotate(180deg)}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:-.1rem;width:100%}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__list>.md-nav__item--nested,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block;padding:0}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);font-weight:700;margin-top:0;padding:0 .6rem;position:sticky;top:0;z-index:var(--md-nav__sticky-zindex,1)}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__link--index){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link .md-nav__icon{display:none}.md-nav--lifted .md-nav[data-md-level="1"]{display:block}[dir=ltr] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-right:.6rem}[dir=rtl] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-left:.6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested){padding:0 .6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested)>.md-nav__link{padding:0}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:rgba(0,0,0,.54);cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@media screen and (min-width:60em) and (max-width:76.1875em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem transparent;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:rgba(0,0,0,.26);border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:hsla(0,0%,100%,.12)}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem rgba(0,0,0,.07);color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:transparent;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::-moz-placeholder{-moz-transition:color .25s;transition:color .25s}.md-search__input::placeholder{transition:color .25s}.md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.9375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::-moz-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon,[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:var(--md-default-fg-color--light)}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>*{margin-left:.2rem}[dir=rtl] .md-search__options>*{margin-right:.2rem}.md-search__options>*{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>*{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.9375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:is(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more summary{color:var(--md-typeset-a-color);cursor:pointer;display:block;font-size:.64rem;outline:none;padding:.75em .8rem;scroll-snap-align:start;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more summary{padding-left:2.2rem}[dir=rtl] .md-search-result__more summary{padding-right:2.2rem}}.md-search-result__more summary:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary:is(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary::marker{display:none}.md-search-result__more summary::-webkit-details-marker{display:none}.md-search-result__more summary~*>*{opacity:.65}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}.md-search-result__article--document .md-search-result__title{font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.9375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result__title{font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result__teaser{-webkit-box-orient:vertical;-webkit-line-clamp:2;color:var(--md-default-fg-color--light);display:-webkit-box;font-size:.64rem;line-height:1.6;margin:.5em 0;max-height:2rem;overflow:hidden;text-overflow:ellipsis}@media screen and (max-width:44.9375em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}.md-search-result__teaser mark{background-color:transparent;text-decoration:underline}.md-search-result__terms{font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:transparent;color:var(--md-accent-fg-color)}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:-webkit-any(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);-webkit-transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms;transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select:-moz-any(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);-moz-transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms;transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select:is(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid transparent;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid transparent;border-right:.2rem solid transparent;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.1875em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.1875em){.md-overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.1875em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;list-style:none;margin:0;padding:0;white-space:nowrap}.md-tabs__item{display:inline-block;height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link--active,.md-tabs__link:-webkit-any(:focus,:hover){color:inherit;opacity:1}.md-tabs__link--active,.md-tabs__link:-moz-any(:focus,:hover){color:inherit;opacity:1}.md-tabs__link--active,.md-tabs__link:is(:focus,:hover){color:inherit;opacity:1}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags{margin-bottom:.75em;margin-top:-.125em}[dir=ltr] .md-typeset .md-tag{margin-right:.5em}[dir=rtl] .md-typeset .md-tag{margin-left:.5em}.md-typeset .md-tag{background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-block;font-size:.64rem;font-weight:700;letter-spacing:normal;line-height:1.6;margin-bottom:.5em;padding:.3125em .9375em;vertical-align:middle}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;margin-right:.4em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon:-webkit-any(a:focus,a:hover):before{background-color:var(--md-accent-bg-color)}.md-typeset .md-tag-icon:-moz-any(a:focus,a:hover):before{background-color:var(--md-accent-bg-color)}.md-typeset .md-tag-icon:is(a:focus,a:hover):before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{box-shadow:0 0 0 0 var(--md-default-fg-color--lightest);transform:scale(.95)}75%{box-shadow:0 0 0 .625em transparent;transform:scale(1)}to{box-shadow:0 0 0 0 transparent;transform:scale(.95)}}:root{--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}:-webkit-any(.focus-visible>.md-tooltip,.md-tooltip:target){outline:var(--md-accent-fg-color) auto}:-moz-any(.focus-visible>.md-tooltip,.md-tooltip:target){outline:var(--md-accent-fg-color) auto}:is(.focus-visible>.md-tooltip,.md-tooltip:target){outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}.md-annotation:not([hidden]){display:inline-block;line-height:1.325}.md-annotation__index{cursor:pointer;font-family:var(--md-code-font-family);font-size:.85em;margin:0 1ch;outline:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:0}.md-annotation .md-annotation__index{color:#fff;transition:z-index .25s}.md-annotation .md-annotation__index:-webkit-any(:focus,:hover){color:#fff}.md-annotation .md-annotation__index:-moz-any(:focus,:hover){color:#fff}.md-annotation .md-annotation__index:is(:focus,:hover){color:#fff}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);border-radius:2ch;content:"";height:2.2ch;left:-.125em;margin:0 -.4ch;padding:0 .4ch;position:absolute;top:0;transition:color .25s,background-color .25s;width:calc(100% + 1.2ch);width:max(2.2ch,100% + 1.2ch);z-index:-1}@media not all and (prefers-reduced-motion){[data-md-visible]>.md-annotation__index:after{animation:pulse 2s infinite}}.md-tooltip--active+.md-annotation__index:after{animation:none;transition:color .25s,background-color .25s}code .md-annotation__index{font-family:var(--md-code-font-family);font-size:inherit}:-webkit-any(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index){color:var(--md-accent-bg-color)}:-moz-any(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index){color:var(--md-accent-bg-color)}:is(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index){color:var(--md-accent-bg-color)}:-webkit-any(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index):after{background-color:var(--md-accent-fg-color)}:-moz-any(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index):after{background-color:var(--md-accent-fg-color)}:is(.md-tooltip--active+.md-annotation__index,:hover>.md-annotation__index):after{background-color:var(--md-accent-fg-color)}.md-tooltip--active+.md-annotation__index{animation:none;transition:none;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block;line-height:90%}.md-annotation__index [data-md-annotation-id]:before{content:attr(data-md-annotation-id);display:inline-block;padding-bottom:.1em;transform:scale(1.15);transition:transform .4s cubic-bezier(.1,.7,.1,1);vertical-align:.065em}@media not print{.md-annotation__index [data-md-annotation-id]:before{content:"+"}:focus-within>.md-annotation__index [data-md-annotation-id]:before{transform:scale(1.25) rotate(45deg)}}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top:is(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:-webkit-any(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;-webkit-transition:max-height 0ms,opacity .25s;transition:max-height 0ms,opacity .25s}.md-version:-moz-any(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;-moz-transition:max-height 0ms,opacity .25s;transition:max-height 0ms,opacity .25s}.md-version:is(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.05rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition :-webkit-any(.admonition,details),.md-typeset details :-webkit-any(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset .admonition :-moz-any(.admonition,details),.md-typeset details :-moz-any(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset .admonition :is(.admonition,details),.md-typeset details :is(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:rgba(68,138,255,.1);border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.note){border-color:#448aff}.md-typeset :-moz-any(.admonition,details):-moz-any(.note){border-color:#448aff}.md-typeset :is(.admonition,details):is(.note){border-color:#448aff}.md-typeset :-webkit-any(.note)>:-webkit-any(.admonition-title,summary){background-color:rgba(68,138,255,.1)}.md-typeset :-moz-any(.note)>:-moz-any(.admonition-title,summary){background-color:rgba(68,138,255,.1)}.md-typeset :is(.note)>:is(.admonition-title,summary){background-color:rgba(68,138,255,.1)}.md-typeset :-webkit-any(.note)>:-webkit-any(.admonition-title,summary):before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset :-moz-any(.note)>:-moz-any(.admonition-title,summary):before{background-color:#448aff;mask-image:var(--md-admonition-icon--note)}.md-typeset :is(.note)>:is(.admonition-title,summary):before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset :-webkit-any(.note)>:-webkit-any(.admonition-title,summary):after{color:#448aff}.md-typeset :-moz-any(.note)>:-moz-any(.admonition-title,summary):after{color:#448aff}.md-typeset :is(.note)>:is(.admonition-title,summary):after{color:#448aff}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :-moz-any(.admonition,details):-moz-any(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :is(.admonition,details):is(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :-webkit-any(.abstract,.summary,.tldr)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,176,255,.1)}.md-typeset :-moz-any(.abstract,.summary,.tldr)>:-moz-any(.admonition-title,summary){background-color:rgba(0,176,255,.1)}.md-typeset :is(.abstract,.summary,.tldr)>:is(.admonition-title,summary){background-color:rgba(0,176,255,.1)}.md-typeset :-webkit-any(.abstract,.summary,.tldr)>:-webkit-any(.admonition-title,summary):before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset :-moz-any(.abstract,.summary,.tldr)>:-moz-any(.admonition-title,summary):before{background-color:#00b0ff;mask-image:var(--md-admonition-icon--abstract)}.md-typeset :is(.abstract,.summary,.tldr)>:is(.admonition-title,summary):before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset :-webkit-any(.abstract,.summary,.tldr)>:-webkit-any(.admonition-title,summary):after{color:#00b0ff}.md-typeset :-moz-any(.abstract,.summary,.tldr)>:-moz-any(.admonition-title,summary):after{color:#00b0ff}.md-typeset :is(.abstract,.summary,.tldr)>:is(.admonition-title,summary):after{color:#00b0ff}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.info,.todo){border-color:#00b8d4}.md-typeset :-moz-any(.admonition,details):-moz-any(.info,.todo){border-color:#00b8d4}.md-typeset :is(.admonition,details):is(.info,.todo){border-color:#00b8d4}.md-typeset :-webkit-any(.info,.todo)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,184,212,.1)}.md-typeset :-moz-any(.info,.todo)>:-moz-any(.admonition-title,summary){background-color:rgba(0,184,212,.1)}.md-typeset :is(.info,.todo)>:is(.admonition-title,summary){background-color:rgba(0,184,212,.1)}.md-typeset :-webkit-any(.info,.todo)>:-webkit-any(.admonition-title,summary):before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset :-moz-any(.info,.todo)>:-moz-any(.admonition-title,summary):before{background-color:#00b8d4;mask-image:var(--md-admonition-icon--info)}.md-typeset :is(.info,.todo)>:is(.admonition-title,summary):before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset :-webkit-any(.info,.todo)>:-webkit-any(.admonition-title,summary):after{color:#00b8d4}.md-typeset :-moz-any(.info,.todo)>:-moz-any(.admonition-title,summary):after{color:#00b8d4}.md-typeset :is(.info,.todo)>:is(.admonition-title,summary):after{color:#00b8d4}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :-moz-any(.admonition,details):-moz-any(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :is(.admonition,details):is(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :-webkit-any(.tip,.hint,.important)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,191,165,.1)}.md-typeset :-moz-any(.tip,.hint,.important)>:-moz-any(.admonition-title,summary){background-color:rgba(0,191,165,.1)}.md-typeset :is(.tip,.hint,.important)>:is(.admonition-title,summary){background-color:rgba(0,191,165,.1)}.md-typeset :-webkit-any(.tip,.hint,.important)>:-webkit-any(.admonition-title,summary):before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset :-moz-any(.tip,.hint,.important)>:-moz-any(.admonition-title,summary):before{background-color:#00bfa5;mask-image:var(--md-admonition-icon--tip)}.md-typeset :is(.tip,.hint,.important)>:is(.admonition-title,summary):before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset :-webkit-any(.tip,.hint,.important)>:-webkit-any(.admonition-title,summary):after{color:#00bfa5}.md-typeset :-moz-any(.tip,.hint,.important)>:-moz-any(.admonition-title,summary):after{color:#00bfa5}.md-typeset :is(.tip,.hint,.important)>:is(.admonition-title,summary):after{color:#00bfa5}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.success,.check,.done){border-color:#00c853}.md-typeset :-moz-any(.admonition,details):-moz-any(.success,.check,.done){border-color:#00c853}.md-typeset :is(.admonition,details):is(.success,.check,.done){border-color:#00c853}.md-typeset :-webkit-any(.success,.check,.done)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,200,83,.1)}.md-typeset :-moz-any(.success,.check,.done)>:-moz-any(.admonition-title,summary){background-color:rgba(0,200,83,.1)}.md-typeset :is(.success,.check,.done)>:is(.admonition-title,summary){background-color:rgba(0,200,83,.1)}.md-typeset :-webkit-any(.success,.check,.done)>:-webkit-any(.admonition-title,summary):before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset :-moz-any(.success,.check,.done)>:-moz-any(.admonition-title,summary):before{background-color:#00c853;mask-image:var(--md-admonition-icon--success)}.md-typeset :is(.success,.check,.done)>:is(.admonition-title,summary):before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset :-webkit-any(.success,.check,.done)>:-webkit-any(.admonition-title,summary):after{color:#00c853}.md-typeset :-moz-any(.success,.check,.done)>:-moz-any(.admonition-title,summary):after{color:#00c853}.md-typeset :is(.success,.check,.done)>:is(.admonition-title,summary):after{color:#00c853}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.question,.help,.faq){border-color:#64dd17}.md-typeset :-moz-any(.admonition,details):-moz-any(.question,.help,.faq){border-color:#64dd17}.md-typeset :is(.admonition,details):is(.question,.help,.faq){border-color:#64dd17}.md-typeset :-webkit-any(.question,.help,.faq)>:-webkit-any(.admonition-title,summary){background-color:rgba(100,221,23,.1)}.md-typeset :-moz-any(.question,.help,.faq)>:-moz-any(.admonition-title,summary){background-color:rgba(100,221,23,.1)}.md-typeset :is(.question,.help,.faq)>:is(.admonition-title,summary){background-color:rgba(100,221,23,.1)}.md-typeset :-webkit-any(.question,.help,.faq)>:-webkit-any(.admonition-title,summary):before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset :-moz-any(.question,.help,.faq)>:-moz-any(.admonition-title,summary):before{background-color:#64dd17;mask-image:var(--md-admonition-icon--question)}.md-typeset :is(.question,.help,.faq)>:is(.admonition-title,summary):before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset :-webkit-any(.question,.help,.faq)>:-webkit-any(.admonition-title,summary):after{color:#64dd17}.md-typeset :-moz-any(.question,.help,.faq)>:-moz-any(.admonition-title,summary):after{color:#64dd17}.md-typeset :is(.question,.help,.faq)>:is(.admonition-title,summary):after{color:#64dd17}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :-moz-any(.admonition,details):-moz-any(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :is(.admonition,details):is(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :-webkit-any(.warning,.caution,.attention)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,145,0,.1)}.md-typeset :-moz-any(.warning,.caution,.attention)>:-moz-any(.admonition-title,summary){background-color:rgba(255,145,0,.1)}.md-typeset :is(.warning,.caution,.attention)>:is(.admonition-title,summary){background-color:rgba(255,145,0,.1)}.md-typeset :-webkit-any(.warning,.caution,.attention)>:-webkit-any(.admonition-title,summary):before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset :-moz-any(.warning,.caution,.attention)>:-moz-any(.admonition-title,summary):before{background-color:#ff9100;mask-image:var(--md-admonition-icon--warning)}.md-typeset :is(.warning,.caution,.attention)>:is(.admonition-title,summary):before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset :-webkit-any(.warning,.caution,.attention)>:-webkit-any(.admonition-title,summary):after{color:#ff9100}.md-typeset :-moz-any(.warning,.caution,.attention)>:-moz-any(.admonition-title,summary):after{color:#ff9100}.md-typeset :is(.warning,.caution,.attention)>:is(.admonition-title,summary):after{color:#ff9100}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :-moz-any(.admonition,details):-moz-any(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :is(.admonition,details):is(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :-webkit-any(.failure,.fail,.missing)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,82,82,.1)}.md-typeset :-moz-any(.failure,.fail,.missing)>:-moz-any(.admonition-title,summary){background-color:rgba(255,82,82,.1)}.md-typeset :is(.failure,.fail,.missing)>:is(.admonition-title,summary){background-color:rgba(255,82,82,.1)}.md-typeset :-webkit-any(.failure,.fail,.missing)>:-webkit-any(.admonition-title,summary):before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset :-moz-any(.failure,.fail,.missing)>:-moz-any(.admonition-title,summary):before{background-color:#ff5252;mask-image:var(--md-admonition-icon--failure)}.md-typeset :is(.failure,.fail,.missing)>:is(.admonition-title,summary):before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset :-webkit-any(.failure,.fail,.missing)>:-webkit-any(.admonition-title,summary):after{color:#ff5252}.md-typeset :-moz-any(.failure,.fail,.missing)>:-moz-any(.admonition-title,summary):after{color:#ff5252}.md-typeset :is(.failure,.fail,.missing)>:is(.admonition-title,summary):after{color:#ff5252}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.danger,.error){border-color:#ff1744}.md-typeset :-moz-any(.admonition,details):-moz-any(.danger,.error){border-color:#ff1744}.md-typeset :is(.admonition,details):is(.danger,.error){border-color:#ff1744}.md-typeset :-webkit-any(.danger,.error)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,23,68,.1)}.md-typeset :-moz-any(.danger,.error)>:-moz-any(.admonition-title,summary){background-color:rgba(255,23,68,.1)}.md-typeset :is(.danger,.error)>:is(.admonition-title,summary){background-color:rgba(255,23,68,.1)}.md-typeset :-webkit-any(.danger,.error)>:-webkit-any(.admonition-title,summary):before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset :-moz-any(.danger,.error)>:-moz-any(.admonition-title,summary):before{background-color:#ff1744;mask-image:var(--md-admonition-icon--danger)}.md-typeset :is(.danger,.error)>:is(.admonition-title,summary):before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset :-webkit-any(.danger,.error)>:-webkit-any(.admonition-title,summary):after{color:#ff1744}.md-typeset :-moz-any(.danger,.error)>:-moz-any(.admonition-title,summary):after{color:#ff1744}.md-typeset :is(.danger,.error)>:is(.admonition-title,summary):after{color:#ff1744}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.bug){border-color:#f50057}.md-typeset :-moz-any(.admonition,details):-moz-any(.bug){border-color:#f50057}.md-typeset :is(.admonition,details):is(.bug){border-color:#f50057}.md-typeset :-webkit-any(.bug)>:-webkit-any(.admonition-title,summary){background-color:rgba(245,0,87,.1)}.md-typeset :-moz-any(.bug)>:-moz-any(.admonition-title,summary){background-color:rgba(245,0,87,.1)}.md-typeset :is(.bug)>:is(.admonition-title,summary){background-color:rgba(245,0,87,.1)}.md-typeset :-webkit-any(.bug)>:-webkit-any(.admonition-title,summary):before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset :-moz-any(.bug)>:-moz-any(.admonition-title,summary):before{background-color:#f50057;mask-image:var(--md-admonition-icon--bug)}.md-typeset :is(.bug)>:is(.admonition-title,summary):before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset :-webkit-any(.bug)>:-webkit-any(.admonition-title,summary):after{color:#f50057}.md-typeset :-moz-any(.bug)>:-moz-any(.admonition-title,summary):after{color:#f50057}.md-typeset :is(.bug)>:is(.admonition-title,summary):after{color:#f50057}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.example){border-color:#7c4dff}.md-typeset :-moz-any(.admonition,details):-moz-any(.example){border-color:#7c4dff}.md-typeset :is(.admonition,details):is(.example){border-color:#7c4dff}.md-typeset :-webkit-any(.example)>:-webkit-any(.admonition-title,summary){background-color:rgba(124,77,255,.1)}.md-typeset :-moz-any(.example)>:-moz-any(.admonition-title,summary){background-color:rgba(124,77,255,.1)}.md-typeset :is(.example)>:is(.admonition-title,summary){background-color:rgba(124,77,255,.1)}.md-typeset :-webkit-any(.example)>:-webkit-any(.admonition-title,summary):before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset :-moz-any(.example)>:-moz-any(.admonition-title,summary):before{background-color:#7c4dff;mask-image:var(--md-admonition-icon--example)}.md-typeset :is(.example)>:is(.admonition-title,summary):before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset :-webkit-any(.example)>:-webkit-any(.admonition-title,summary):after{color:#7c4dff}.md-typeset :-moz-any(.example)>:-moz-any(.admonition-title,summary):after{color:#7c4dff}.md-typeset :is(.example)>:is(.admonition-title,summary):after{color:#7c4dff}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.quote,.cite){border-color:#9e9e9e}.md-typeset :-moz-any(.admonition,details):-moz-any(.quote,.cite){border-color:#9e9e9e}.md-typeset :is(.admonition,details):is(.quote,.cite){border-color:#9e9e9e}.md-typeset :-webkit-any(.quote,.cite)>:-webkit-any(.admonition-title,summary){background-color:hsla(0,0%,62%,.1)}.md-typeset :-moz-any(.quote,.cite)>:-moz-any(.admonition-title,summary){background-color:hsla(0,0%,62%,.1)}.md-typeset :is(.quote,.cite)>:is(.admonition-title,summary){background-color:hsla(0,0%,62%,.1)}.md-typeset :-webkit-any(.quote,.cite)>:-webkit-any(.admonition-title,summary):before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset :-moz-any(.quote,.cite)>:-moz-any(.admonition-title,summary):before{background-color:#9e9e9e;mask-image:var(--md-admonition-icon--quote)}.md-typeset :is(.quote,.cite)>:is(.admonition-title,summary):before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset :-webkit-any(.quote,.cite)>:-webkit-any(.admonition-title,summary):after{color:#9e9e9e}.md-typeset :-moz-any(.quote,.cite)>:-moz-any(.admonition-title,summary):after{color:#9e9e9e}.md-typeset :is(.quote,.cite)>:is(.admonition-title,summary):after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:-webkit-any(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li:-moz-any(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li:is(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :-webkit-any(:hover,:target)>.headerlink{opacity:1;-webkit-transition:color .25s,opacity 125ms;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset :-moz-any(:hover,:target)>.headerlink{opacity:1;-moz-transition:color .25s,opacity 125ms;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset :is(:hover,:target)>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:-webkit-any(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset .headerlink:-moz-any(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset .headerlink:is(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset :-webkit-any(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset :-moz-any(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset :is(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.9375em){.md-typeset div.arithmatex{margin:0 -.8rem}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto;width:-moz-min-content;width:min-content}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset :-webkit-any(del,ins,.comment).critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset :-moz-any(del,ins,.comment).critic{box-decoration-break:clone}.md-typeset :is(del,ins,.comment).critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :-moz-any(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :is(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.md-typeset :-moz-any(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.md-typeset :is(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.highlight :-webkit-any(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight :-moz-any(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight :is(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight :-webkit-any(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :-moz-any(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :is(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :-webkit-any(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :-moz-any(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :is(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :-webkit-any(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :-moz-any(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :is(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :-webkit-any(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :-moz-any(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :is(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :-webkit-any(.n){color:var(--md-code-hl-name-color)}.highlight :-moz-any(.n){color:var(--md-code-hl-name-color)}.highlight :is(.n){color:var(--md-code-hl-name-color)}.highlight :-webkit-any(.kc,.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :-moz-any(.kc,.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :is(.kc,.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :-webkit-any(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :-moz-any(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :is(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :-webkit-any(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :-moz-any(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :is(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :-webkit-any(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :-moz-any(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :is(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :-webkit-any(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :-moz-any(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :is(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :-webkit-any(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :-moz-any(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :is(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :-webkit-any(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight :-moz-any(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight :is(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color);display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable :-webkit-any(tbody,td){display:block;padding:0}.highlighttable :-moz-any(tbody,td){display:block;padding:0}.highlighttable :is(tbody,td){display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset :-webkit-any(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :-moz-any(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :is(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :-webkit-any(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result:after{clear:both;content:"";display:block}.md-typeset :-moz-any(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result:after{clear:both;content:"";display:block}.md-typeset :is(.highlight,.highlighttable,.literal-block-wrapper,div[class^=highlight-],.results-prefix)+.result:after{clear:both;content:"";display:block}.md-typeset .results .results-prefix+.result{margin-top:0}.md-typeset .results .results-prefix{background-color:var(--md-code-bg-color);font-size:.85em;font-weight:700;margin-top:-1em;padding:.6617647059em 1.1764705882em}@media screen and (max-width:44.9375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:-webkit-any(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys kbd:-moz-any(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys kbd:is(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-accent-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid transparent;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-accent-fg-color)}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,transparent);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,transparent);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.9375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-accent-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){background-color:var(--md-accent-fg-color--transparent)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}.rst-versions{font-family:var(--md-text-font-family)}.rst-versions.rst-badge{bottom:inherit!important;font-size:.85rem;height:auto;top:50px}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color)}.mermaid{line-height:normal;margin:1em 0}:root>*{--md-graphviz-edge-color:var(--md-default-fg-color);--md-graphviz-node-bg-color:var(--md-accent-fg-color--transparent);--md-graphviz-node-fg-color:var(--md-accent-fg-color);--md-graphviz-label-bg-color:var(--md-default-bg-color);--md-graphviz-label-fg-color:var(--md-code-fg-color);--md-graphviz-a-hover-color:var(--md-primary-fg-color)}.graphviz{margin:1em 0}.graphviz a:hover>text{fill:var(--md-graphviz-hover-color)!important}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}}.md-typeset .align-left{text-align:left}.md-typeset .align-right{text-align:right}.md-typeset .align-center{clear:both;text-align:center}.md-typeset .align-top{vertical-align:top}.md-typeset .align-middle{vertical-align:middle}.md-typeset .align-bottom{vertical-align:bottom}.md-typeset .figure.align-left,.md-typeset figure.align-left,.md-typeset img.align-left,.md-typeset object.align-left,.md-typeset table.align-left{margin-right:auto}.md-typeset .figure.align-center,.md-typeset figure.align-center,.md-typeset img.align-center,.md-typeset object.align-center,.md-typeset table.align-center{margin-left:auto;margin-right:auto}.md-typeset .figure.align-right,.md-typeset figure.align-right,.md-typeset img.align-right,.md-typeset object.align-right,.md-typeset table.align-right{margin-left:auto}.md-typeset .figure.align-center,.md-typeset .figure.align-right,.md-typeset figure.align-center,.md-typeset figure.align-right,.md-typeset img.align-center,.md-typeset img.align-right,.md-typeset object.align-center,.md-typeset object.align-right{display:block}.md-typeset .figure.align-left,.md-typeset .figure.align-right,.md-typeset figure.align-left,.md-typeset figure.align-right,.md-typeset table.align-center,.md-typeset table.align-left,.md-typeset table.align-right{text-align:inherit}.md-typeset .rubric{font-weight:700}.md-typeset .viewcode-block .viewcode-back{float:right}.md-typeset .versionmodified{font-style:italic}.md-typeset div.line-block{display:block}.md-typeset div.line-block div.line-block{margin-left:1.5em}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt{background:var(--md-code-bg-color);font-family:var(--md-code-font-family)}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt{background:var(--md-code-bg-color);font-family:var(--md-code-font-family)}.md-typeset :is(dl.objdesc,dl.api-field)>dt{background:var(--md-code-bg-color);font-family:var(--md-code-font-family)}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt code{border-radius:0;padding:0}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt code{border-radius:0;padding:0}.md-typeset :is(dl.objdesc,dl.api-field)>dt code{border-radius:0;padding:0}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .sig-name:not(.sig-name-nonprimary){color:var(--md-code-hl-name-color);font-weight:700;padding:0}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .sig-name:not(.sig-name-nonprimary){color:var(--md-code-hl-name-color);font-weight:700;padding:0}.md-typeset :is(dl.objdesc,dl.api-field)>dt .sig-name:not(.sig-name-nonprimary){color:var(--md-code-hl-name-color);font-weight:700;padding:0}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .sig-param{font-style:normal}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .sig-param{font-style:normal}.md-typeset :is(dl.objdesc,dl.api-field)>dt .sig-param{font-style:normal}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .sig-param .n:not(.desctype){color:var(--md-default-fg-color--light)}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .sig-param .n:not(.desctype){color:var(--md-default-fg-color--light)}.md-typeset :is(dl.objdesc,dl.api-field)>dt .sig-param .n:not(.desctype){color:var(--md-default-fg-color--light)}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .sig-param a.reference .n:not(.desctype):hover{color:var(--md-accent-fg-color)}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .sig-param a.reference .n:not(.desctype):hover{color:var(--md-accent-fg-color)}.md-typeset :is(dl.objdesc,dl.api-field)>dt .sig-param a.reference .n:not(.desctype):hover{color:var(--md-accent-fg-color)}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-param-decl:before{content:"\a ";white-space:pre}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-param-decl:before{content:"\a ";white-space:pre}.md-typeset :is(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-param-decl:before{content:"\a ";white-space:pre}.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-paren~.sig-paren:before{content:"\a";white-space:pre}.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-paren~.sig-paren:before{content:"\a";white-space:pre}.md-typeset :is(dl.objdesc,dl.api-field)>dt.sig-wrap .sig-paren~.sig-paren:before{content:"\a";white-space:pre}.md-typeset dl.objdesc>dd>dl.field-list>dt>.colon{display:none}.md-typeset .sig-inline a.reference.sig-name,.md-typeset .sig-inline a.reference:not(.desctype)>.n,.md-typeset .sig-inline a.reference>.sig-name,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference.sig-name,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference>.sig-name{color:var(--md-typeset-a-color)}.md-typeset .sig-inline a.reference.sig-name,.md-typeset .sig-inline a.reference:not(.desctype)>.n,.md-typeset .sig-inline a.reference>.sig-name,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference.sig-name,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference>.sig-name{color:var(--md-typeset-a-color)}.md-typeset .sig-inline a.reference.sig-name,.md-typeset .sig-inline a.reference:not(.desctype)>.n,.md-typeset .sig-inline a.reference>.sig-name,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference.sig-name,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference>.sig-name{color:var(--md-typeset-a-color)}.md-typeset .sig-inline a.reference.sig-name:hover,.md-typeset .sig-inline a.reference:not(.desctype)>.n:hover,.md-typeset .sig-inline a.reference>.sig-name:hover,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference.sig-name:hover,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n:hover,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt a.reference>.sig-name:hover{color:var(--md-accent-fg-color)}.md-typeset .sig-inline a.reference.sig-name:hover,.md-typeset .sig-inline a.reference:not(.desctype)>.n:hover,.md-typeset .sig-inline a.reference>.sig-name:hover,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference.sig-name:hover,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n:hover,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt a.reference>.sig-name:hover{color:var(--md-accent-fg-color)}.md-typeset .sig-inline a.reference.sig-name:hover,.md-typeset .sig-inline a.reference:not(.desctype)>.n:hover,.md-typeset .sig-inline a.reference>.sig-name:hover,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference.sig-name:hover,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference:not(.desctype)>.n:hover,.md-typeset :is(dl.objdesc,dl.api-field)>dt a.reference>.sig-name:hover{color:var(--md-accent-fg-color)}.md-typeset .sig-inline .desctype,.md-typeset .sig-inline .desctype>a.reference,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference{color:var(--md-code-hl-special-color)}.md-typeset .sig-inline .desctype,.md-typeset .sig-inline .desctype>a.reference,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference{color:var(--md-code-hl-special-color)}.md-typeset .sig-inline .desctype,.md-typeset .sig-inline .desctype>a.reference,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype>a.reference{color:var(--md-code-hl-special-color)}.md-typeset .sig-inline .desctype .n,.md-typeset .sig-inline .desctype>a.reference .n,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype .n,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference .n{color:inherit}.md-typeset .sig-inline .desctype .n,.md-typeset .sig-inline .desctype>a.reference .n,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype .n,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference .n{color:inherit}.md-typeset .sig-inline .desctype .n,.md-typeset .sig-inline .desctype>a.reference .n,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype .n,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype>a.reference .n{color:inherit}.md-typeset .sig-inline .desctype:-webkit-any(a.reference):hover,.md-typeset .sig-inline .desctype>a.reference:hover,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype:-webkit-any(a.reference):hover,.md-typeset :-webkit-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference:hover{color:var(--md-accent-fg-color)}.md-typeset .sig-inline .desctype:-moz-any(a.reference):hover,.md-typeset .sig-inline .desctype>a.reference:hover,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype:-moz-any(a.reference):hover,.md-typeset :-moz-any(dl.objdesc,dl.api-field)>dt .desctype>a.reference:hover{color:var(--md-accent-fg-color)}.md-typeset .sig-inline .desctype:is(a.reference):hover,.md-typeset .sig-inline .desctype>a.reference:hover,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype:is(a.reference):hover,.md-typeset :is(dl.objdesc,dl.api-field)>dt .desctype>a.reference:hover{color:var(--md-accent-fg-color)}.md-typeset dl.objdesc>dt{background:var(--md-code-bg-color);font-family:var(--md-code-font-family);padding-left:.5em;padding-right:.5em;padding-top:.5em}.md-typeset dl.objdesc>dt,.md-typeset dl.objdesc>dt code{font-size:.75rem}.md-typeset dl.objdesc>dt .property{color:var(--md-code-hl-keyword-color);font-style:normal;font-weight:700}.md-typeset dl.objdesc>dt .sig-prename{color:var(--md-code-hl-name-color);padding:0}.md-typeset dl.objdesc>dt .viewcode-back,.md-typeset dl.objdesc>dt .viewcode-link{float:right;text-align:right}.md-typeset dl.objdesc>dt.api-include-path,.md-typeset dl.objdesc>dt.api-include-path code{font-size:.65rem}.md-typeset dl.objdesc>dt:first-child{padding-top:.5em}.md-typeset dl.objdesc>dt:last-of-type{padding-bottom:.5em}.md-typeset dl.objdesc>dd dl.field-list>dt{font-size:1em;font-weight:700;margin-bottom:1em}.md-typeset dl.objdesc>dd dd.noindent{margin-left:0}.md-typeset dl.api-field>dt{display:table}.md-typeset dl.api-field>dt a.headerlink{left:.5em;margin-left:0;position:relative;width:0}.md-typeset dl.api-field>dt,.md-typeset dl.api-field>dt code{font-size:.65rem}.md-typeset dl.api-field>dt.api-parameter-kind{float:right;font-family:var(--md-text-font-family)}.md-typeset dl.api-field>dt.api-parameter-kind:before{content:"["}.md-typeset dl.api-field>dt.api-parameter-kind:after{content:"]"}.md-typeset dl.objdesc.summary>dd,.md-typeset dl.objdesc.summary>dd>p:first-child{margin-top:0}.md-typeset .sig-inline:-webkit-any(.c-texpr,.cpp-texpr){background-color:unset;font-family:unset}.md-typeset .sig-inline:-moz-any(.c-texpr,.cpp-texpr){background-color:unset;font-family:unset}.md-typeset .sig-inline:is(.c-texpr,.cpp-texpr){background-color:unset;font-family:unset}.md-nav__link{white-space:nowrap}:root>*{--objinfo-icon-fg-alias:#e65100;--objinfo-icon-fg-default:#424242;--objinfo-icon-fg-data:#1565c0;--objinfo-icon-fg-procedure:#6a1b9a;--objinfo-icon-fg-sub-data:#2e7d32;--objinfo-icon-bg-default:var(--md-default-bg-color)}@media screen{[data-md-color-scheme=slate]{--objinfo-icon-fg-alias:#ffb74d;--objinfo-icon-fg-default:#e0e0e0;--objinfo-icon-fg-data:#64b5f6;--objinfo-icon-fg-procedure:#ce93d8;--objinfo-icon-fg-sub-data:#81c784}}.objinfo-icon{background-color:var(--objinfo-icon-bg-default);border:1px solid var(--objinfo-icon-fg-default);border-radius:2px;color:var(--objinfo-icon-fg-default);display:inline-table;flex-shrink:0;font-family:var(--md-text-font-family);font-weight:500;height:16px;line-height:16px;margin-right:8px;text-align:center;vertical-align:middle;width:16px}.objinfo-icon__alias{background-color:var(--objinfo-icon-fg-alias);border:1px solid var(--objinfo-icon-fg-alias);color:var(--objinfo-icon-bg-default)}.objinfo-icon__procedure{background-color:var(--objinfo-icon-fg-procedure);border:1px solid var(--objinfo-icon-fg-procedure);color:var(--objinfo-icon-bg-default)}.objinfo-icon__data{background-color:var(--objinfo-icon-fg-data);border:1px solid var(--objinfo-icon-fg-data);color:var(--objinfo-icon-bg-default)}.objinfo-icon__sub-data{background-color:var(--objinfo-icon-fg-sub-data);border:1px solid var(--objinfo-icon-fg-sub-data);color:var(--objinfo-icon-bg-default)}.search-result-objlabel{border:1px solid var(--md-default-fg-color--light);border-radius:2px;float:right;padding:2px}table.longtable.docutils.data.align-default tbody>tr>td>p>a.reference.internal>code.xref.py.py-obj.docutils.literal.notranslate>span.pre{word-break:normal} +@media screen{[data-md-color-scheme=slate]{--md-hue:232;--md-default-fg-color:hsla(var(--md-hue),75%,95%,1);--md-default-fg-color--light:hsla(var(--md-hue),75%,90%,0.62);--md-default-fg-color--lighter:hsla(var(--md-hue),75%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),75%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,21%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,21%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,21%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,21%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,1);--md-code-bg-color:hsla(var(--md-hue),15%,15%,1);--md-code-hl-color:rgba(66,135,255,.15);--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(66,135,255,.3);--md-typeset-kbd-color:hsla(var(--md-hue),15%,94%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,94%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-table-color:hsla(var(--md-hue),75%,95%,0.12);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,12%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,10%,1);--md-shadow-z1:0 0.2rem 0.5rem rgba(0,0,0,.2),0 0 0.05rem rgba(0,0,0,.1);--md-shadow-z2:0 0.2rem 0.5rem rgba(0,0,0,.3),0 0 0.05rem rgba(0,0,0,.25);--md-shadow-z3:0 0.2rem 0.5rem rgba(0,0,0,.4),0 0 0.05rem rgba(0,0,0,.35)}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=slate] img[src$="#only-dark"]{display:initial}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#bd78c9}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a682e3}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#6c91d5}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff9575}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c7846b}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#6c91d5}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:rgba(255,25,71,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:rgba(245,0,86,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:rgba(223,65,251,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:rgba(124,77,255,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:rgba(66,135,255,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:rgba(0,145,235,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:rgba(0,186,214,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:rgba(0,189,164,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:rgba(0,199,83,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:rgba(99,222,23,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:rgba(176,235,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:rgba(255,213,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:rgba(255,170,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:rgba(255,145,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:rgba(255,110,66,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00}[data-md-color-primary=white]{--md-primary-fg-color:#fff;--md-primary-fg-color--light:hsla(0,0%,100%,.7);--md-primary-fg-color--dark:rgba(0,0,0,.07);--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54);--md-typeset-a-color:#4051b5}[data-md-color-primary=white] .md-hero--expand{border-bottom:.05rem solid rgba(0,0,0,.07)}@media screen and (max-width:76.1875em){[data-md-color-primary=white] .md-hero{border-bottom:.05rem solid rgba(0,0,0,.07)}}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:rgba(0,0,0,.07)}[data-md-color-primary=white] .md-search__form:hover{background-color:rgba(0,0,0,.32)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:rgba(0,0,0,.87)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid rgba(0,0,0,.07)}}[data-md-color-primary=black]{--md-primary-fg-color:#000;--md-primary-fg-color--light:rgba(0,0,0,.54);--md-primary-fg-color--dark:#000;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-header,[data-md-color-primary=black] .md-hero{background-color:#000}@media screen and (max-width:59.9375em){[data-md-color-primary=black] .md-nav__source{background-color:rgba(0,0,0,.87)}}@media screen and (min-width:60em){[data-md-color-primary=black] .md-search__form{background-color:hsla(0,0%,100%,.12)}[data-md-color-primary=black] .md-search__form:hover{background-color:hsla(0,0%,100%,.3)}}@media screen and (max-width:76.1875em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:#000}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:#000}} +@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/2aadfad5aee7ceeaf4eb0924efabe5b4.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/8e48cf20cf9f9e5feb7197c79028132b.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/638764dc2513deb09c55fc025f6dd36c.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/8007dfe835cfb201b8caaa9651098588.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/8c3798e37724f71bc0c63c44a5307413.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/ca7eea0cf248d6e8442c01074765bd33.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/10b31f4cad9ea78d43449886bfbb88ac.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 100;src: url(fonts/0f303f31706d39866cced9dcc17b61fb.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/6de03a64aa8100032abc6e836b3ed803.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/0ec3cc19652785204ea2e322330f0f1b.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/b57a5ada789f195d5d42f4073a6cf313.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/4f17f22fc6bff4f3333ccf7ed7126e6d.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/daf12b5f1889502004bba85ad71f9fa4.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/7b63598dcc2a26583b82594bd0e36d5b.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/64a6b4e954cf84685cbf8de77eb47344.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 100;src: url(fonts/b19ac4e57f2a56639eebd1c35319e5a7.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/8c49ed8b472d38d3985ec9bbbccea601.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/435e4b7f9f250d9d9243d4754799fc96.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/20dc200cc43ab904876fb0c1697ebe39.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/455c2c1af0a2bf20047a1864d7d7c174.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/51f3f41805329fb8341beb56ded833ea.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/b076e86301cbee8c5c9aef51863a9c0a.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 300;src: url(fonts/f75911313e1c7802c23345ab57e754d8.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/28e6b81b1bc1964707edd4179e4268f5.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/e704ef18719c08839bc99a32437ef0f8.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/76945c7494c20515bb45d1dedab8f706.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/b5b4146d87e5d22d0a4e0d04f3ee5626.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/21953b998bab09c1f60c599caee56378.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/e33716333704ab19fdf9989e072ad49a.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/f53f3b5a15d717b6d21d7885285e90ed.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 300;src: url(fonts/bb8007225d94a099cddbade7ea904667.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/3254c528e2ab56454a9f22191035c5fe.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/b7ef2cd1159a8cbfd271ff2abe07f237.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/495d38d4b9741e8aa4204002414069e2.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/d368cf5bed7856dbafa2af36b51acb9c.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/daf51ab540602b2d0b87646621637bac.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/77b24796a3d4ab521f66765651875338.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/3c23eb02de6b34e30f18cfb7167abd81.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 400;src: url(fonts/f6734f8177112c0839b961f96d813fcb.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/0e326670106c8eb6a11a8c30734ecfc8.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/a6caf7b9888eb0c382948c1ca5e8bebb.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/a6933e678530b263486fa7b185a449ca.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/144860ed1e48e186f08997e6388a9c3f.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/2ea7a97b7c976b121112a088eb398561.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/0b68e8634c96265eb32a0c769416b5b0.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/9582ced8a675bf267cc7ac392a86413e.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 400;src: url(fonts/db0424fb67fb52e7e538490240cc7fb9.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/2096d27efc16cbdd79183bf295c8ebde.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/3728fbdd191d75bad5b83a838dfe2fc1.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/713780d8b30bda5583052ea847cdcb4f.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/7f1c829b0c90fd664a03bb714a74f7d3.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 500;src: url(fonts/b019538234514166ec7665359d097403.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/38f3ee1f96b758f95672c632d8759594.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/4c815fdc869f885520f7c8eae6730edf.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/6deb20301c65a96db17c433ad0cf8158.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/63111d307c01b52ffccf7b0319cb7917.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/e56cc9fb5272752b78f144b4be43175d.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/2e10480d4154762bc7c8fbb40877e104.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/7af61b2367eba2b1852e837c46a75696.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 500;src: url(fonts/661d4b208656c006e7aab58acf778485.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/ac848474638236e67a64bc654fb18de0.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/3c505383d37d2078648e37868bbd1fad.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/6a84eeee6a25e7c9a8a03191007a6720.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/5b6377da4c959db6d4b22738a27f1bee.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/1c9cc76fd52238330f0aabac35acd2ca.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/4ec57f2a80b91090971b83970230ca09.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/fc66f942651a9fe1a598770d3d896529.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 700;src: url(fonts/f5aebdfea35d1e7656ef4acc5db1f243.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/8aa562790559d61dd5178a88a296d70f.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/ccdebed88064e470c15f37c432922e57.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/c8a9fd4eab4e83382cc66fde70911b41.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/5d7ff31ac7bf945e8d61878f8a941239.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/2a8c422bef4a7099e99dbf0e61ed5e49.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/bdbb6b52604c2451fdcba9cdfd44f4e1.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/acaac043ca238f0e56e61864456777fa.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 700;src: url(fonts/6be97ca17228a69c406231d89c003194.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/84e959dd07f302392f0ffd86f87db888.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/f265cee675c0e5b2d6ab263d0edcc754.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/9fdb12ceee3a402d3a54afe354552459.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/76da333ab59c6d625cabfb0768f82b4a.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/2550c2e2d8495c3ed2d4d52f824374f1.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/3a38c967413f7bce36d3baefc321aade.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/2781e9e7c3f369b8fc7965e679b17b60.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: normal;font-weight: 900;src: url(fonts/7e262106f82cc52663e403f5b73795bb.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/2f5c32f094829c0278bce28fe2bbe074.ttf) format('truetype');}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/0d1b73eee266eabb2cff35dfa4ce25a3.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/da6cd48e6dad1888fccc91735e7522f7.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/50aacf068f685be0dd903a91d5bab7d8.woff2) format('woff2');unicode-range: U+1F00-1FFF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/1383417807f7965daaf94e7c497dcddb.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/3f1918538864f9681d47a4538d48289c.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/bfd1a0c9c783e84595589f33e1828a57.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto';font-style: italic;font-weight: 900;src: url(fonts/3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/c6dc61b627bbc5af9130518297bd4f17.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/78a9265759e7b861a1639a36f4c01d04.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/2f7c3c315334a99574ee4ceb21af654d.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/870e5928dd14fcfe0ce9386107666774.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/36e39c6463ae1c71c71e69c05e593e1b.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/cbfd26d5bcf084ee407a0b2b7599e84b.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 100;src: url(fonts/cce2217cc8323fe49789adefb3596291.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/7fa86b886bee5d6ab420a8e89b9f3052.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/4f93c2808e3b69e525c118074e5de31f.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/99be4d68845d66c27c7f7d3a48687b66.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/029e176ad602329b4434892101db9cf3.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/9095d663e4d450059bcc2260bb75cd62.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/1181a8e619707033241139715eca64c6.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 200;src: url(fonts/22aadc77cafa07b2db9ed560d0320616.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/6ad3f6bbe6220cc476a0d3c731d3fb04.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/5dc0e4b14e903ba7f45c581df7402b3f.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/63f4b74ebf127dbeb033126ea988f54e.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/f5f971e9640a9eb86ef553a7e7e999c7.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/0053ba6958e79f26751eabb555bd73d0.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/4207cbc8cb7bc2cbd0bcce565298cbbc.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 300;src: url(fonts/90ebb29b5cffa197b184773983ba7e91.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/6ac1ee292434fac2313c42b0dfb7897c.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/aa28d99c7db60ad23f96a5c317615c42.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/52f28cb4d065b4adfa78df4f9559c639.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/122802d03aed4bf8cd6a03997a97aca4.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/4039566f251699c4b421ed1a38a59b24.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/93b6c99d936df38895a0d95e3ffea2fd.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 400;src: url(fonts/32c8a74ac0816253d69a7cc68a60986d.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/1512b579343c6b61c7523cdd838d8328.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/d6f9cdf1a40893111566fcdee3bbe5a9.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/25c52b9af13f0d1b10719f5289e8c803.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/b4e42731e8d667ae87c3450c345754ae.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/214adfc289a2f2af8b0008c59ed0c7f2.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/f55dac651a40fce74a5cf5728d9f8ffc.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 500;src: url(fonts/0e1f73c6737cdf273efb4b79504e4c0a.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/bf2ad3287f13eb7076cccb516ec2986f.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/aab05142e0e2dadf7df633e061e612ad.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/7b8c2179b6b778308d2ff39bdb82e926.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/77ff81100e5a1db3d925f713660700ad.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/101522bafe9c61c68698ecc784607772.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 600;src: url(fonts/c28a41f656599f6694528b5463c6a445.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/89b4f174a5a728d2d8c85b87990c9ab4.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/bcd47c2f3649cfcaa86a08fb741255d6.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/770518db51bed1e082feecc532cfcbf8.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/85a41b80c5fdc14e3dc48636a30d87dd.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/fb17f56622e45dd4ecee00bb5c63cd2b.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/cd3d1f17e048e2116f438bd7157baccf.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: normal;font-weight: 700;src: url(fonts/6f8d857c5a8545e67de6b60aa0fe5c33.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/302b0425bf5ea66f37a822a61d723adc.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/c22066c14662d6c80415ae04c5dd9d51.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/de018865c95896bb57265fc97c48ebd7.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/3177dacffeac1eb4102852811ae4a2c7.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/5989ef3a21d7f252337ab3326f78bde7.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/dd719f1662079ce6a61260f9af972379.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 100;src: url(fonts/07ff82964967feebb9c96288e0e0df05.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/765bd4a97597a4d7781193793477a6cd.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/bd0efe13f0d9d591b337ddc7f289f494.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/ab03beb9091fa15ce4e783199e076bc6.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/a70ff2592da5e3453943f727633aff54.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/0a0ad0eae50e549ecd713b9ad417f1a1.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/d07f561ba87d93460742b060727d9e0d.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 200;src: url(fonts/43358c04243de546caddd0898dbf0757.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/71e06579279fba7436d58a1c49288909.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/fb1aaa90783b8cb9375265abeb91b153.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/2325b97b584755067ea4f7f56ee05430.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/f534242dea2255c25b9d05c2371986e3.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/9c9be791a58af8a04c611ca1d13f51c6.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/f2f69e8cd15fdd15a4244c95ec8a8514.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 300;src: url(fonts/555ceea3a65ffbbecf8b7e6d04966c7f.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/d98f35e926c11f3d5c0c8e3205d43907.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/b93199bb6f964f190f4da04ecdbaf5a4.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/99cf36e763be9cce7b4c59b91841af58.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/1488146d8b2e9859d6c90e6c2b48f7ef.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/255cf41e0317d95e3992683a76ef28a8.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/f154d62b4879af7a22895af7a4ef03f0.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 400;src: url(fonts/dc25cbf4baaf778bd8ae78fbc0e79479.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/bc67bba106323289ea3eda0826de1912.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/33c5d27ca0eaeb12ebe728ae2fc7106d.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/c13b34dd5b6a35b309944b61c91b2ace.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/392ff374142585f7b886ee1fe66e686e.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/1f1481679a64a39f3427547aa1b13f0f.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/f17ee050ada0453f3bd07bc466c2dde2.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 500;src: url(fonts/6725a7e91680edd1cdc9ed5c26ac05fd.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/cadfb311297a9362b07fab73934b432a.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/e99627cd27de169d23ece4573006af2a.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/bd51fb0ca67e64c809ffcf7e1370f969.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/437939342255944b82a49f916404c5fc.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/83614c36460a4a9734968789cb535de7.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/d9e6a498dac7e9e91f6e0b4f8930eba0.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 600;src: url(fonts/b4d3c40a77fd9e35a881a79077957055.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/d422317033deb87342a5e56c7be67458.ttf) format('truetype');}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/2c0f74be498d2da814c0a84dd6833f70.woff2) format('woff2');unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/9bcbc88b33b2efc2aee821b831499f1c.woff2) format('woff2');unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/8898c4b754d5d96c1a5e1b1d54100554.woff2) format('woff2');unicode-range: U+0370-03FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2) format('woff2');unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/60eb682678bbea5e8ad71f66f2f65536.woff2) format('woff2');unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face {font-family: 'Roboto Mono';font-style: italic;font-weight: 700;src: url(fonts/5ce47d5195e59af38114d0b70217baf2.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;} +:root{--si-icon--material_alert-circle:url('data:image/svg+xml;charset=utf-8,');--si-icon--material_delete:url('data:image/svg+xml;charset=utf-8,');}.md-typeset .si-icon-inline.material_alert-circle::before{-webkit-mask-image:var(--si-icon--material_alert-circle);mask-image:var(--si-icon--material_alert-circle);}.md-typeset .si-icon-inline.material_delete::before{-webkit-mask-image:var(--si-icon--material_delete);mask-image:var(--si-icon--material_delete);}.md-typeset .admonition.versionadded{border-color:rgb(72,138,87);}.md-typeset .versionadded>.admonition-title{background-color:rgba(72,138,87,0.1);border-color:rgb(72,138,87);}.md-typeset .versionadded>.admonition-title::before{background-color:rgb(72,138,87);-webkit-mask-image:var(--si-icon--material_alert-circle);mask-image:var(--si-icon--material_alert-circle);}.md-typeset .admonition.versionchanged{border-color:rgb(238,144,64);}.md-typeset .versionchanged>.admonition-title{background-color:rgba(238,144,64,0.1);border-color:rgb(238,144,64);}.md-typeset .versionchanged>.admonition-title::before{background-color:rgb(238,144,64);-webkit-mask-image:var(--si-icon--material_alert-circle);mask-image:var(--si-icon--material_alert-circle);}.md-typeset .admonition.deprecated{border-color:rgb(203,70,83);}.md-typeset .deprecated>.admonition-title{background-color:rgba(203,70,83,0.1);border-color:rgb(203,70,83);}.md-typeset .deprecated>.admonition-title::before{background-color:rgb(203,70,83);-webkit-mask-image:var(--si-icon--material_delete);mask-image:var(--si-icon--material_delete);} diff --git a/_static/white.svg b/_static/white.svg new file mode 100644 index 0000000..d80d149 --- /dev/null +++ b/_static/white.svg @@ -0,0 +1,54 @@ + + \ No newline at end of file diff --git a/api.html b/api.html new file mode 100644 index 0000000..3d2d68f --- /dev/null +++ b/api.html @@ -0,0 +1,3005 @@ + + + + + + + + + + + + + + + + RDFM Server API Reference - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Server API Reference

+

API Authentication

+

By default, the RDFM server expects all API requests to be authenticated. +Depending on the type of the API, this can be either:

+
    +
  • Device Token

  • +
  • Management Token

  • +
+

In either case, the server expects the token to be passed as part of the request, in the HTTP Authorization header. +An example authenticated request is shown below:

+
GET /api/v1/groups HTTP/1.1
+Host: rdfm-server:5000
+User-Agent: python-requests/2.31.0
+Accept-Encoding: gzip, deflate
+Accept: */*
+Connection: keep-alive
+Authorization: Bearer token=eyJhbGciOiJSUzI1NiIsInR5cC<...truncated...>RpPonb7-IAsk89YpGayxg
+
+
+

Any request that was not successfully authenticated (because of a missing or otherwise invalid token) will return the 401 Unauthorized status code. +Additionally, in the case of management tokens, if the given token does not provide sufficient access to the requested resource, the request will be rejected with a 403 Forbidden status code. +This can happen if the token does not claim all scopes required by the target endpoint (for example: trying to upload a package using a read-only token).

+

Error Handling

+

Should an error occur during the handling of an API request, either because of incorrect request data or other endpoint-specific scenarios, the server will return an error structure containing a user-friendly description of the error. +An example error response is shown below:

+
{
+   "error": "delete failed, the package is assigned to at least one group"
+}
+
+
+

Packages API

+
+
+GET /api/v1/packages
+

Fetch a list of packages uploaded to the server

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • id (integer) – package identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • sha256 (string) – sha256 of the uploaded package

  • +
  • driver (string) – storage driver used to store the package

  • +
  • metadata (dict[str, str]) – package metadata (key/value pairs)

  • +
+
+
+

Example Request

+
GET /api/v1/packages HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+    {
+        "created": "Thu, 17 Aug 2023 10:41:08 GMT",
+        "id": 1,
+        "metadata": {
+            "rdfm.hardware.devtype": "dummydevice",
+            "rdfm.software.version": "v10",
+            "rdfm.storage.local.length": 4194304,
+            "rdfm.storage.local.uuid": "6f7483ac-5cde-467f-acf7-39e4b397e313"
+        },
+        "driver": "local",
+        "sha256": "4e415854e6d0cf9855b2290c02638e8651537989b8862ff9c9cb91b8d956ea06"
+    }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v1/packages
+

Upload an update package.

+

Uploads an update package to the server. +Remaining key/value pairs in the form request are used as +metadata for the artifact.

+

If required, an additional storage directory can be specified that +indicates the directory within server-side storage that the package +is placed in.

+
+
Form Parameters:
+
    +
  • file – binary contents of the package

  • +
  • rdfm.software.version – required: software version of the package

  • +
  • rdfm.hardware.devtype – required: compatible device type

  • +
  • rdfm.storage.directory – optional: storage directory specific to the current storage driver

  • +
  • ... – remaining package metadata

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error, package was uploaded

  • +
  • 400 Bad Request – provided metadata contains keys reserved by RDFM +or a file was not provided

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to upload packages

  • +
+
+
+

Example Request

+
POST /api/v1/packages HTTP/1.1
+Accept: */*
+Content-Length: 4194738
+Content-Type: multipart/form-data; boundary=------------------------0f8f9642db3a513e
+
+--------------------------0f8f9642db3a513e
+Content-Disposition: form-data; name="rdfm.software.version"
+
+v10
+--------------------------0f8f9642db3a513e
+Content-Disposition: form-data; name="rdfm.hardware.devtype"
+
+dummydevice
+--------------------------0f8f9642db3a513e
+Content-Disposition: form-data; name="file"; filename="file.img"
+Content-Type: application/octet-stream
+
+<file contents>
+--------------------------0f8f9642db3a513e--
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with the appropriate package write scope. Available scopes are: rdfm_upload_single_file - single file package, rdfm_upload_rootfs_image - rootfs image package.``rdfm_admin_rw`` - all package write scopes.

+
+
+ +
+
+DELETE /api/v1/packages/(int: identifier)
+

Delete the specified package

+

Deletes the specified package from the server and from the +underlying storage. +The package can only be deleted if it’s not assigned to any +group.

+
+
Parameters:
+
    +
  • identifier – package identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to delete packages

  • +
  • 404 Not Found – specified package does not exist

  • +
  • 409 Conflict – package is assigned to a group and cannot +be deleted

  • +
+
+
+

Example Request

+
DELETE /api/v1/packages/1 HTTP/1.1
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v1/packages/(int: identifier)
+

Fetch information about a single package given by the specified ID

+
+
Parameters:
+
    +
  • identifier – package identifier

  • +
+
+
Status Codes:
+
+
+
Response JSON Object:
+
    +
  • id (integer) – package identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • sha256 (string) – sha256 of the uploaded package

  • +
  • driver (string) – storage driver used to store the package

  • +
  • metadata (dict[str, str]) – package metadata (simple key/value pairs)

  • +
+
+
+

Example Request

+
GET /api/v1/packages/1 HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "created": "Thu, 17 Aug 2023 10:41:08 GMT",
+    "id": 1,
+    "metadata": {
+        "rdfm.hardware.devtype": "dummydevice",
+        "rdfm.software.version": "v10",
+        "rdfm.storage.local.length": 4194304,
+        "rdfm.storage.local.uuid": "6f7483ac-5cde-467f-acf7-39e4b397e313"
+    },
+    "driver": "local",
+    "sha256": "4e415854e6d0cf9855b2290c02638e8651537989b8862ff9c9cb91b8d956ea06"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+GET /local_storage/(path: name)
+

Endpoint for exposing local package storage.

+

WARNING: Local storage should not be used in production deployment, +only for local testing! +This will be disabled in the future for non-prod configurations.

+
+
Parameters:
+
    +
  • name – identifier (UUID) of the package object in local storage

  • +
+
+
Status Codes:
+
+
+
+
+

Note

+

This is a public API route; no authorization is required to access it.

+
+
+ +

Group API

+
+
+POST /api/v2/groups
+

Create a new group

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to create groups

  • +
  • 404 Not Found – group does not exist

  • +
+
+
Request JSON Object:
+
    +
  • metadata (dict[str, str]) – device metadata

  • +
  • priority (optional[int]) – priority of the group, lower value takes +precedence

  • +
+
+
+

Example request

+
POST /api/v2/groups/1 HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    priority: 1,
+    "metadata":
+    {
+        "description": "A test group",
+    }
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "created": "Mon, 14 Aug 2023 11:50:40 GMT",
+    "devices": [],
+    "id": 2,
+    "packages": [],
+    "priority": 1,
+    "metadata": {
+        "description": "A test group",
+    },
+    "policy": "no_update,"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v2/groups
+

Fetch all groups

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • id (integer) – group identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • packages (array[integer]) – currently assigned package identifiers

  • +
  • devices (array[integer]) – currently assigned device identifiers

  • +
  • metadata (dict[str, str]) – group metadata

  • +
  • policy (str) – group update policy

  • +
  • priority (integer) – group priority

  • +
+
+
+

Example Request

+
GET /api/v2/groups HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+    {
+        "created": "Mon, 14 Aug 2023 11:00:56 GMT",
+        "devices": [],
+        "id": 1,
+        "packages": [],
+        "metadata": {},
+        "policy": "no_update,",
+        "priority": 25
+    }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+DELETE /api/v2/groups/(int: identifier)
+

Delete a group

+

The group being deleted must NOT be assigned to any devices.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to delete groups

  • +
  • 404 Not Found – group does not exist

  • +
  • 409 Conflict – at least one device is still assigned to the group

  • +
+
+
+

Example Request

+
DELETE /api/v2/groups/1 HTTP/1.1
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v2/groups/(int: identifier)
+

Fetch information about a group

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
+
+
Response JSON Object:
+
    +
  • id (integer) – group identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • packages (array[integer]) – currently assigned package identifiers

  • +
  • devices (array[integer]) – currently assigned device identifiers

  • +
  • metadata (dict[str, str]) – group metadata

  • +
  • policy (str) – group update policy

  • +
  • priority (integer) – group priority

  • +
+
+
+

Example Request

+
GET /api/v2/groups/1 HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "created": "Mon, 14 Aug 2023 11:00:56 GMT",
+    "devices": [],
+    "id": 1,
+    "packages": [],
+    "metadata": {},
+    "policy": "no_update,",
+    "priority": 25
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+PATCH /api/v2/groups/(int: identifier)/devices
+

Modify the list of devices assigned to a group

+

This endpoint allows modifying the list of devices assigned to the group, +as described by two arrays containing device identifiers of devices that +will be added/removed from the group.

+

This operation is atomic - if at any point an invalid device identifier is +encountered, the entire operation is aborted. This covers:

+
+
    +
  • Any device identifier which does not match a registered device

  • +
  • Any device identifier in additions which already has an assigned +group which has the same priority as the group specified by +identifier +(even if the group is the same as specified by identifier)

  • +
  • Any device identifier in removals which is not currently assigned +to the specified group

  • +
+
+

Additions are evaluated first, followed by the removals.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to delete groups

  • +
  • 404 Not Found – group does not exist

  • +
  • 409 Conflict – one of the conflict situations described above has occurred

  • +
+
+
Request JSON Object:
+
    +
  • add (array[string]) – identifiers of devices that should be assigned +to the group

  • +
  • remove (array[string]) – identifiers of devices that should be removed +from the group

  • +
+
+
+

Example request

+
PATCH /api/v2/groups/1/devices HTTP/1.1
+Accept: application/json, text/javascript
+
+{
+    "add": [
+        1,
+        2,
+        5,
+    ]
+    "remove": [
+        3,
+    ]
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v2/groups/(int: identifier)/package
+

Assign a package to a specific group

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 400 Bad Request – invalid request schema

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to assign packages

  • +
  • 404 Not Found – the specified package or group does not exist

  • +
  • 409 Conflict – the package/group was modified or deleted during the operation

  • +
+
+
Request JSON Object:
+
    +
  • packages (array[integer]) – identifiers of the packages to assign, or +empty array

  • +
+
+
+

Example request

+
POST /api/v2/groups/1/package HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "packages": [1],
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v2/groups/(int: identifier)/policy
+

Change the update policy of the group

+

The update policy defines the target versions that each device within +the group should be receiving. +For information about group policies, consult the OTA manual.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 400 Bad Request – invalid request schema, or an invalid policy schema +was requested

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to modify groups

  • +
  • 404 Not Found – the specified group does not exist

  • +
+
+
Request JSON Object:
+
    +
  • policy (string) – new group policy string to set

  • +
+
+
+

Example Request

+
POST /api/v2/groups/1/policy HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "policy": "exact_match,v1",
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v2/groups/(int: identifier)/priority
+

Change the priority of the group

+

The priority controls which group will be applied to a device which is +assigned to multiple groups.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to modify groups

  • +
  • 404 Not Found – the specified group does not exist

  • +
  • 409 Conflict – at least one device which is assigned to this group is also

  • +
+
+
+

assigned to another group with the requested priority

+
+
Request JSON Object:
+
    +
  • priority (int) – new group priority to set

  • +
+
+
+

Example Request

+
POST /api/v2/groups/1/priority HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "priority": 1,
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +

Group API (legacy)

+
+
+POST /api/v1/groups
+

Create a new group

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to create groups

  • +
  • 404 Not Found – group does not exist

  • +
+
+
Request JSON Object:
+
    +
  • key (any) – metadata value

  • +
+
+
+

Example request

+
POST /api/v1/groups/1 HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "description": "A test group",
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "created": "Mon, 14 Aug 2023 11:50:40 GMT",
+    "devices": [],
+    "id": 2,
+    "packages": [],
+    "metadata": {
+        "description": "A test group",
+    },
+    "policy": "no_update,"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v1/groups
+

Fetch all groups

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • id (integer) – group identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • packages (array[integer]) – currently assigned package identifiers

  • +
  • devices (array[integer]) – currently assigned device identifiers

  • +
  • metadata (dict[str, str]) – group metadata

  • +
  • policy (str) – group update policy

  • +
+
+
+

Example Request

+
GET /api/v1/groups HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+    {
+        "created": "Mon, 14 Aug 2023 11:00:56 GMT",
+        "devices": [],
+        "id": 1,
+        "packages": [],
+        "metadata": {},
+        "policy": "no_update,"
+    }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+DELETE /api/v1/groups/(int: identifier)
+

Delete a group

+

The group being deleted must NOT be assigned to any devices.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to delete groups

  • +
  • 404 Not Found – group does not exist

  • +
  • 409 Conflict – at least one device is still assigned to the group

  • +
+
+
+

Example Request

+
DELETE /api/v1/groups/1 HTTP/1.1
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v1/groups/(int: identifier)
+

Fetch information about a group

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
+
+
Response JSON Object:
+
    +
  • id (integer) – group identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • packages (array[integer]) – currently assigned package identifiers

  • +
  • devices (array[integer]) – currently assigned device identifiers

  • +
  • metadata (dict[str, str]) – group metadata

  • +
  • policy (str) – group update policy

  • +
+
+
+

Example Request

+
GET /api/v1/groups/1 HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "created": "Mon, 14 Aug 2023 11:00:56 GMT",
+    "devices": [],
+    "id": 1,
+    "packages": [],
+    "metadata": {},
+    "policy": "no_update,"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+PATCH /api/v1/groups/(int: identifier)/devices
+

Modify the list of devices assigned to a group

+

This endpoint allows modifying the list of devices assigned to the group, +as described by two arrays containing device identifiers of devices that +will be added/removed from the group.

+

This operation is atomic - if at any point an invalid device identifier is +encountered, the entire operation is aborted. This covers:

+
+
    +
  • Any device identifier which does not match a registered device

  • +
  • Any device identifier in additions which already has an assigned +group +(even if the group is the same as specified by identifier)

  • +
  • Any device identifier in removals which is not currently assigned +to the specified group

  • +
+
+

Additions are evaluated first, followed by the removals.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to delete groups

  • +
  • 404 Not Found – group does not exist

  • +
  • 409 Conflict – one of the conflict situations described above has occurred

  • +
+
+
Request JSON Object:
+
    +
  • add (array[string]) – identifiers of devices that should be assigned +to the group

  • +
  • remove (array[string]) – identifiers of devices that should be removed +from the group

  • +
+
+
+

Example request

+
PATCH /api/v1/groups/1/devices HTTP/1.1
+Accept: application/json, text/javascript
+
+{
+    "add": [
+        1,
+        2,
+        5,
+    ]
+    "remove": [
+        3,
+    ]
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v1/groups/(int: identifier)/package
+

Assign a package to a specific group

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 400 Bad Request – invalid request schema

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to assign packages

  • +
  • 404 Not Found – the specified package or group does not exist

  • +
  • 409 Conflict – the package/group was modified or deleted during the operation

  • +
+
+
Request JSON Object:
+
    +
  • packages (array[integer]) – identifiers of the packages to assign, or +empty array

  • +
+
+
+

Example request

+
POST /api/v1/groups/1/package HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "packages": [1],
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v1/groups/(int: identifier)/policy
+

Change the update policy of the group

+

The update policy defines the target versions that each device within +the group should be receiving. +For information about group policies, consult the OTA manual.

+
+
Parameters:
+
    +
  • identifier – group identifier

  • +
+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 400 Bad Request – invalid request schema, or an invalid policy schema +was requested

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to modify groups

  • +
  • 404 Not Found – the specified group does not exist

  • +
+
+
Request JSON Object:
+
    +
  • policy (string) – new group policy string to set

  • +
+
+
+

Example Request

+
POST /api/v1/groups/1/policy HTTP/1.1
+Content-Type: application/json
+Accept: application/json, text/javascript
+
+{
+    "policy": "exact_match,v1",
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ +

Update API

+
+
+POST /api/v1/update/check
+

Check for available updates

+

Device clients must call this endpoint with their associated metadata. +At minimum, the rdfm.software.version, rdfm.hardware.devtype and +rdfm.hardware.macaddr pairs must be present. Based on this metadata, +the device’s currently assigned groups (if any) and package, an +update package is picked from the available ones. If more than one group is +assigned, the group with the lowest priority value takes precedence.

+
+
Status Codes:
+
    +
  • 200 OK – an update is available

  • +
  • 204 No Content – no updates are available

  • +
  • 400 Bad Request – device metadata is missing device type, software version, +and/or MAC address

  • +
  • 401 Unauthorized – device did not provide authorization data, +or the authorization has expired

  • +
+
+
Request JSON Array of Objects:
+
    +
  • rdfm.software.version (string) – required: running software version

  • +
  • rdfm.hardware.devtype (string) – required: device type

  • +
  • rdfm.hardware.macaddr (string) – required: MAC address (used as ID)

  • +
  • ... (string) – other device metadata

  • +
+
+
Response JSON Object:
+
    +
  • id (integer) – package identifier

  • +
  • created (string) – UTC creation date (RFC822)

  • +
  • sha256 (string) – sha256 of the uploaded package

  • +
  • uri (string) – generated URI for downloading the package

  • +
+
+
+

Example Request

+
POST /api/v1/update/check HTTP/1.1
+Accept: application/json, text/javascript
+Content-Type: application/json
+
+{
+    "rdfm.software.version": "v0.0.1",
+    "rdfm.hardware.macaddr": "00:11:22:33:44:55",
+    "rdfm.hardware.devtype": "example"
+}
+
+
+

Example Responses

+
HTTP/1.1 204 No Content
+
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "created": "Mon, 14 Aug 2023 13:03:27 GMT",
+  "id": 1,
+  "sha256": "4e415854e6d0cf9855b2290c02638e8651537989b8862ff9c9cb91b8d956ea06",
+  "uri": "http://127.0.0.1:5000/local_storage/12a83ff3-2de2-4a95-8f3f-c7a884e426e5"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a device token.

+
+
+ +

Device Management API

+
+
+GET /api/v2/devices
+

Fetch a list of devices registered on the server

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • id (integer) – device identifier

  • +
  • last_access (string) – UTC datetime of last access to the server (RFC822)

  • +
  • name (string) – device-reported user friendly name

  • +
  • mac_addr (string) – device-reported MAC address

  • +
  • groups (optional[array[integer]]) – group identifiers of assigned groups

  • +
  • metadata (dict[str, str]) – device metadata (key/value pairs)

  • +
  • capabilities (dict[str, bool]) – device RDFM client capabilities

  • +
+
+
+

Example Request

+
GET /api/v1/devices HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "capabilities": {
+      "exec_cmds": false,
+      "file_transfer": true,
+      "shell_connect": true
+    },
+    "groups": [1],
+    "id": 1,
+    "last_access": null,
+    "mac_address": "loopback",
+    "metadata": {},
+    "name": "dummy_device"
+  }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v2/devices/(int: identifier)
+

Fetch information about a single device given by the identifier

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 404 Not Found – device with the specified identifier does not exist

  • +
+
+
Response JSON Object:
+
    +
  • id (integer) – device identifier

  • +
  • last_access (string) – UTC datetime of last access to the server (RFC822)

  • +
  • name (string) – device-reported user friendly name

  • +
  • mac_addr (string) – device-reported MAC address

  • +
  • groups (optional[array[integer]]) – group identifiers of assigned groups

  • +
  • metadata (dict[str, str]) – device metadata (key/value pairs)

  • +
  • capabilities (dict[str, bool]) – device RDFM client capabilities

  • +
+
+
+

Example Request

+
GET /api/v2/devices/1 HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "capabilities": {
+    "exec_cmds": false,
+    "file_transfer": true,
+    "shell_connect": true
+  },
+  "groups": [1],
+  "id": 1,
+  "last_access": null,
+  "mac_address": "loopback",
+  "metadata": {},
+  "name": "dummy_device"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +

Device Management API (legacy)

+
+
+GET /api/v1/devices
+

Fetch a list of devices registered on the server

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • id (integer) – device identifier

  • +
  • last_access (string) – UTC datetime of last access to the server (RFC822)

  • +
  • name (string) – device-reported user friendly name

  • +
  • mac_addr (string) – device-reported MAC address

  • +
  • group (optional[integer]) – group identifier of assigned group

  • +
  • metadata (dict[str, str]) – device metadata (key/value pairs)

  • +
  • capabilities (dict[str, bool]) – device RDFM client capabilities

  • +
+
+
+

Example Request

+
GET /api/v1/devices HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "capabilities": {
+      "exec_cmds": false,
+      "file_transfer": true,
+      "shell_connect": true
+    },
+    "group": 1,
+    "id": 1,
+    "last_access": null,
+    "mac_address": "loopback",
+    "metadata": {},
+    "name": "dummy_device"
+  }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+GET /api/v1/devices/(int: identifier)
+

Fetch information about a single device given by the identifier

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 404 Not Found – device with the specified identifier does not exist

  • +
+
+
Response JSON Object:
+
    +
  • id (integer) – device identifier

  • +
  • last_access (string) – UTC datetime of last access to the server (RFC822)

  • +
  • name (string) – device-reported user friendly name

  • +
  • mac_addr (string) – device-reported MAC address

  • +
  • group (optional[integer]) – group identifier of assigned group

  • +
  • metadata (dict[str, str]) – device metadata (key/value pairs)

  • +
  • capabilities (dict[str, bool]) – device RDFM client capabilities

  • +
+
+
+

Example Request

+
GET /api/v1/devices/1 HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "capabilities": {
+    "exec_cmds": false,
+    "file_transfer": true,
+    "shell_connect": true
+  },
+  "group": 1,
+  "id": 1,
+  "last_access": null,
+  "mac_address": "loopback",
+  "metadata": {},
+  "name": "dummy_device"
+}
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +

Device Authorization API

+
+
+POST /api/v1/auth/device
+

Device authorization endpoint

+

All device clients must first authorize with the RDFM server via this endpoint.

+
+
Status Codes:
+
+
+
Request JSON Object:
+
    +
  • metadata (dict[str, str]) – device metadata

  • +
  • public_key (str) – the device’s RSA public key, in PEM format, with +newline characters escaped

  • +
  • timestamp (int) – POSIX timestamp at the time of making the request

  • +
+
+
+

Example Request

+
POST /api/v1/auth/device HTTP/1.1
+Accept: application/json, text/javascript
+Content-Type: application/json
+X-RDFM-Device-Signature: FGACvvZ4CFC0np9Z8QNeuF8jnaE7y8v532FNtwMjkWKyT6sHj0hTIgggxfgaC1mOmY/9xmnwv2aQLgUxbzCJs0yf1/PyxG3Gyf8Mt47+aXbT4/Mj8j++8EB2QxbB9TKwZiCGa+lkevXsZwOrD6l4WNWUeQFA/jgWzTLoYxsIdz0=
+
+{
+    "metadata": {
+        "rdfm.software.version": "v0",
+        "rdfm.hardware.devtype": "dummy",
+        "rdfm.hardware.macaddr": "00:00:00:00:00:00"
+    },
+    "public_key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVqdgCAfyXUqLfOpHYwHFv4OQL\n2p3LwHm5ag9XMY2ylvqU2r9eGNWkdXTtEnL81S6u+4CDFNmbUuimoeDMazqSKYED\n3FtOU4+FrqaHf7T3oMkng5mNHcAqbyq6WAXs/HrXfvj7lR38qLJXgslgR3Js3M0k\nB91oGfFwUa7I67BZYwIDAQAB\n-----END PUBLIC KEY-----",
+    "timestamp": 1694414456
+}
+
+
+

Example Response

+

Unauthorized device:

+
HTTP/1.1 401 Unauthorized
+Content-Type: application/json
+
+
+

Authorized device:

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+    "expires": 300,
+    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXZpY2VfaWQiOiIwMDowMDowMDowMDowMDowMCIsImNyZWF0ZWQiOjE2OTQ0MTQ0NTYsImV4cGlyZXMiOjMwMH0.cG37RTA1niB8NhokqI0ryvDKZj_0eRpWWEeqawu4IYE"
+}
+
+
+
+

Note

+

This is a public API route; no authorization is required to access it.

+
+
+ +
+
+GET /api/v1/auth/pending
+

Fetch all pending registrations

+

This endpoint returns device registrations requests that have not been accepted +by an administrator yet.

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
+
+
Response JSON Array of Objects:
+
    +
  • metadata (dict[str, str]) – device metadata

  • +
  • public_key (str) – the device’s RSA public key, in PEM format, with +newline characters escaped

  • +
  • mac_address (str) – the device’s MAC address

  • +
  • last_appeared (str) – datetime (RFC822) of the last registration +request made by the device

  • +
+
+
+

Example Request

+
GET /api/v1/auth/registrations HTTP/1.1
+Accept: application/json, text/javascript
+
+
+

Example Response

+
HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "last_appeared": "Wed, 13 Sep 2023 10:40:49 GMT",
+    "mac_address": "00:00:00:00:00:00",
+    "metadata": {
+      "rdfm.hardware.devtype": "dummy",
+      "rdfm.hardware.macaddr": "00:00:00:00:00:00",
+      "rdfm.software.version": "v0"
+    },
+    "public_key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdBgmI/FGkb17Bcxr99lEF1Nof\njwQaPcipnBWW+S3N6c937rGkINH0vkHMjcS3HRF2ku6/Knjj4uXrZtbwUbPoP4bP\nbK+HrYVw9Di6hTHr042W7FxIzU3howCF68QQnUMG/5XmqwdsucH1gMRv8cuU21Vz\nQazvf08UWZCUeQjw5QIDAQAB\n-----END PUBLIC KEY-----"
+  }
+]
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-only scope rdfm_admin_ro or administrative scope rdfm_admin_rw.

+
+
+ +
+
+POST /api/v1/auth/register
+

Accept registration request

+

Accepts an incoming device registration request. As a result, the device +will be allowed access to the RDFM server on next registration attempt.

+
+
Status Codes:
+
    +
  • 200 OK – no error

  • +
  • 401 Unauthorized – user did not provide authorization data, +or the authorization has expired

  • +
  • 403 Forbidden – user was authorized, but did not have permission +to change device registration status

  • +
  • 404 Not Found – the specified registration request does not exist

  • +
+
+
Request JSON Object:
+
    +
  • public_key (str) – RSA public key used in the registration request

  • +
  • mac_address (str) – MAC address used in the registration request

  • +
+
+
+

Example Request

+
POST /api/v1/auth/registrations HTTP/1.1
+Accept: application/json, text/javascript
+Content-Type: application/json
+
+{
+    "mac_address": "00:00:00:00:00:00",
+    "public_key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdBgmI/FGkb17Bcxr99lEF1Nof\njwQaPcipnBWW+S3N6c937rGkINH0vkHMjcS3HRF2ku6/Knjj4uXrZtbwUbPoP4bP\nbK+HrYVw9Di6hTHr042W7FxIzU3howCF68QQnUMG/5XmqwdsucH1gMRv8cuU21Vz\nQazvf08UWZCUeQjw5QIDAQAB\n-----END PUBLIC KEY-----"
+}
+
+
+

Example Response

+
HTTP/1.1 200 OK
+
+
+
+

Warning

+

Accessing this endpoint requires providing a management token with read-write scope rdfm_admin_rw.

+
+
+ + + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..e0e01f6 --- /dev/null +++ b/genindex.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + None - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +None + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/http-routingtable.html b/http-routingtable.html new file mode 100644 index 0000000..a3646bd --- /dev/null +++ b/http-routingtable.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + None - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +None + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..182917d --- /dev/null +++ b/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Documentation

+
+ +
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/introduction.html b/introduction.html new file mode 100644 index 0000000..c63d46a --- /dev/null +++ b/introduction.html @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + Introduction - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

Introduction

+

RDFM - Remote Device Fleet Manager - is an open-source ecosystem of tools that enable Over-The-Air (OTA) update delivery and fleet management for systems of embedded devices.

+

This manual describes the main components of RDFM. It is divided into the following chapters:

+
    +
  • System Architecture - a short overview of the system architecture, and how each component of the system interacts with the other

  • +
  • RDFM Linux Device Client - build instructions and manual for the Linux RDFM Client, used for installing updates on a device

  • +
  • RDFM Android Device Client - integration guide and user manual for the RDFM Android Client/app used for providing OTA updates via RDFM on embedded Android devices

  • +
  • RDFM MCUmgr Device Client - build instructions and manual for the RDFM MCUmgr Client app, used for providing updates via RDFM on embedded devices running ZephyrRTOS

  • +
  • RDFM Artifact utility - instruction manual for the rdfm-artifact utility used for generating update packages for use with the RDFM Linux device client

  • +
  • RDFM Manager utility - instruction manual for the rdfm-mgmt utility, which allows management of devices connected to the RDFM server

  • +
  • RDFM Management Server - build instructions and deployment manual for the RDFM Management Server

  • +
  • RDFM Server API Reference - comprehensive reference of the HTTP APIs exposed by the RDFM Management Server

  • +
  • RDFM OTA Manual - introduces key concepts of the RDFM OTA system and explains it’s basic operation principles

  • +
  • RDFM Frontend - build instructions for the RDFM Frontend application

  • +
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..e1cfb88 Binary files /dev/null and b/objects.inv differ diff --git a/rdfm-docs.pdf b/rdfm-docs.pdf new file mode 100644 index 0000000..39a4168 Binary files /dev/null and b/rdfm-docs.pdf differ diff --git a/rdfm_android_device_client.html b/rdfm_android_device_client.html new file mode 100644 index 0000000..772cb5d --- /dev/null +++ b/rdfm_android_device_client.html @@ -0,0 +1,830 @@ + + + + + + + + + + + + + + + + RDFM Android Device Client - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Android Device Client

+

Introduction

+

The RDFM Android Device Client allows for integrating an Android-based device with the RDFM server. +Currently, only OTA update functionality is implemented.

+

Integrating the app

+

This app is not meant to be built separately (i.e in Android Studio), but as part of the source tree for an existing device. +The app integrates with the Android UpdateEngine to perform the actual update installation, which requires it to be a system app. +Some configuration is required to the existing system sources.

+

Copying the sources

+

First, copy the sources of the app to the root directory of the AOSP source tree. +After cloning this repository, run the following:

+
mkdir -v -p <path-to-aosp-tree>/vendor/antmicro/rdfm
+cd devices/android-client/
+cp -r app/src/main/* <path-to-aosp-tree>/vendor/antmicro/rdfm
+
+
+

Configuring the device Makefile

+

The product Makefile must be configured to build the RDFM app into the system image. +To do this, add rdfm to the PRODUCT_PACKAGES variable in the target device Makefile:

+
PRODUCT_PACKAGES += rdfm
+
+
+

Building the app

+

Afterwards, the usual Android build procedure can be used to build just the app. +From an already configured build environment, run:

+
mma rdfm
+
+
+

The resulting signed APK is in out/target/product/<product-name>/system/app/rdfm/rdfm.apk.

+

Using HTTPS for server requests

+

The default Android system CA certificates are used when validating the certificate presented by the server. +If the RDFM server that is configured in the app uses a certificate that is signed by a custom Certificate Authority, the CA certificate must be added to the system roots.

+

System versioning

+

The app performs update check requests to the configured RDFM server. +The build version and device type are retrieved from the system properties:

+
    +
  • ro.build.version.incremental - the current software version (matches rdfm.software.version)

  • +
  • ro.build.product - device type (matches rdfm.hardware.devtype)

  • +
+

When uploading an OTA package to the RDFM server, currently these values must be manually extracted from the update package, and passed as arguments to rdfm-mgmt:

+
rdfm-mgmt packages upload --path ota.zip --version <ro.build.version.incremental> --device <ro.build.product>
+
+
+

You can extract the values from the package metadata file by unzipping the OTA package.

+

Configuring the app

+

The application will automatically start on system boot. +Available configuration options are shown below.

+

Build-time app configuration

+

The default build-time configuration can be modified by providing a custom conf.xml file in the app/src/main/res/values/ folder, similar to the one shown below:

+
<?xml version="1.0" encoding="utf-8"?>
+<resources>
+<!--
+    This is an example overlay configuration file for the RDFM app.
+    To modify the default server address, you can do:
+
+        <string name="default_rdfm_server_address">http://rdfm.example.local:6000/</string>
+
+    Likewise, overlaying the default update check interval can be done similarly:
+
+        <string name="default_update_check_interval_seconds">240</string>
+
+    NOTE: These settings are only used during the app's first startup. To change them afterwards,
+    you must delete the existing configuration file.
+-->
+</resources>
+
+
+

This build-time configuration is applied only once, at first startup of the app, as the main use case for this is first-time configuration for newly provisioned devices. +Modifying it afterwards (for example, via an update containing a new version of the RDFM app) will not result in the change of existing configuration.

+

Runtime app configuration

+

It is possible to change the app’s configuration at runtime by simply starting the RDFM app from the drawer and selecting Settings from the context menu.

+

Configuration options

+

The following configuration options are available:

+
    +
  • RDFM server URL (http/https scheme)

  • +
  • Update check interval (in seconds)

  • +
  • Maximum amount of concurrent shell sessions (set to 0 to disable reverse shell functionality)

  • +
+

Available intents

+

Update check intent

+

This intent allows an external app to force perform an update check outside of the usual automatic update check interval. +To do this, the app that wants to perform the update check must have the com.antmicro.update.rdfm.permission.UPDATE_CHECK permission defined in its AndroidManifest.xml file:

+
<uses-permission android:name="com.antmicro.update.rdfm.permission.UPDATE_CHECK" />
+
+
+

Afterwards, an update check can then be forced like so:

+
Intent configIntent = new Intent("com.antmicro.update.rdfm.startUpdate");
+mContext.sendBroadcast(configIntent);
+
+
+

External configuration via intents

+

The app settings can also be configured via intents, for example in order to change between different deployment environments. +To do this, the app that performs the configuring step must have the com.antmicro.update.rdfm.permission.CONFIGURATION permission defined in its AndroidManifest.xml file:

+
<uses-permission android:name="com.antmicro.update.rdfm.permission.CONFIGURATION" />
+
+
+

To configure the app, use the com.antmicro.update.rdfm.configurationSet intent and set extra values on the intent to the settings you wish to change. +For example, to set the server address:

+
Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationSet");
+configIntent.putExtra("ota_server_address", "http://CUSTOM-OTA-ADDRESS/");
+mContext.sendBroadcast(configIntent);
+
+
+

The supported configuration key names can be found in the res/values/strings.xml file with the preference_ prefix.

+

Aside from setting the configuration, you can also fetch the current configuration of the app:

+
Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationGet");
+mContext.sendBroadcast(configIntent);
+
+// Now listen for `com.antmicro.update.rdfm.configurationResponse` broadcast intent
+// The intent's extras bundle will contain the configuration keys and values
+
+
+

Development

+

The provided Gradle files can be used for development purposes, simply open the devices/android-client directory in Android Studio. +Missing references to the UpdateEngine class are expected, but they do not prevent regular use of the IDE.

+

Do note however that the app is not buildable from Android Studio, as it requires integration with the aforementioned system API. +To test the app, an existing system source tree must be used. +Copy the modified sources to the AOSP tree, and re-run the application build. +The modified APK can then be uploaded to the device via ADB by running:

+
adb install <path-to-rdfm.apk>
+
+
+

Restarting the app

+

With the target device connected via ADB, run:

+
adb shell am force-stop com.antmicro.update.rdfm
+adb shell am start -n com.antmicro.update.rdfm/.MainActivity
+
+
+

Fetching app logs

+

To view the application logs, run:

+
adb logcat --pid=`adb shell pidof -s com.antmicro.update.rdfm`
+
+
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_artifact.html b/rdfm_artifact.html new file mode 100644 index 0000000..6e2dc3c --- /dev/null +++ b/rdfm_artifact.html @@ -0,0 +1,767 @@ + + + + + + + + + + + + + + + + RDFM Artifact utility - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Artifact utility

+

Introduction

+

The RDFM Artifact tool (rdfm-artifact) allows for easy creation and modification of RDFM Linux client-compatible artifacts containing rootfs partition images. +A basic RDFM artifact consists of a rootfs image, as well as its checksum, metadata and compatibility with certain device types.

+

Additionally, rdfm-artifact allows for the generation of delta updates, which contain only the differences between two versions of an artifact rather than the entire artifact itself. +This can be useful for reducing the size of updates and improving the efficiency of the deployment process.

+

rdfm-artifact can also be used for generation of Zephyr MCUboot artifacts, which allows for updating embedded devices running Zephyr. +Additionally, multiple Zephyr images can be combined into one grouped artifact to allow multiple boards to act as one logical device.

+

Single file updates are also supported. +This option allows for creating, or updating specific files on the device, without the need to update the whole partition.

+

Getting started

+

In order to support robust updates and rollback, the RDFM Client requires proper partition layout and a bootloader that supports A/B update scheme. To make it easy to integrate the RDFM Client into your Yocto image-building project, it’s recommended to use the meta-rdfm Yocto layer when building the BSPs.

+

Building from source

+

Requirements

+
    +
  • Go compiler

  • +
  • C Compiler

  • +
  • liblzma-dev and libglib2.0-dev packages

  • +
+

Steps

+

To build rdfm-artifact on a device from source, clone the repository and build the binary using make:

+
git clone https://github.com/antmicro/rdfm.git && cd tools/rdfm-artifact/
+make
+
+
+

Basic usage

+

The basic functionality of writing an artifact is available with the write subcommand:

+
NAME:
+   rdfm-artifact write - Allows creation of RDFM-compatible artifacts
+
+USAGE:
+   rdfm-artifact write command [command options] [arguments...]
+
+COMMANDS:
+   rootfs-image        Create a full rootfs image artifact
+   delta-rootfs-image  Create a delta rootfs artifact
+   zephyr-image        Create a full Zephyr MCUboot image artifact
+   zephyr-group-image  Create a Zephyr MCUboot group image artifact
+   single-file         Create a single file artifact
+
+OPTIONS:
+   --help, -h  show help
+
+
+

Creating a full-rootfs artifact

+

For example, to create a simple rootfs artifact for a given system image:

+
rdfm-artifact write rootfs-image \
+	--file "my-rootfs-image.img" \
+	--artifact-name "my-artifact-name" \
+	--device-type "my-device-type" \
+	--output-path "path-to-output.rdfm"
+
+
+

Creating a delta rootfs artifact

+

For creating a delta artifact, you should have already created two separate full-rootfs artifacts:

+
    +
  • base artifact - the rootfs image that the deltas will be applied on top of, or in other words: the currently running rootfs on the device

  • +
  • target artifact - the updated rootfs image that will be installed on the device

  • +
+

Given these two artifacts, a delta artifact can be generated like this:

+
rdfm-artifact write delta-rootfs-image \
+    --base-artifact "base.rdfm" \
+    --target-artifact "target.rdfm" \
+    --output-path "base-to-target.rdfm"
+
+
+

Creating a Zephyr MCUboot artifact

+

To create a Zephyr MCUboot artifact, you’ll have to have already created a Zephyr image with MCUboot support enabled. +You should use the signed bin image (by default zephyr.signed.bin). +Artifact version will be extracted from provided image.

+

With this image, you can generate an artifact like so:

+
rdfm-artifact write zephyr-image \
+   --file "my-zephyr-image.signed.bin" \
+   --artifact-name "my-artifact-name" \
+   --device-type "my-device-type" \
+   --output-path "path-to-output.rdfm"
+
+
+

Creating a Zephyr MCUboot group artifact

+

To create a grouped Zephyr MCUboot artifact, you should have already created at least two Zephyr images with MCUboot support enabled. +The version of individual images in a grouped artifact must be identical.

+

Given images one.bin and two.bin for group targets one and two respectively, an artifact can be generated with:

+
rdfm-artifact write zephyr-group-image \
+	--group-type "my-group" \
+	--target "one:one.bin" \
+	--target "two:two.bin" \
+	--ouptput-path "path-to-output.rdfm"
+
+
+
+

Note

+

It’s possible to create a grouped artifact with just one image, +however in cases like that you should create simple zephyr-image instead.

+
+

Creating a single file artifact

+

Apart from updating a whole partition, it’s also possible to update a single file on the device. +The usage is the same as for rootfs artifacts, but with the single-file subcommand and two new options:

+
    +
  • --dest-dir - the destination directory on the device where the file should be placed

  • +
  • --rollback-support - (optional) determines, whether a backup of the file should be created for rollback purposes. +The backup file is stored in the same directory as the original file, with the .tmp extension added to the name. +By default, the rollback support is disabled.

  • +
+
rdfm-artifact write single-file \
+	--file "my-file.txt" \
+	--artifact-name "my-artifact-name" \
+	--device-type "my-device-type" \
+	--output-path "path-to-output.rdfm" \
+	--dest-dir "/destination/device/directory" \
+	--rollback-support
+
+
+

Running tests

+

To run rdfm-artifact tests, use the test Makefile target:

+
make test
+
+
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_frontend.html b/rdfm_frontend.html new file mode 100644 index 0000000..c4bd151 --- /dev/null +++ b/rdfm_frontend.html @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + RDFM Frontend - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Frontend

+

Introduction

+

Repository contains code for a frontend application that is able to render data from and communicate with rdfm-server through HTTP requests.

+

The application uses HTTP Polling to dynamically detect any changes in the data and update the UI accordingly, so multiple users can use the application simultaneously (as well as the rdfm-mgmt tool).

+

To use the frontend application, make sure that rdfm-server is up and running. +Details on how to run it can be found in RDFM Management Server. +To be able to send requests to rdfm-server its URL has to be defined in the .env file using VITE_SERVER_URL key.

+
+

Warning

+

If no authentication is used in the frontend application make sure that the RDFM_DISABLE_ENCRYPTION and RDFM_DISABLE_API_AUTH values are set to 1.

+
+

Before running any of the commands, make sure that you have npm installed.

+

Building the application

+

To install dependencies and build the application for production run the following commands in the root directory of the project:

+
npm install
+npm run build
+
+
+

The built static files are located in the dist directory. +The frontend can be started alongside the RDFM API in the same Docker image. +The following changes must be applied:

+
    +
  • VITE_RDFM_BACKEND in the .env file to 'true'.

  • +
  • VITE_SERVER_URL in the .env file to the URL of the backend server.

  • +
  • RDFM_INCLUDE_FRONTEND_ENDPOINT in the docker-compose configuration. As a consequence, the frontend application will be served on /api/static/frontend endpoint once the HTTP server is started.

  • +
+

The frontend may also be deployed independently of the RDFM API. +The following configuration settings must then be set:

+
    +
  • VITE_RDFM_BACKEND in the .env file to 'false'.

  • +
  • VITE_SERVER_URL in the .env file to the URL of the backend server.

  • +
  • RDFM_ENABLE_CORS in the docker-compose configuration to 1 to enable CORS requests.

  • +
  • RDFM_FRONTEND_APP_URL in the docker-compose configuration to the URL of the frontend application server, as it is used for redirects.

  • +
+
+

Warning

+

RDFM_ENABLE_CORS variable should not be set in production environment, as it allows for cross-origin requests.

+
+

Running development server

+

When developing the application it is recommended to use the vite development server, as features like Hot Module Replacement is enabled. +To install dependencies and start the development server run the following commands in the root directory of the project:

+
npm install
+npm run dev
+
+
+

To communicate with rdfm-server when using the development server, make sure to set all variables as described in the Building section in the same as it is done for a separate server deployment.

+

Configuration

+

The frontend application can be configured using an .env file. +That file contains variables that can be set to change the behavior of the application. +Below there is a description of all available variables.

+
    +
  • VITE_SERVER_URL - RDFM server URL

  • +
  • VITE_RDFM_BACKEND - Indicates if the backend hosts the frontend application

  • +
  • VITE_LOGIN_URL - OIDC login URL

  • +
  • VITE_LOGOUT_URL - OIDC logout URL

  • +
  • VITE_OAUTH2_CLIENT - OAUTH2 Client ID

  • +
+

Formatting

+

To format the code using prettier run the following command:

+
npm install
+npm run format
+
+
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_linux_device_client.html b/rdfm_linux_device_client.html new file mode 100644 index 0000000..6f890fa --- /dev/null +++ b/rdfm_linux_device_client.html @@ -0,0 +1,923 @@ + + + + + + + + + + + + + + + + RDFM Linux Device Client - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Linux Device Client

+

Introduction

+

The RDFM Linux Device Client (rdfm-client) integrates an embedded Linux device with the RDFM Server. +This allows for performing robust Over-The-Air (OTA) updates of the running system and remote management of the device.

+

rdfm-client runs on the target Linux device and handles the process of checking for updates in the background along with maintaining a connection to the RDFM Management Server.

+

Getting started

+

In order to support robust updates and rollback, the RDFM Client requires proper partition layout and integration with the U-Boot bootloader. To make it easy to integrate the RDFM Client into your Yocto image-building project, it’s recommended to use the meta-rdfm Yocto layer when building the BSPs.

+

Installing from source

+

Requirements

+
    +
  • C compiler

  • +
  • Go compiler

  • +
  • liblzma-dev, libssl-dev and libglib2.0-dev packages

  • +
+

Steps

+

To install an RDFM client on a device from source, first clone the repository and build the binary:

+
git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/
+make
+
+
+

Then run the install command:

+
make install
+
+
+

Installation notes

+

Installing rdfm this way does not offer a complete system updater. +System updates require additional integration with the platform’s bootloader and a dual-root partition setup for robust updates. +For this, it’s recommended to build complete BSPs containing rdfm using the meta-rdfm Yocto layer.

+

Building using Docker

+

All build dependencies for compiling the RDFM Client are included in a dedicated Dockerfile. To build a development container image, you can use:

+
git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/
+sudo docker build -t rdfmbuilder .
+
+
+

This will create a Docker image that can be later used to compile the RDFM binary:

+
sudo docker run --rm -v <rdfm-dir>:/data -it rdfmbuilder
+cd data/devices/linux-client
+make
+
+
+

Configuring the client

+

RDFM default config

+

The main config file contents are located in /etc/rdfm/rdfm.conf. It’s JSON formatted and with the following keys of interest:

+

RootfsPartA string

+

Partition A for the A/B updating scheme.

+

RootfsPartB string

+

Partition B for the A/B updating scheme.

+

RDFM overlay config

+

The file /var/lib/rdfm/rdfm.conf defines the high-level RDFM client configurations. They are overlaid over the configuration located in /etc/rdfm/rdfm.conf during client startup.

+

DeviceTypeFile string

+

Path to the device type file.

+

UpdatePollIntervalSeconds int

+

Poll interval for checking for new updates.

+

RetryPollIntervalSeconds int

+

Maximum number of seconds between each retry when authorizing.

+

ServerCertificate string

+

Path to a server SSL certificate.

+

ServerURL string

+

Management server URL.

+

HttpCacheEnabled bool

+

Describing if artifact caching is enabled. True by default.

+

ReconnectRetryCount int

+

HTTP reconnect retry count.

+

ReconnectRetryTime int

+

HTTP reconnect retry time.

+

TelemetryEnable bool

+

Describing if telemetry is enabled. False by default.

+

TelemetryBatchSize int

+

Number of log entries to be sent to a management server at a time. Fifty by default.

+

RDFM telemetry config

+

The JSON structured loggers.conf file, laying under /etc/rdfm/, serves as a configuration file that defines a set of loggers to be executed once the client establishes a connection to the RDFM management server. Each logger can be any executable binary, which will be invoked by the client at predefined intervals. The client captures and processes the output generated by these loggers, providing a flexible mechanism for collecting and reporting system or application data during runtime.

+

The loggers.json file contains an array of dictionaries, each of which describes a logger.

+

Consider the following example:

+
[
+    {
+    "name": "current date",
+    "path": "date",
+    "args": ["--rfc-email"],
+    "tick": 1000
+    }
+]
+
+
+
+

Note

+

Since the file gives the capacity to run arbitrary binaries, its permissions should be set to -rw-r--r--.

+
+

name string

+

Denotes the name of the logger, each one should have a unique name. Loggers lower in the file will overwrite their counterparts that are above them.

+

path string

+

A path to an executable to be ran.

+

args []string

+

A list of arguments for the given executable.

+

tick int

+

Number of milliseconds between each time a logger is ran. In the case of a logger taking more than tick to execute, it is killed and the client reports a timeout error.

+

Testing server-device integration with a demo Linux device client

+

For development purposes, it’s often necessary to test server integration with an existing device client. +To do this, it is possible to use the RDFM Linux device client, without having to build a compatible system image utilizing the Yocto meta-rdfm layer. +First, build the demo container image:

+
cd devices/linux-client/
+make docker-demo-client
+
+
+

You can then start a demo Linux client by running the following:

+
docker-compose -f docker-compose.demo.yml up
+
+
+

If required, the following environment variables can be changed in the above docker-compose.demo.yml file:

+
    +
  • RDFM_CLIENT_SERVER_URL - URL to the RDFM Management Server, defaults to http://127.0.0.1:5000/.

  • +
  • RDFM_CLIENT_SERVER_CERT (optional) - path (within the container) to the CA certificate to use for verification of the connection to the RDFM server. When this variable is set, the server URL must also be updated to use HTTPS instead of HTTP.

  • +
  • RDFM_CLIENT_DEVTYPE - device type that will be advertised to the RDFM server; used for determining package compatibility, defaults to x86_64.

  • +
  • RDFM_CLIENT_PART_A, RDFM_CLIENT_PART_B (optional) - specifies path (within the container) to the rootfs A/B partitions that updates will be installed to. They do not need to be specified for basic integration testing; any updates that are installed will go to /dev/zero by default.

  • +
+

The demo client will automatically connect to the specified RDFM server and fetch any available packages. +To manage the device and update deployment, you can use the RDFM Manager utility.

+

Developer Guide

+

Running tests

+

Use the test make target to run the unit tests:

+
make test
+
+
+

Additionally, run the scripts available in the scripts/test-docker directory. These scripts test basic functionality of the RDFM client.

+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_manager.html b/rdfm_manager.html new file mode 100644 index 0000000..622c917 --- /dev/null +++ b/rdfm_manager.html @@ -0,0 +1,804 @@ + + + + + + + + + + + + + + + + RDFM Manager utility - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Manager utility

+

Introduction

+

The RDFM Manager (rdfm-mgmt) utility allows authorized users to manage resources exposed by the RDFM Management Server.

+

Installation

+

Before proceeding, make sure that you have installed Python (at least version 3.11) and the pipx utility:

+
    +
  • Debian (Bookworm) - run sudo apt update && sudo apt install pipx

  • +
  • Arch - sudo pacman -S python-pipx

  • +
+

The prefered mode of installation for rdfm-mgmt is via pipx. +To install rdfm-mgmt, you must first clone the RDFM repository:

+
git clone https://github.com/antmicro/rdfm.git
+cd rdfm/
+
+
+

Afterwards, run the following commands:

+
cd manager/
+pipx install .
+
+
+

This will install the rdfm-mgmt utility and its dependencies for the current user within a virtual environment located at /home/<user>/.local/pipx/venv. +The rdfm-mgmt executable will be placed in /home/<user>/.local/bin/ and should be immediately accessible from the shell. +Depending on the current system configuration, adding the above directory to the PATH may be required.

+

Configuration

+

Additional RDFM Manager configuration is stored in the current user’s $HOME directory, in the $HOME/.config/rdfm-mgmt/config.json file. +By default, RDFM Manager will add authentication data to all requests made to the RDFM server, which requires configuration of an authorization server and client credentials for use with the OAuth2 Client Credentials flow. +If authentication was disabled on the server-side, you can disable it in the manager as well by passing the --no-api-auth CLI flag like so:

+
rdfm-mgmt --no-api-auth groups list
+
+
+

An example configuration file is shown below. +In this case, the Keycloak authorization server was used:

+
{
+        "auth_url": "http://keycloak:8080/realms/master/protocol/openid-connect/token",
+        "client_id": "rdfm-client",
+        "client_secret": "RDSwDyUMOT7UXxMqMmq2Y4vQ1ezxqobi"
+}
+
+
+

Explanation of each required configuration field is shown below:

+
    +
  • auth_url - URL to the authorization server’s token endpoint

  • +
  • client_id - Client ID to use for authentication using OAuth2 Client Credentials flow

  • +
  • client_secret - Client secret to use for authentication using OAuth2 Client Credentials flow

  • +
+
+

Note

+

If you’re also setting up the server, please note that the above client credentials are NOT the same as the server’s Token Introspection credentials. +Each user of rdfm-mgmt should receive different credentials and be assigned scopes based on their allowed access level.

+
+

Building the wheel

+

For installation instructions, see the Installation section. +Building the wheel is not required in this case.

+

To build the rdfm-mgmt wheel, you must have Python 3 installed, along with the Poetry dependency manager.

+

Building the wheel can be done as follows:

+
cd manager/
+poetry build
+
+
+

Usage

+

For more detailed information, see the help messages associated with each subcommand:

+
$ rdfm-mgmt -h
+usage: rdfm-mgmt
+
+RDFM Manager utility
+
+options:
+  -h, --help            show this help message and exit
+  --url URL             URL to the RDFM Management Server (default: http://127.0.0.1:5000/)
+  --cert CERT           path to the server CA certificate used for establishing an HTTPS connection (default: ./certs/CA.crt)
+  --no-api-auth         disable OAuth2 authentication for API requests (default: False)
+
+available commands:
+  {devices,packages,groups}
+    devices             device management
+    packages            package management
+    groups              group management
+
+
+

Listing available resources

+

Listing devices:

+
rdfm-mgmt devices list
+
+
+

Listing registration requests:

+
rdfm-mgmt devices pending
+
+
+

Listing packages:

+
rdfm-mgmt packages list
+
+
+

Listing groups:

+
rdfm-mgmt groups list
+
+
+

Uploading packages

+
rdfm-mgmt packages upload \
+    --path file.img \
+    --version "v0" \
+    --device "x86_64"
+
+
+

Deleting packages

+
rdfm-mgmt packages delete --package-id <package>
+
+
+

Creating groups

+
rdfm-mgmt groups create --name "Group #1" --description "A very long description of the group"
+
+
+

Deleting groups

+
rdfm-mgmt groups delete --group-id <group>
+
+
+

Assign package to a group

+

Assigning one package:

+
rdfm-mgmt groups assign-package --group-id <group> --package-id <package>
+
+
+

Assigning many packages:

+
rdfm-mgmt groups assign-package --group-id <group> --package-id <package1> --package-id <package2>
+
+
+

Clearing package assignments:

+
rdfm-mgmt groups assign-package --group-id <group>
+
+
+

Assign devices to a group

+

Adding devices:

+
rdfm-mgmt groups modify-devices --group-id <group> --add <device>
+
+
+

Removing devices:

+
rdfm-mgmt groups modify-devices --group-id <group> --remove <device>
+
+
+

Setting a group’s target version

+
rdfm-mgmt groups target-version --group-id <group> --version <version-identifier>
+
+
+

Authorizing a device

+
rdfm-mgmt devices auth <mac-address>
+
+
+

You can then select the registration for this device to authorize.

+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_mcumgr_device_client.html b/rdfm_mcumgr_device_client.html new file mode 100644 index 0000000..70837d8 --- /dev/null +++ b/rdfm_mcumgr_device_client.html @@ -0,0 +1,1201 @@ + + + + + + + + + + + + + + + + RDFM MCUmgr Device Client - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM MCUmgr Device Client

+

Introduction

+

The RDFM MCUmgr Device Client (rdfm-mcumgr-client) allows for integrating an embedded device running ZephyrRTOS with the RDFM server via its MCUmgr SMP server implementation. +Currently, only the update functionality is implemented with support for serial, UDP and BLE transports.

+

rdfm-mcumgr-client runs on a proxy device that’s connected to the targets via one of the supported transports that handles the process of checking for updates, fetching update artifacts and pushing update images down to correct targets.

+

Getting started

+

In order to properly function, both the Zephyr application and the rdfm-mcumgr-client have to be correctly configured in order for the update functionality to work. +Specifically:

+
    +
  • Zephyr applications must be built with MCUmgr support, with any transport method of your choice and with image management and reboot command groups enabled.

  • +
  • The device running Zephyr must be connected to a proxy device running rdfm-mcumgr-client as the updates are coming from it.

  • +
  • For reliable updates, the SMP server must be running alongside your application and be accessible at all times.

  • +
+

Building client from source

+

Requirements

+
    +
  • C compiler

  • +
  • Go compiler (1.22+)

  • +
  • liblzma-dev and libssl-dev packages

  • +
+

Steps

+

To install the proxy client from source, first clone the repository and build the binary:

+
git clone https://github.com/antmicro/rdfm.git
+cd rdfm/devices/mcumgr-client/
+make
+
+
+

Then run the install command:

+
make install
+
+
+

Setting up target device

+

Setting up the bootloader

+

To allow rollbacks and update verification, the MCUboot bootloader is used. +Images uploaded by rdfm-mcumgr-client are written to a secondary flash partition, while leaving the primary (currently running) image intact. +During update, the images are swapped by the bootloader. +If the update was successful, the new image is permanently set as the primary one, otherwise the images are swapped back to restore the previous version. +For more details on MCUboot, you can read the official guide from MCUboot’s website.

+

Generating image signing key

+

In order to enable updates, MCUboot requires all images to be signed. +During update, the bootloader will first validate the image using this key.

+

MCUboot provides imgtool.py image tool script which can be used to generate appropriate signing key. +Below are the steps needed to generate a new key using this tool:

+

Install additional packages required by the tool (replace ~/zephyrproject with path to your Zephyr workspace):

+
cd ~/zephyrproject/bootloader/mcuboot
+pip3 install --user -r ./scripts/requirements.txt
+
+
+

Generate new key:

+
cd ~/zephyrproject/bootloader/mcuboot/scripts
+./imgtool.py keygen -k <filename.pem> -t <key-type>
+
+
+

MCUboot currently supports rsa-2048, rsa-3072, ecdsa-p256 or ed25519 key types. +For more details on the image tool, please refer to its official documentation.

+

Building the bootloader

+

Besides the signing key, MCUboot also requires that the target board has specific flash partitions defined in its devicetree. +These partitions are:

+
    +
  • boot_partition: for MCUboot itself

  • +
  • slot0_partition: the priamry slot of image 0

  • +
  • slot1_partition: the secondary slot of image 0

  • +
+

If you choose the swap-using-scratch update algorithm, one more partition has to be defined:

+
    +
  • scratch_partition: the scratch slot

  • +
+

You can check whether your board has those partitions predefined by looking at its devicetree file (boards/<arch>/<board>/<board>.dts). +Look for fixed-partitions compatible entry. If your default board configuration doesn’t specify those partitions (or you would like to modify them), +you can either modify the devicetree file directly or use devicetree overlays.

+

Sample overlay file for the stm32f746g_disco board:

+
#include <mem.h>
+
+/delete-node/ &quadspi;
+
+&flash0 {
+    partitions {
+        compatible = "fixed-partitions";
+        #address-cells = <1>;
+        #size-cells = <1>;
+
+        boot_partition: partition@0 {
+            label = "mcuboot";
+            reg = <0x00000000 DT_SIZE_K(64)>;
+        };
+
+        slot0_partition: partition@40000 {
+            label = "image-0";
+            reg = <0x00040000 DT_SIZE_K(256)>;
+        };
+
+        slot1_partition: partition@80000 {
+            label = "image-1";
+            reg = <0x00080000 DT_SIZE_K(256)>;
+        };
+
+        scratch_partition: partition@c0000 {
+        	label = "scratch";
+        	reg = <0x000c0000 DT_SIZE_K(256)>;
+        };
+    };
+};
+
+/ {
+    aliases {
+        /delete-property/ spi-flash0;
+    };
+
+    chosen {
+        zephyr,flash = &flash0;
+        zephyr,flash-controller = &flash;
+        zephyr,boot-partition = &boot_partition;
+        zephyr,code-partition = &slot0_partition;
+    };
+};
+
+
+
+

Note

+

If you do use devicetree overlay, make sure to add app.overlay as the last overlay file +since it’s needed to correctly store the MCUboot image in boot_partition.

+
+

Besides the devicetree, you also have to specify:

+
    +
  • BOOT_SIGNATURE_KEY_FILE: path to the previously generate signing key

  • +
  • BOOT_SIGNATURE_TYPE: signing key type:

    +
      +
    • BOOT_SIGNATURE_TYPE_RSA and BOOT_SIGNATURE_TYPE_RSA_LEN

    • +
    • BOOT_SIGNATURE_TYPE_ECDSA_P256

    • +
    • BOOT_SIGNATURE_TYPE_ED25519

    • +
    +
  • +
  • BOOT_IMAGE_UPGRADE_MODE: the update algorithm used for swapping images in primary and secondary slots:

    +
      +
    • BOOT_SWAP_USING_MOVE

    • +
    • BOOT_SWAP_USING_SCRATCH

    • +
    +
  • +
+

For example, if you wanted to build the bootloader for the stm32f746g_disco board with partitions defined in stm32_disco.overlay, +using swap-using-scratch update algorithm and using rsa-2048 key.pem signing key, +you would run (replace ~/zephyrproject with path to your Zephyr workspace):

+
    west build \
+        -d mcuboot \
+        -b stm32f746g_disco \
+        ~/zephyrproject/bootloader/mcuboot/boot/zephyr \
+        -- \
+            -DDTC_OVERLAY_FILE="stm32_disco.overlay;app.overlay" \
+            -DCONFIG_BOOT_SIGNATURE_KEYFILE='"key.pem"' \
+            -DCONFIG_BOOT_SIGNATURE_TYPE_RSA=y \
+            -DCONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN=2048 \
+            -DCONFIG_BOOT_SWAP_USING_SCRATCH=y
+
+
+

The produced image can be flashed to your device. +For more details on building and using MCUboot with Zephyr, please refer to official MCUboot guide.

+

Setting up the Zephyr application

+

Building the image

+

To allow your application to be used with MCUmgr client, you will have to enable Zephyr’s device management subsystem. +For the client to function properly, both image management +and OS management groups need to be enabled. +You will also have to enable and configure SMP transport +(either serial, BLE or udp) that you wish to use. +To learn how to do that, you can reference Zephyr’s smp_svr sample +which provides configuration for all of them.

+

You will also have set MCUBOOT_BOOTLOADER_MODE setting to match the swapping algorithm you’ve configured for the bootloader:

+ + + + + + + + + + + + + + +

MCUboot

Zephyr

BOOT_SWAP_USING_MOVE

MCUBOOT_BOOTLOADER_MODE_SWAP_WITHOUT_SCRATCH

BOOT_SWAP_USING_SCRATCH

MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH

+
+

Important

+

Bluetooth specific

+

Bluetooth transport additionally requires you to manually start SMB Bluetooth advertising. +Refer to the main.c +and bluetooth.c +from the smp_svr sample for details on that.

+
+

To build the smp_svr sample +for the stm32f746g_disco board with stm32_disco.overlay devicetree overlay, +configured to use serial transport with swap-using-scratch update algorithm, +you would run (replace ~/zephyrproject with path to your Zephyr workspace):

+
    west build \
+        -d build \
+        -b stm32f746g_disco \
+        "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \
+        -- \
+            -DDTC_OVERLAY_FILE="stm32_disco.overlay" \
+            -DEXTRA_CONF_FILE="overlay-serial.conf" \
+            -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y
+
+
+

For more information on the smp_svr sample, please refer to Zephyr’s documentation.

+

Signing the image

+

By default MCUboot will only accept images that are properly signed with the same key as the bootloader itself. +Only BIN and HEX output types can be signed. +The recommended way for managing signing keys is using MCUboot’s image tool, +which is shipped together with Zephyr’s MCUboot implementation. +When signing an image, you also have to provide an image version, that’s embedded in the signed image header. +This is also the value that will be reported by the MCUmgr client as the current running software version back to the RDFM server. +Image version is specified in major.minor.revision+build format.

+
Automatically
+

Zephyr build system can automatically sign the final image for you. +To enable this functionality, you will have to set:

+
    +
  • MCUBOOT_SIGNATURE_KEY_FILE: path to the signing key

  • +
  • MCUBOOT_IMGTOOL_SIGN_VERSION: version of the produced image +before building your application. +Here’s a modification of the build command from building the image with those settings applied:

  • +
+
    west build \
+        -d build \
+        -b stm32f746g_disco \
+        "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \
+        -- \
+            -DDTC_OVERLAY_FILE="stm32_disco.overlay" \
+            -DEXTRA_CONF_FILE="overlay-serial.conf" \
+            -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y \
+            -DCONFIG_MCUBOOT_SIGNATURE_KEY_FILE='"key.pem"' \
+            -DCONFIG_IMGTOOL_SIGN_VERSION='"1.2.3+4"'
+
+
+
Manually
+

You can also sign the produced images yourself using the image tool. +Below is a sample showing how to sign previously built image:

+
west sign -d build -t imgtool -- --key <key-file> --version <sign-version>
+
+
+

Either way, the signed images will be stored next to their unsigned counterparts. They will have signed inserted into the filename (e.g. unsigned zephyr.bin will produce zephyr.signed.bin signed image).

+

Self-confirmed updates

+

By default, MCUmgr client will try to manually confirm a new image during an update. +While this works in simple cases, you might wish to run some additional test logic that should be used to determine if an update should be finalized. +For example, you might want to reject an update in case one of the drivers failed to start or if the network stack is misconfigured. +The client supports these kinds of use cases using self-confirming images. +Rather than confirming an update by itself, +the client will instead watch the primary image slot of the device to determine if an update was marked as permanent or if it was rejected. +In that case, the final decision falls on the updated device.

+

For this feature to work correctly, you will have to modify your application to include the self-testing logic.

+
/*
+ * An example of self-test function.
+ * It will first check if this is a fresh update and run the testing logic.
+ * Based on results, it will either mark the update as permanent or reboot,
+ * causing MCUboot to revert to the previous version.
+ *
+ * This function should be called before the main application logic starts,
+ * preferably at the beginning of the `main` function.
+ */
+
+#include <zephyr/dfu/mcuboot.h>
+#include <zephyr/sys/reboot.h>
+
+void run_self_tests() {
+    if (!boot_is_img_confirmed()) {
+        bool passed;
+
+        /* Testing logic goes here */
+
+        if (!passed) {
+            sys_reboot(SYS_REBOOT_COLD); // (1)
+            return;
+        }
+
+        boot_write_img_confirmed(); // (2)
+    }
+}
+
+
    +
  1. Tests failed - device reboots itself, returning to previous version

  2. +
  3. Tests passed - device confirms the update, marking it as permanent

  4. +
+
+

Configuring MCUmgr client

+

Search locations

+

The client is configured using config.json configuration file. +By default, the client will look for this file in:

+
    +
  • current working directory

  • +
  • $HOME/.config/rdfm-mcumgr

  • +
  • /etc/rdfm-mcumgr

  • +
+

stopping at first configuration file found. +You can override this by specifying path to a different configuration file with -c/--config flag:

+
rdfm-mcumgr-client --config <path-to-config>
+
+
+

All of the non-device specific options can also be overwritten by specifying their flag counterpart. +For a full list you can run:

+
rdfm-mcumgr-client --help
+
+
+

Configuration values

+
    +
  • server - URL of the RDFM server the client should connect to

  • +
  • key_dir - path (relative or absolute) to the directory where all device keys are stored

  • +
  • update_interval - interval between each update poll to RDFM server (accepts time suffixes ‘s’, ‘m’, ‘h’)

  • +
  • retries - (optional) how many times should an update be attempted for a device in case of an error +(no value or value 0 means no limit)

  • +
  • devices - an array containing configuration for each device the client should handle

    +
      +
    • name - display name for device, used only for logging

    • +
    • id - unique device identifier used when communicating with RDFM server

    • +
    • device_type - device type reported to RDFM server used to specify compatible artifacts

    • +
    • key - name of the file containing device private key in PEM format. Key should be stored in key_dir directory.

    • +
    • self_confirm - (optional) bool indicating whether the device will confirm updates by itself. False by default

    • +
    • update_interval - (optional) override global update_interval for this device

    • +
    • transport - specifies the transport type for the device and it’s specific options

    • +
    +
  • +
  • groups - an array containing configuration for device groups

    +
      +
    • name - display name for group, used for logging

    • +
    • id - unique group identifier used when communicating with RDFM server

    • +
    • type - type reported to RDFM server to specify compatible artifacts

    • +
    • key - name of the file containing group private key in PEM format. Key should be stored in key_dir directory.

    • +
    • update_interval - (optional) override global update_interval for this group

    • +
    • members - an array containing configuration for each device that’s a member of this group

      +
        +
      • name - display name for device, used for logging

      • +
      • device - name of target image to match from an artifact

      • +
      • self_confirm - (optional) bool indicating whether the device will confirm updates by itself. False by default

      • +
      • transport - specifies the transport type for the device and its specific options

      • +
      +
    • +
    +
  • +
+

Transport specific:

+
    +
  • type - specific transport type for this device. Currently supported: ble, serial, udp

  • +
+
    +
  • BLE transport:

    +
      +
    • device_index - controller index to be used for connection (e.g. hci0 -> 0)

    • +
    • peer_name - the name the target BLE device advertises. Should match with CONFIG_BT_DEVICE_NAME

    • +
    +
  • +
  • Serial transport:

    +
      +
    • device - device name used for communicating with device. OS specific (e.g. "/dev/ttyUSB0", "/dev/tty.usbserial")

    • +
    • baud - communication speed; must match the baudrate of connected device

    • +
    • mtu - Maximum Transmission Unit, maximum protocol packet size

    • +
    +
  • +
  • UDP transport:

    +
      +
    • address: IPv4 / IPv6 address and port in IP:port form

    • +
    +
  • +
+

Device groups

+

The client supports grouping multiple Zephyr MCUboot boards to act as one complete device from management server’s perspective. +While each device in a group can be running different Zephyr application, +all devices are synchronized by the MCUmgr client to be running the exact same software version. +Group updates are performed using zephyr group artifacts +which contain update images for each member of the group and metadata on how to match image to device.

+

During an update, the MCUmgr client matches each image to its target member and tries to apply it. +Group update is considered successful only if all members of the group went through the update process without errors. +Otherwise all members are rolled back by the client to the previous version.

+

Example configuration

+
{
+  "server": "http://localhost:5000",
+  "key_dir": "keys",
+  "update_interval": "10s",
+  "retries": 3,
+  "devices": [
+    {
+      "name": "zephyr-ble",
+      "id": "11:11:11:11:11:11",
+      "dev_type": "zeph-ble",
+      "update_interval": "15s",
+      "key": "ble.key",
+      "transport": {
+        "type": "ble",
+        "device_index": 0,
+        "peer_name": "test0"
+      }
+    },
+    {
+      "name": "zephyr-serial",
+      "id": "22:22:22:22:22:22",
+      "dev_type": "zeph-ser",
+      "key": "serial.key",
+      "self_confirm": true,
+      "transport": {
+        "type": "serial",
+        "device": "/dev/ttyACM0",
+        "baud": 115200,
+        "mtu": 128
+      }
+    }
+  ],
+  "groups": [
+    {
+      "name": "group-one",
+      "id": "gr1",
+      "type": "group1",
+      "key": "group1.key",
+      "members": [
+        {
+          "name": "udpl",
+          "device": "udp-left",
+          "transport": {
+            "type": "udp",
+            "address": "192.168.1.2:1337"
+          }
+        },
+        {
+          "name": "udpr",
+          "device": "udp-right",
+          "transport": {
+            "type": "udp",
+            "address": "192.168.1.3:1337"
+          }
+        },
+        {
+          "name": "bleh",
+          "device": "ble",
+          "self_confirm": true,
+          "transport": {
+            "type": "ble",
+            "device_index": 0,
+            "peer_name": "ble_head",
+          }
+        }
+      ]
+    }
+  ]
+}
+
+
+

Device keys

+

Each device uses its own private key for authentication with rdfm-server as described in device authentication. +Each key should be stored under key_dir specified in configuration. +If the client doesn’t find corresponding device key for configured device, it will attempt to generate one itself. +The resulting key will be saved to the configured location with 0600 permissions.

+
+

Note

+

Device keys are different from the signing key used for signing the bootloader and application images!

+
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_mgmt_server.html b/rdfm_mgmt_server.html new file mode 100644 index 0000000..602dd0f --- /dev/null +++ b/rdfm_mgmt_server.html @@ -0,0 +1,1097 @@ + + + + + + + + + + + + + + + + RDFM Management Server - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM Management Server

+

Introduction

+

The RDFM Management Server is a core part of the RDFM ecosystem. The server manages incoming device connections and grants authorization only to those which are allowed to check-in with the server. +It also handles package upload and management, deploy group management and other crucial functionality required for robust and secure device Over-The-Air (OTA) updates along with allowing remote system management without exposing devices to the outside world.

+

REST API

+

The server exposes a management and device API that is used by management software and end devices. A comprehensive list of all API endpoints is available in the RDFM Server API Reference chapter.

+

Setting up a Dockerized development environment

+

The preferred method for running the RDFM server is by using a Docker container. +To set up a local development environment, first clone the RDFM repository:

+
git clone https://github.com/antmicro/rdfm.git
+cd rdfm/
+
+
+

A Dockerfile is provided in the server/deploy/ directory that builds a container suitable for running the server. +Currently, it is required to build the container image manually. +To do this, run the following from the cloned RDFM repository root folder:

+
docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest .
+
+
+

A simple docker-compose file that can be used to run the server is provided below, and in the server/deploy/docker-compose.development.yml file.

+
services:
+  rdfm-server:
+    image: antmicro/rdfm-server:latest
+    restart: unless-stopped
+    environment:
+      - RDFM_JWT_SECRET=<REPLACE_WITH_CUSTOM_JWT_SECRET>
+      - RDFM_DB_CONNSTRING=sqlite:////database/development.db
+      - RDFM_HOSTNAME=rdfm-server
+      - RDFM_API_PORT=5000
+      - RDFM_DISABLE_ENCRYPTION=1
+      - RDFM_DISABLE_API_AUTH=1
+      - RDFM_LOCAL_PACKAGE_DIR=/packages/
+      - RDFM_WSGI_SERVER=werkzeug
+    ports:
+      - "5000:5000"
+    volumes:
+      - db:/database/
+      - pkgs:/packages/
+
+volumes:
+  db:
+  pkgs:
+
+
+

The server can then be started using the following command:

+
docker-compose -f server/deploy/docker-compose.development.yml up
+
+
+

Configuration via environment variables

+

Configuration of the RDFM server can be changed by using the following environment variables:

+
    +
  • RDFM_JWT_SECRET - secret key used by the server when issuing JWT tokens, this value must be kept secret and not easily guessable (for example, a random hexadecimal string).

  • +
  • RDFM_DB_CONNSTRING - database connection string, for examples please refer to: SQLAlchemy - Backend-specific URLs. Currently, only the SQLite and PostgreSQL engines were verified to work with RDFM (however: the PostgreSQL engine requires adding additional dependencies which are currently not part of the default server image, this may change in the future).

  • +
+

Development configuration:

+
    +
  • RDFM_DISABLE_ENCRYPTION - if set, disables the use of HTTPS, falling back to exposing the API over HTTP. This can only be used in production if an additional HTTPS reverse proxy is used in front of the RDFM server.

  • +
  • RDFM_DISABLE_API_AUTH - if set, disables request authentication on the exposed API routes. WARNING: This is a development flag only! Do not use in production! This causes all API methods to be freely accessible, without any access control in place!

  • +
  • RDFM_ENABLE_CORS - if set, disables CORS checks, which in consequence allows any origin to access the server. WARNING: This is a development flag only! Do not use in production!

  • +
+

HTTP/WSGI configuration:

+
    +
  • RDFM_HOSTNAME - hostname/IP address to listen on. This is additionally used for constructing package URLs when storing packages in a local directory.

  • +
  • RDFM_API_PORT - API port.

  • +
  • RDFM_SERVER_CERT - required when HTTPS is enabled; path to the server’s certificate. The certificate can be stored on a Docker volume mounted to the container. For reference on generating the certificate/key pairs, see the server/tests/certgen.sh script.

  • +
  • RDFM_SERVER_KEY - required when HTTPS is enabled; path to the server’s private key. Additionally, the above also applies here.

  • +
  • RDFM_WSGI_SERVER - WSGI server to use, this value should be left default. Accepted values: gunicorn (default, production-ready), werkzeug (recommended for development).

  • +
  • RDFM_WSGI_MAX_CONNECTIONS - (when using Gunicorn) maximum amount of connections available to the server worker. This value must be set to at minimum the amount of devices that are expected to be maintaining a persistent (via WebSocket) connection with the server. Default: 4000.

  • +
  • RDFM_INCLUDE_FRONTEND_ENDPOINT - specifies whether the RDFM server should serve the frontend application. If set, the server will serve the frontend application from endpoint /api/static/frontend. Before setting this variable, the frontend application must be built and placed in the frontend/dist directory.

  • +
  • RDFM_FRONTEND_APP_URL - specifies URL to the frontend application. This variable is required when RDFM_INCLUDE_FRONTEND_ENDPOINT is not set, as backend HTTP server has to know where to redirect the user.

  • +
+

API OAuth2 configuration (must be present when RDFM_DISABLE_API_AUTH is omitted):

+
    +
  • RDFM_OAUTH_URL - specifies the URL to an authorization server endpoint compatible with the RFC 7662 OAuth2 Token Introspection extension. This endpoint is used to authorize access to the RDFM server based on tokens provided in requests made by API users.

  • +
  • RDFM_LOGIN_URL - specifies the URL to a login page of the authorization server. It is used to authorize users and generate an access token and start a session.

  • +
  • RDFM_LOGOUT_URL - specified the URL to a logout page of the authorization server. It is used to end the session and revoke the access token.

  • +
  • RDFM_OAUTH_CLIENT_ID - if the authorization server endpoint provided in RDFM_OAUTH_URL requires the RDFM server to authenticate, this variable defines the OAuth2 client_id used for authentication.

  • +
  • RDFM_OAUTH_CLIENT_SEC - if the authorization server endpoint provided in RDFM_OAUTH_URL requires the RDFM server to authenticate, this variable defines the OAuth2 client_secret used for authentication.

  • +
+

Package storage configuration:

+
    +
  • RDFM_STORAGE_DRIVER - storage driver to use for storing artifacts. Accepted values: local (default), s3.

  • +
  • RDFM_LOCAL_PACKAGE_DIR - specifies a path (local for the server) to a directory where the packages are stored.

  • +
  • RDFM_S3_BUCKET - when using S3 storage, name of the bucket to upload the packages to.

  • +
  • RDFM_S3_ACCESS_KEY_ID - when using S3 storage, Access Key ID to access the specified bucket.

  • +
  • RDFM_S3_ACCESS_SECRET_KEY - when using S3 storage, Secret Access Key to access the specified bucket.

  • +
+

Configuring package storage location

+

Storing packages locally

+

By default (when not using one of the above deployment setups), the server stores all uploaded packages to a temporary folder under /tmp/.rdfm-local-storage/. +To persist package data, configuration of an upload folder is required. +This can be done by using the RDFM_LOCAL_PACKAGE_DIR environment variable (in the Dockerized deployment), which should contain a path to the desired upload folder.

+
+

Warning

+

This storage method should NOT be used for production deployments! +The performance of the built-in file server is severely limited and provides NO caching, which will negatively affect the update speed for all devices even when a few of them try downloading an update package at the same time. +It is recommended to use a dedicated storage solution such as S3 to store packages.

+
+

Storing packages on S3-compatible storage

+

The RDFM server can also store package data on S3 and other S3 API-compatible object storage servers. +The following environment variables allow changing the configuration of the S3 integration:

+
    +
  • RDFM_S3_BUCKET - name of the bucket to upload the packages to

  • +
  • RDFM_S3_ACCESS_KEY_ID - Access Key ID to access the specified bucket

  • +
  • RDFM_S3_ACCESS_SECRET_KEY - Secret Access Key to access the specified bucket +Additionally, when using S3 storage, the environment variable RDFM_STORAGE_DRIVER must be set to s3.

  • +
+

An example reference setup utilizing the MinIO Object Storage server is provided in the server/deploy/docker-compose.minio.yml file. +To run it, first build the RDFM server container like in the above setup guides:

+
docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest .
+
+
+

Then, run the following:

+
docker-compose -f server/deploy/docker-compose.minio.development.yml up
+
+
+

Configuring API authentication

+

Basic configuration

+

The above development setup does not provide any authentication for the RDFM API. +This is helpful for development or debugging purposes, however under no circumstance should this be used in production deployments, as it exposes the entire API with no restrictions in place.

+

By default, the RDFM server requires configuration of an external authorization server to handle token creation and scope management. +To be compatible with RDFM Management Server, the authentication server MUST support the OAuth2 Token Introspection extension (RFC 7662).

+

The authorization server is configured using the following environment variables:

+
    +
  • RDFM_OAUTH_URL - specifies the URL to the Token Introspection endpoint of the authorization server.

  • +
  • RDFM_OAUTH_CLIENT_ID - specifies the client identifier to use for authenticating the RDFM server to the authorization server.

  • +
  • RDFM_OAUTH_CLIENT_SEC - specifies the client secret to use for authenticating the RDFM server to the authorization server.

  • +
+

For accessing the management API, the RDFM server does not issue any tokens itself. +This task is delegated to the authorization server that is used in conjunction with RDFM. +The following scopes are used for controlling access to different methods of the RDFM API:

+
    +
  • rdfm_admin_ro - read-only access to the API (fetching devices, groups, packages)

  • +
  • rdfm_admin_rw - complete administrative access to the API with modification rights

  • +
+

Additional rules are defined for package uploading route from Packages API.

+
    +
  • rdfm_upload_single_file - allows uploading an artifact of type single-file.

  • +
  • rdfm_upload_rootfs_image - allows uploading artifacts rootfs-image and delta-rootfs-image. +Each package type requires its corresponding scope, or the complete admin access - rdfm_admin_rw.

  • +
+

Refer to the RDFM Server API Reference chapter for a breakdown of the scopes required for accessing each API method.

+

API authentication using Keycloak

+

Running the services

+

An example docker-compose file that can be used to run the RDFM server using Keycloak Identity and Access Management server as an authorization server is provided below, and in the server/deploy/docker-compose.keycloak.development.yml file.

+
services:
+  rdfm-server:
+    image: antmicro/rdfm-server:latest
+    restart: unless-stopped
+    environment:
+      - RDFM_JWT_SECRET=<REPLACE_WITH_CUSTOM_JWT_SECRET>
+      - RDFM_DB_CONNSTRING=sqlite:////database/development.db
+      - RDFM_HOSTNAME=rdfm-server
+      - RDFM_API_PORT=5000
+      - RDFM_DISABLE_ENCRYPTION=1
+      - RDFM_LOCAL_PACKAGE_DIR=/packages/
+      - RDFM_OAUTH_URL=http://keycloak:8080/realms/master/protocol/openid-connect/token/introspect
+      - RDFM_OAUTH_CLIENT_ID=rdfm-server-introspection
+      - RDFM_OAUTH_CLIENT_SEC=<REPLACE_WITH_RDFM_INTROSPECTION_SECRET>
+    networks:
+      - rdfm
+    ports:
+      - "5000:5000"
+    volumes:
+      - db:/database/
+      - pkgs:/packages/
+
+  keycloak:
+    image: quay.io/keycloak/keycloak:22.0.1
+    restart: unless-stopped
+    environment:
+      - KEYCLOAK_ADMIN=admin
+      - KEYCLOAK_ADMIN_PASSWORD=admin
+    networks:
+      - rdfm
+    ports:
+      - "8080:8080"
+    command:
+      - start-dev
+    volumes:
+      - keycloak:/opt/keycloak/data/
+      - ../keycloak-themes:/opt/keycloak/themes
+
+volumes:
+  db:
+  pkgs:
+  keycloak:
+
+networks:
+  rdfm:
+
+
+

Before running the above services, you must first build the RDFM server container by running the following from the RDFM repository root folder:

+
docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest .
+
+
+

You can then run the services by running:

+
docker-compose -f server/deploy/docker-compose.keycloak.development.yml up
+
+
+

Keycloak configuration

+

Further configuration on the Keycloak server is required before any requests are successfully authenticated. +First, navigate to the Keycloak Administration Console found at http://localhost:8080/ and login with the initial credentials provided in Keycloak’s configuration above (by default: admin/admin).

+

Next, go to Clients and press Create client. +This client is required for the RDFM server to perform token validation. +The following settings must be set when configuring the client:

+
    +
  • Client ID - must match RDFM_OAUTH_CLIENT_ID provided in the RDFM server configuration, can be anything (for example: rdfm-server-introspection)

  • +
  • Client Authentication - set to On

  • +
  • Authentication flow - select only Service accounts roles

  • +
+

After saving the client, go to the Credentials tab found under the client details. +Make sure the authenticator used is Client Id and Secret, and copy the Client secret. +This secret must be configured in the RDFM server under the RDFM_OAUTH_CLIENT_SEC environment variable.

+
+

Note

+

After changing the docker-compose variables, remember to restart the services (by pressing Ctrl+C and re-running the docker-compose up command).

+
+

Additionally, you must create proper client scopes and user roles to define which users have access to the read-only and read-write parts of the RDFM API. +To create new scopes, navigate to the Client scopes tab and select Create client scope. +Create four separate scopes with the following names; the rest of the settings can be left as default (if required, you may also add a description to the scope):

+
    +
  • rdfm_admin_ro

  • +
  • rdfm_admin_rw

  • +
  • rdfm_upload_single_file

  • +
  • rdfm_upload_rootfs_image

  • +
+

To create new roles, navigate to the Realm roles tab and select Create role. +Create separate roles with the same names. The rest of the settings can be left as default (if required, you may also add a description to the role).

+

After restarting the services, the RDFM server will now validate requests against the Keycloak server. +To further setup the rdfm-mgmt manager to use the Keycloak server, refer to the RDFM manager manual. To add users with roles to the Keycloak server, which can then be used to access the RDFM API using the frontend application, refer to the Adding a User section below.

+

Adding an API client

+

First, navigate to the Keycloak Administration Console found at http://localhost:8080/ and login with the initial credentials provided in Keycloak’s configuration above (by default: admin/admin).

+

Next, go to Clients and press Create client. +This client will represent a user of the RDFM API. +The following settings must be set when configuring the client:

+
    +
  • Client Authentication - set to On

  • +
  • Authentication flow - select only Service accounts roles

  • +
+

After saving the client, go to the Credentials tab found under the client details. +Make sure the authenticator used is Client Id and Secret, and copy the Client secret.

+

Finally, assign the required scope to the client: under the Client scopes tab, click Add client scope and select one of the two RDFM scopes: read-only rdfm_admin_ro or read-write rdfm_admin_rw.

+
+

Note

+

The newly-created client will now have access to the RDFM API. +To configure rdfm-mgmt to use this client, follow the Configuration section of the RDFM manager manual.

+
+

Adding a User

+

First, navigate to the Keycloak Administration Console found at http://localhost:8080/ and login with the initial credentials provided in Keycloak’s configuration above (by default: admin/admin).

+

Next, go to Users tab and press Add user. This will open up a form to create a new user. +Fill in the Username field and press Create.

+

Next, go to Credentials tab found under the user details and press Set password. +This form allows you to set a password for the user and determine whether creating a new one is required on the next login.

+

After configuring the user, go to Role mapping tab under the user details. +There, appropriate roles can be assigned to the user using the Assign role button.

+
+

Note

+

The newly created users can now log in using the RDFM frontend application. +To configure and run the frontend application, refer to the RDFM Frontend chapter.

+
+
Configuring frontend application
+

When using the frontend application, logging in functionality is provided by the Keycloak server. +To integrate the Keycloak server with the frontend application first go to the client details created in the Keycloak configuration section.

+

Go to Capability config and make sure that Implicit flow and Standard flow are enabled.

+

Open Settings panel and set Valid redirect URIs and Valid post logout redirect URIs values to the URL of the frontend application. +The value depends on the deployment method, if the rdfm-server is used to host the frontend application the value can be inferred from the RDFM_HOSTNAME and RDFM_API_PORT environment variables and will most likely be http[s]://{RDFM_HOSTNAME}:{RDFM_API_PORT}. +Otherwise, the value should be equal to RDFM_FRONTEND_APP_URL variable.

+

Additionally, you can change the theme of the login page to match the frontend application. +To do this, go to Login settings section and rdfm in the Login theme dropdown.

+

Configuring HTTPS

+

For simple deployments, the server can expose an HTTPS API directly without requiring an additional reverse proxy. +Configuration of the server’s HTTPS can be done using the following environment variables:

+
    +
  • RDFM_SERVER_CERT - path to the server’s signed certificate

  • +
  • RDFM_SERVER_KEY - path to the server’s private key

  • +
+

Both of these files must be accessible within the server Docker container.

+

HTTPS demo deployment

+
+

Warning

+

This demo deployment explicitly disables API authentication, and is only meant to be used as a reference on how to configure your particular deployment.

+
+

An example HTTPS deployment can be found in the server/deploy/docker-compose.https.development.yml file. +Before running it, you must execute the tests/certgen.sh in the server/deploy/ directory:

+
cd server/deploy/
+../tests/certgen.sh
+
+
+

This script generates a root CA and an associated signed certificate to be used for running the server. +The following files are generated:

+
    +
  • certs/CA.{crt,key} - CA certificate/private key that is used as the root of trust

  • +
  • certs/SERVER.{crt,key} - signed certificate/private key used by the server

  • +
+

To run the deployment, you must first build the RDFM server container by running the following from the RDFM repository root folder:

+
docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest .
+
+
+

You can then start the deployment by running:

+
docker-compose -f server/deploy/docker-compose.https.development.yml up
+
+
+

To verify the connection to the server, you must provide the CA certificate. +For example, when using curl to access API methods:

+
curl --cacert server/deploy/certs/CA.crt https://127.0.0.1:5000/api/v1/devices
+
+
+

When using rdfm-mgmt:

+
rdfm-mgmt --url https://127.0.0.1:5000/     \
+          --cert server/deploy/certs/CA.crt \
+          --no-api-auth                     \
+          devices list
+
+
+

Production deployments

+

Production considerations

+

The following is a list of considerations when deploying the RDFM server:

+
    +
  1. HTTPS must be enabled; RDFM_DISABLE_ENCRYPTION must not be set (or the server is behind a dedicated reverse proxy that adds HTTPS on the edge).

  2. +
  3. API authentication must be enabled; RDFM_DISABLE_API_AUTH must not be set.

  4. +
  5. RDFM must use a production WSGI server; RDFM_WSGI_SERVER must not be set to werkzeug. +When not provided, the server defaults to using a production-ready WSGI server (gunicorn). +The development server (werkzeug) does not provide sufficient performance to handle production workloads, and a high percentage of requests will be dropped under heavy load.

  6. +
  7. RDFM must use a dedicated (S3) package storage location; the local directory driver does not provide adequate performance when compared to dedicated object storage.

  8. +
+

Refer to the above configuration chapters for how to configure each aspect of the RDFM server:

+
    +
  1. Configuring HTTPS

  2. +
  3. Configuring API authentication

  4. +
  5. Configuring the WSGI server

  6. +
  7. Configuring S3 package storage

  8. +
+

A practical example of a deployment that includes all the above considerations can be found below, in the Production example deployment section.

+

Production example deployment

+
+

Warning

+

For simplicity, this example deployment has static credentials pre-configured pretty much everywhere, and as such should never be used directly as a production setup. +At least the following secrets are pre-configured and would require changes:

+
    +
  • S3 Access Key ID/Access Secret Key

  • +
  • rdfm-server JWT secret

  • +
  • Keycloak Administrator username/password

  • +
  • Keycloak Client: rdfm-server introspection Client ID/Secret

  • +
  • Keycloak Client: rdfm-mgmt admin user Client ID/Secret

  • +
+

Additionally, the Keycloak server requires further configuration for production deployments. +For more information, refer to the Configuring Keycloak for production page in Keycloak documentation.

+
+

A reference setup is provided in server/deploy/docker-compose.production.yml that can be used for customizing production server deployments. +Prior to starting the deployment, you must generate a signed server certificate that will be used for establishing the HTTPS connection to the server. +This can be done by either providing your own certificate, or by running the provided example certificate generation script:

+
cd server/deploy/
+../tests/certgen.sh
+
+
+

When using the certgen.sh script, the CA certificate found at server/deploy/certs/CA.crt can be used for validating the connection made to the server.

+

Similarly to previous example deployments, it can be started by running the following command from the RDFM monorepository root folder:

+
docker-compose -f server/deploy/docker-compose.production.yml up
+
+
+

rdfm-mgmt configuration for this deployment can be found in server/deploy/test-rdfm-mgmt-config.json. +After copying the configuration to $HOME/.config/rdfm-mgmt/config.json, you can access the server by running:

+
rdfm-mgmt --url https://127.0.0.1:5000/ --cert server/deploy/CA.crt \
+          devices list
+
+
+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/rdfm_ota_manual.html b/rdfm_ota_manual.html new file mode 100644 index 0000000..fbe5bed --- /dev/null +++ b/rdfm_ota_manual.html @@ -0,0 +1,733 @@ + + + + + + + + + + + + + + + + RDFM OTA Manual - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

RDFM OTA Manual

+

This chapter contains key information about the RDFM OTA update system.

+

Key concepts

+

Below is a brief explanation of the key entities of the RDFM update system.

+

Devices

+

From the server’s point of view, a device is any system that is running an RDFM-compatible update client. +For example, see RDFM Linux Device Client. +Each device actively reports its metadata to the server:

+
    +
  • Currently running software version (rdfm.software.version)

  • +
  • Device type (rdfm.hardware.devtype)

  • +
  • Other client-specific metadata

  • +
+

Packages

+

A package is any file that can be used by a compatible update client to update the running system. +From the server’s point of view, update packages are simple binary blobs and no specific structure is enforced. +Each package has metadata assigned to it that indicates its contents. +The following metadata fields are mandatory for all packages:

+
    +
  • Software version (rdfm.software.version) - indicates the version of the contained software

  • +
  • Device type (rdfm.hardware.devtype) - indicates the device type a package is compatible with

  • +
+

The device type is used as the first filter when searching for a compatible update package. +Any package that does not match the device type reported by the update client will be considered incompatible.

+

A package may also contain metadata with requires: clauses. +The requires clause is used to indicate dependencies on certain metadata properties of the device. +In its most basic form, it can be used to indicate a dependency on a certain system image to be installed for proper delta update installation. +For more complex use cases involving many intermediate update steps, it can also be used to enforce an order in which certain packages must be installed.

+

Groups

+

A group consists of many assigned devices. Each group can also be assigned one or many packages. +The group itself also contains metadata about the group name, description, update policy, and other arbitrary information which can be used by custom frontends interacting with the server.

+

Update policy

+

An update policy defines the target version the devices within a given group will be updated to. +The policy is a string with the syntax <policy>,[arguments]. +Required arguments depend on the specific policy being used. +Currently, the following policies are supported:

+
    +
  • no_update (default) - requires no arguments, the server will treat all devices within the group as up-to-date, and will not return any packages to devices requesting an update check. This is the default update policy for all newly created groups.

  • +
  • exact_match - specifies that the server will attempt to install the target software version on each of the devices in the group. +Example usage: exact_match,version1 - this specifies that the server will attempt to bring all of the devices to the software version version1. +This process may involve installing many intermediate packages, but the end result is a device that’s running the specified version. +The server will use group-assigned packages when resolving the dependency graph required for reaching the target version.

  • +
+

Update resolution

+

When resolving a path to the correct target version, the server utilizes only the group-assigned packages. +When a device is requesting an update check, a package dependency graph is created. +The edges of the graph correspond to different packages available during the update process (which are compatible with the device, as indicated by the rdfm.hardware.devtype field), while the nodes indicate the software versions (as indicated by the rdfm.software.version fields of each package). +Next, the group’s update policy is queried, which indicates the target version/node each device should be attempting to reach. +The shortest path between the currently running node and the target node is used as instructions for how the server should lead the device to the specified version.

+

Example scenario: simple update assignment

+

Consider a group with the following packages assigned:

+
    +
  • P0 - devtype=foo, version=v1

  • +
  • P1 - devtype=bar, version=v2

  • +
  • P2 - devtype=baz, version=v3

  • +
+

The group is specified to update to version v3 per the policy. Devices are reporting the following metadata:

+
    +
  • D0 - devtype=foo, version=v3

  • +
  • D1 - devtype=bar, version=v3

  • +
  • D2 - devtype=baz, version=v3

  • +
+

In this scenario, devices D0 and D1 shall receive the update packages P0 and P1 respectively. +The device D2 is considered up-to-date, as its version matches the target specified in the group policy.

+

Example scenario: downgrades

+

Consider a group with the following packages assigned:

+
    +
  • P0 - devtype=foo, version=v4

  • +
+

The group is specified to update to version v4 per the policy. Devices are reporting the following metadata:

+
    +
  • D0 - devtype=foo, version=v5

  • +
+

In this scenario, the device D0 will receive the package P0 to be installed next.

+

Example scenario: sequential updates

+

Consider a group with the following packages assigned:

+
    +
  • P0 - devtype=foo, version=v2, requires:version=v1

  • +
  • P1 - devtype=foo, version=v3, requires:version=v2

  • +
+

The group is specified to update to version v3 per the policy. Devices are reporting the following metadata:

+
    +
  • D0 - devtype=foo, version=v1

  • +
+

In this scenario, the device D0 will first be updated to the package P0, as it’s the only package that is compatible (matching device type and different version than the one running on the device). +The package’s only requires clause also matches against the device’s metadata.

+

After successful installation, during the next update check on the newly installed version (v1), the device will receive the next available package. +As the device is now reporting a version field of v1 and the package’s requires: clause passes, package P1 becomes the next candidate package available for installation. +After successful instalation of P1, no more packages are available and the device is considered to be up-to-date.

+

Example scenario: delta updatess

+

Consider a group with the following packages assigned:

+
    +
  • P0 (delta) - devtype=foo, version=v5, rootfs=e6e2531.., requires:version=v0, requires:rootfs=2f646ac..

  • +
  • P1 (delta) - devtype=foo, version=v5, rootfs=e6e2531.., requires:version=v2, requires:rootfs=6d9aee4..

  • +
+

The group is specified to update to version v5 per the policy. Devices are reporting the following metadata:

+
    +
  • D0 - devtype=foo, version=v0, rootfs=2f646ac..

  • +
  • D1 - devtype=foo, version=v2, rootfs=6d9aee4..

  • +
+

In this scenario, devices D0 and D1 will receive packages P0 and P1 as updates respectively. +The packages themselves contain different binary contents, in this case a delta between a given base version’s system partition (v0 and v2) and the target (v5), but the end result is an identical system on both devices. +This way, many delta packages may be provided for updating a fleet consisting of a wide range of running versions.

+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..d2db218 --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"titles": ["RDFM Server API Reference", "RDFM Documentation", "Introduction", "RDFM Android Device Client", "RDFM Artifact utility", "RDFM Frontend", "RDFM Linux Device Client", "RDFM Manager utility", "RDFM MCUmgr Device Client", "RDFM Management Server", "RDFM OTA Manual", "Server Integration flows", "System Architecture"], "terms": {"By": [0, 4, 7, 8, 9, 11], "default": [0, 3, 4, 7, 8, 9, 10, 11], "expect": [0, 3, 9, 11], "all": [0, 5, 6, 7, 8, 9, 10, 11], "request": [0, 5, 7, 9, 10, 11, 12], "depend": [0, 5, 6, 7, 9, 10, 11], "type": [0, 3, 4, 6, 8, 9, 10, 11], "thi": [0, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], "can": [0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "either": [0, 8, 9], "token": [0, 7, 9, 11], "In": [0, 4, 6, 7, 8, 10, 11], "case": [0, 3, 4, 6, 7, 8, 10, 11], "pass": [0, 3, 7, 8, 10, 11], "part": [0, 3, 9], "http": [0, 1, 2, 4, 5, 6, 7, 8, 11], "header": [0, 8, 11], "an": [0, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12], "exampl": [0, 1, 3, 4, 6, 7, 11, 12], "i": [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "shown": [0, 3, 7, 11], "below": [0, 3, 5, 7, 8, 9, 10, 11], "get": [0, 1], "v1": [0, 9, 10, 11], "1": [0, 3, 5, 6, 7, 8, 9, 11], "host": [0, 5, 9], "5000": [0, 6, 7, 8, 9], "user": [0, 2, 5, 7, 8, 11, 12], "agent": 0, "python": [0, 7], "2": [0, 8], "31": 0, "0": [0, 3, 4, 6, 7, 8, 9, 12], "accept": [0, 8, 9, 11], "encod": [0, 3, 11, 12], "gzip": 0, "deflat": 0, "connect": [0, 2, 3, 6, 7, 8, 9, 11, 12], "keep": 0, "aliv": 0, "bearer": 0, "eyjhbgcioijsuzi1niisinr5cc": 0, "truncat": 0, "rpponb7": 0, "iask89ypgayxg": 0, "ani": [0, 5, 6, 8, 9, 10, 11], "wa": [0, 7, 8, 11], "successfulli": [0, 9, 11], "becaus": 0, "miss": [0, 3], "otherwis": [0, 8, 9], "invalid": 0, "return": [0, 8, 10, 11], "401": [0, 11], "unauthor": [0, 11], "statu": [0, 11], "code": [0, 5, 8, 11], "addition": [0, 4, 6, 8, 9, 11], "given": [0, 4, 6, 10], "doe": [0, 6, 9, 10, 11], "provid": [0, 2, 3, 4, 6, 8, 9, 10, 11, 12], "suffici": [0, 9], "access": [0, 7, 8, 9, 11, 12], "resourc": [0, 3, 12], "reject": [0, 8], "403": 0, "forbidden": 0, "happen": 0, "claim": 0, "scope": [0, 7, 9], "requir": [0, 3, 7, 9, 10, 11, 12], "target": [0, 1, 3, 4, 6, 10], "endpoint": [0, 5, 7, 9, 11, 12], "try": [0, 8, 9], "upload": [0, 3, 8, 9], "us": [0, 1, 2, 4, 5, 7, 8, 10, 11, 12], "read": [0, 8, 9, 11, 12], "onli": [0, 3, 4, 8, 9, 10, 11, 12], "should": [0, 4, 5, 6, 7, 8, 9, 10, 11], "occur": 0, "dure": [0, 3, 6, 8, 10, 11], "incorrect": 0, "data": [0, 5, 6, 7, 9], "other": [0, 2, 4, 9, 10, 11], "specif": [0, 4, 8, 9, 10, 11], "scenario": [0, 1], "structur": [0, 6, 10, 11, 12], "contain": [0, 3, 4, 5, 6, 8, 9, 10, 11], "friendli": 0, "descript": [0, 5, 7, 9, 10], "respons": [0, 11, 12], "delet": [0, 3, 8], "fail": [0, 8, 11], "assign": [0, 1, 9], "least": [0, 4, 7, 9], "one": [0, 3, 4, 6, 7, 8, 9, 10, 11], "fetch": [0, 6, 8, 9], "list": [0, 6, 8, 9, 11, 12], "200": 0, "ok": 0, "did": 0, "ha": [0, 5, 8, 9, 10, 11], "expir": [0, 11], "json": [0, 6, 7, 8, 9, 11, 12], "arrai": [0, 6, 8], "object": [0, 9, 11, 12], "id": [0, 3, 5, 7, 8, 9], "integ": 0, "identifi": [0, 7, 8, 9, 11], "creat": [0, 6, 9, 10], "string": [0, 3, 9, 10], "utc": 0, "creation": [0, 4, 9], "date": [0, 6, 10], "rfc822": 0, "sha256": 0, "driver": [0, 8, 9], "storag": [0, 1, 11], "store": [0, 4, 7, 8, 11], "metadata": [0, 3, 4, 8, 10, 11], "dict": 0, "str": 0, "kei": [0, 1, 2, 3, 5, 6, 9, 11], "valu": [0, 3, 5, 9, 11], "pair": [0, 9, 11], "applic": [0, 1, 2, 3, 6], "text": [0, 11], "javascript": 0, "content": [0, 6, 10, 11], "thu": 0, "17": 0, "aug": 0, "2023": 0, "10": [0, 8], "41": 0, "08": 0, "gmt": 0, "hardwar": [0, 3, 10, 11], "devtyp": [0, 3, 10, 11], "dummydevic": 0, "softwar": [0, 3, 8, 9, 10, 11], "version": [0, 1, 4, 8, 10, 11], "v10": 0, "local": [0, 3, 7, 11], "length": [0, 11], "4194304": 0, "uuid": [0, 11], "6f7483ac": 0, "5cde": 0, "467f": 0, "acf7": 0, "39e4b397e313": 0, "4e415854e6d0cf9855b2290c02638e8651537989b8862ff9c9cb91b8d956ea06": 0, "rdfm_admin_ro": [0, 9], "administr": [0, 9, 11], "rdfm_admin_rw": [0, 9], "post": [0, 9], "remain": 0, "form": [0, 8, 9, 10, 11], "ar": [0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "artifact": [0, 1, 2, 6, 8, 9], "If": [0, 3, 5, 6, 7, 8, 9, 11], "addit": [0, 6, 7, 8, 9, 11, 12], "directori": [0, 3, 4, 5, 6, 7, 8, 9], "specifi": [0, 6, 8, 9, 10, 11], "indic": [0, 5, 8, 10, 11], "within": [0, 6, 7, 9, 10], "side": [0, 7, 11], "place": [0, 4, 7, 9], "paramet": 0, "file": [0, 3, 5, 6, 7, 8, 9, 10, 11], "binari": [0, 4, 6, 8, 10, 11], "compat": [0, 4, 6, 8, 10, 11], "option": [0, 4, 6, 7, 8], "current": [0, 3, 4, 6, 7, 8, 9, 10, 11], "400": 0, "bad": 0, "reserv": 0, "have": [0, 3, 4, 5, 6, 7, 8, 9, 11], "permiss": [0, 3, 6, 8], "4194738": 0, "multipart": 0, "boundari": 0, "0f8f9642db3a513": 0, "disposit": 0, "name": [0, 3, 4, 7, 8, 9, 10], "filenam": [0, 8], "img": [0, 4, 7], "octet": 0, "stream": [0, 11], "appropri": [0, 8, 9], "write": [0, 4, 9, 11, 12], "avail": [0, 1, 4, 5, 6, 9, 10, 11, 12], "rdfm_upload_single_fil": [0, 9], "singl": [0, 9], "rdfm_upload_rootfs_imag": [0, 9], "rootf": [0, 6, 9, 10], "imag": [0, 3, 4, 5, 6, 9, 10], "int": 0, "from": [0, 1, 3, 5, 7, 9, 10, 11, 12], "underli": 0, "The": [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "": [0, 2, 3, 4, 6, 8, 9, 10, 11], "404": 0, "Not": 0, "found": [0, 3, 5, 8, 9, 11, 12], "exist": [0, 3, 6], "409": 0, "conflict": 0, "cannot": 0, "inform": [0, 7, 8, 9, 10], "about": [0, 10, 11], "simpl": [0, 1, 4, 8, 9, 11], "local_storag": 0, "path": [0, 3, 4, 7, 8, 9, 10], "expos": [0, 2, 7, 9, 12], "warn": [0, 9], "product": [0, 1, 3, 5], "deploy": [0, 1, 2, 3, 4, 5, 6, 12], "test": [0, 1, 3, 8, 9], "disabl": [0, 3, 4, 7, 9], "futur": [0, 9, 11], "non": [0, 8], "prod": 0, "configur": [0, 1], "public": [0, 11], "rout": [0, 9, 11, 12], "v2": [0, 10], "new": [0, 3, 4, 6, 8, 9, 11], "prioriti": 0, "lower": [0, 6], "take": [0, 6, 11], "preced": 0, "A": [0, 4, 6, 7, 9, 10, 11, 12], "mon": 0, "14": 0, "11": [0, 7, 8, 11], "50": 0, "40": 0, "polici": 0, "no_upd": [0, 10], "00": [0, 11], "56": 0, "25": 0, "being": [0, 10], "must": [0, 3, 4, 5, 6, 7, 8, 9, 10, 11], "NOT": [0, 7, 9], "still": 0, "patch": 0, "modifi": [0, 3, 7, 8], "allow": [0, 2, 3, 4, 5, 6, 7, 8, 9, 12], "describ": [0, 2, 5, 6, 8, 11], "two": [0, 4, 9], "ad": [0, 3, 4, 7], "remov": [0, 7], "oper": [0, 2], "atom": 0, "point": [0, 10], "encount": 0, "entir": [0, 4, 9], "abort": 0, "cover": 0, "which": [0, 2, 3, 4, 6, 7, 8, 9, 10, 11], "match": [0, 3, 8, 9, 10], "regist": [0, 11], "alreadi": [0, 3, 4, 11], "same": [0, 4, 5, 7, 8, 9, 11], "even": [0, 9], "evalu": 0, "first": [0, 3, 6, 7, 8, 9, 10, 11], "follow": [0, 2, 3, 5, 6, 7, 9, 10, 11, 12], "situat": [0, 11], "abov": [0, 6, 7, 9, 11], "add": [0, 3, 7, 8, 9], "5": [0, 11], "3": [0, 7, 8], "schema": [0, 11], "empti": 0, "chang": [0, 3, 5, 6, 9], "defin": [0, 3, 5, 6, 8, 9, 10, 11], "each": [0, 2, 6, 7, 8, 9, 10, 11, 12], "receiv": [0, 7, 10, 11], "For": [0, 3, 4, 6, 7, 8, 9, 10, 11, 12], "consult": [0, 11], "ota": [0, 1, 2, 3, 6, 9], "manual": [0, 1, 2, 3, 9], "set": [0, 1, 3, 5, 6, 11], "exact_match": [0, 10], "control": [0, 8, 9], "appli": [0, 3, 4, 5, 8, 9], "multipl": [0, 4, 5, 8], "also": [0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "anoth": 0, "check": [0, 1, 6, 8, 9, 10, 12], "client": [0, 1, 2, 4, 5, 7, 10, 11, 12], "call": [0, 8], "associ": [0, 7, 9], "At": [0, 9, 11], "minimum": [0, 9], "macaddr": [0, 11], "present": [0, 3, 9, 11], "base": [0, 3, 4, 7, 8, 9, 10, 12], "pick": 0, "ones": 0, "more": [0, 6, 7, 8, 9, 10, 11], "than": [0, 4, 6, 8, 10], "lowest": 0, "204": 0, "No": 0, "mac": [0, 7, 11], "address": [0, 3, 7, 8, 9, 11], "run": [0, 1, 2, 3, 7, 8, 10, 11], "uri": [0, 9], "gener": [0, 2, 4, 6, 9, 11], "download": [0, 9, 11], "v0": [0, 7, 10], "22": [0, 8, 9, 11], "33": [0, 11], "44": [0, 11], "55": [0, 11], "13": 0, "03": 0, "27": 0, "127": [0, 6, 7, 9], "12a83ff3": 0, "2de2": 0, "4a95": 0, "8f3f": 0, "c7a884e426e5": 0, "last_access": 0, "datetim": 0, "last": [0, 8], "report": [0, 6, 8, 10, 11], "mac_addr": [0, 11], "capabl": [0, 9, 12], "bool": [0, 8], "exec_cmd": 0, "fals": [0, 5, 6, 7, 8], "file_transf": 0, "true": [0, 5, 6, 8, 12], "shell_connect": 0, "null": 0, "mac_address": 0, "loopback": 0, "dummy_devic": 0, "auth": [0, 7, 9, 11], "via": [0, 1, 2, 7, 8, 11], "signatur": [0, 11], "yet": 0, "public_kei": [0, 11], "rsa": [0, 8, 11], "pem": [0, 8, 11], "format": [0, 1, 6, 8, 11], "newlin": 0, "charact": 0, "escap": 0, "timestamp": [0, 11], "posix": 0, "time": [0, 6, 8, 9, 11], "make": [0, 4, 5, 6, 7, 8, 9, 11], "x": [0, 11], "fgacvvz4cfc0np9z8qneuf8jnae7y8v532fntwmjkwkyt6shj0htigggxfgac1momi": 0, "9xmnwv2aqlguxbzcjs0yf1": 0, "pyxg3gyf8mt47": 0, "axbt4": 0, "mj8j": 0, "8eb2qxbb9tkwzicga": 0, "lkevxszword6l4wnwueqfa": 0, "jgwztloyxsidz0": 0, "dummi": 0, "begin": [0, 8], "nmigfma0gcsqgsib3dqebaquaa4gnadcbiqkbgqcvqdgcafyxuqlfophywhfv4oql": 0, "n2p3lwhm5ag9xmy2ylvqu2r9egnwkdxttenl81s6u": 0, "4cdfnmbuuimoedmazqski": 0, "n3ftou4": 0, "frqahf7t3omkng5mnhcaqbyq6wax": 0, "hrxfvj7lr38qljxgslgr3js3m0k": 0, "nb91ogffwua7i67bzywidaqab": 0, "n": [0, 3], "end": [0, 9, 10], "1694414456": 0, "300": 0, "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9": 0, "eyjkzxzpy2vfawqioiiwmdowmdowmdowmdowmdowmcisimnyzwf0zwqioje2otq0mtq0ntysimv4cglyzxmiojmwmh0": 0, "cg37rta1nib8nhokqi0ryvdkzj_0erpwweeqawu4iy": 0, "pend": [0, 7], "registr": [0, 7], "been": 0, "last_appear": 0, "made": [0, 7, 9, 11], "wed": 0, "sep": 0, "49": 0, "nmigfma0gcsqgsib3dqebaquaa4gnadcbiqkbgqcdbgmi": 0, "fgkb17bcxr99lef1nof": 0, "njwqapcipnbww": 0, "s3n6c937rgkinh0vkhmjcs3hrf2ku6": 0, "knjj4uxrztbwubpop4bp": 0, "nbk": 0, "hryvw9di6hthr042w7fxizu3howcf68qqnumg": 0, "5xmqwdsuch1gmrv8cuu21vz": 0, "nqazvf08uwzcueqjw5qidaqab": 0, "incom": [0, 9, 11], "As": [0, 5, 10], "result": [0, 3, 8, 10], "next": [0, 8, 9, 10], "attempt": [0, 8, 10, 11], "introduct": 1, "system": [1, 2, 4, 6, 7, 8, 9, 10], "architectur": [1, 2], "rest": [1, 11], "api": [1, 2, 3, 5, 7, 11], "devic": [1, 2, 4, 9], "server": [1, 2, 7, 8, 10], "protocol": [1, 7, 8, 9], "linux": [1, 2, 4, 10], "start": [1, 3, 5, 9, 11], "instal": [1, 2, 3, 4, 5, 8, 10, 11], "sourc": [1, 2], "build": [1, 2, 9], "docker": [1, 5], "integr": [1, 2, 4, 8, 9], "demo": 1, "develop": 1, "guid": [1, 2, 8, 9], "android": [1, 2], "app": [1, 2, 8, 11], "intent": 1, "mcumgr": [1, 2], "up": [1, 5, 6, 7, 10], "util": [1, 2, 6, 9, 10, 11, 12], "basic": [1, 2, 6, 10], "usag": [1, 10], "manag": [1, 2, 5, 6, 8, 12], "wheel": 1, "environ": [1, 3, 5, 6, 7], "variabl": [1, 3, 5, 6], "packag": [1, 2, 3, 4, 6, 8, 11, 12], "locat": [1, 5, 6, 7], "authent": [1, 5, 7, 8, 12], "concept": [1, 2], "updat": [1, 2, 4, 5, 6, 7, 9, 12], "resolut": 1, "downgrad": 1, "sequenti": 1, "delta": [1, 9], "updatess": 1, "flow": [1, 7, 9], "websocket": [1, 9, 12], "frontend": [1, 2, 10], "refer": [1, 2, 3, 8, 9, 11, 12], "error": [1, 6, 8, 11], "handl": [1, 6, 8, 9, 11, 12], "group": [1, 9], "legaci": 1, "author": [1, 3, 6, 9, 11], "rdfm": 2, "remot": [2, 6, 9, 12], "fleet": [2, 10], "open": [2, 3, 9], "ecosystem": [2, 9], "tool": [2, 4, 5, 8], "enabl": [2, 4, 5, 6, 8, 9], "over": [2, 6, 9, 11, 12], "air": [2, 6, 9], "deliveri": 2, "embed": [2, 4, 6, 8], "main": [2, 3, 6, 8, 11], "compon": 2, "It": [2, 3, 4, 6, 8, 9], "divid": 2, "chapter": [2, 9, 10, 11, 12], "short": 2, "overview": 2, "how": [2, 5, 8, 9, 10], "interact": [2, 10, 11], "instruct": [2, 7, 10], "zephyrrto": [2, 8], "mgmt": [2, 3, 5, 7, 8, 9], "comprehens": [2, 9], "introduc": 2, "explain": 2, "principl": 2, "function": [3, 4, 6, 8, 9, 11, 12], "implement": [3, 8, 11], "meant": [3, 9], "built": [3, 5, 8, 9], "separ": [3, 4, 5, 9, 11], "e": [3, 8], "studio": 3, "tree": 3, "updateengin": 3, "perform": [3, 6, 8, 9, 12], "actual": 3, "some": [3, 8], "root": [3, 5, 6, 9], "aosp": 3, "after": [3, 9, 10, 11], "clone": [3, 4, 6, 7, 8, 9], "repositori": [3, 4, 5, 6, 7, 8, 9], "mkdir": 3, "v": [3, 6], "p": 3, "vendor": 3, "antmicro": [3, 4, 6, 7, 8, 9], "cd": [3, 4, 6, 7, 8, 9], "cp": 3, "r": [3, 6, 8], "src": [3, 11], "To": [3, 4, 5, 6, 7, 8, 9, 11], "do": [3, 6, 8, 9], "product_packag": 3, "afterward": [3, 7], "usual": 3, "procedur": 3, "just": [3, 4], "mma": 3, "sign": [3, 4, 9, 11], "apk": 3, "out": 3, "ca": [3, 6, 7, 9], "certif": [3, 6, 7, 9], "when": [3, 4, 5, 6, 8, 9, 10, 11], "valid": [3, 8, 9, 11], "custom": [3, 9, 10, 11], "retriev": 3, "properti": [3, 8, 10, 11], "ro": 3, "increment": 3, "extract": [3, 4], "argument": [3, 4, 6, 10], "zip": 3, "you": [3, 4, 5, 6, 7, 8, 9], "unzip": 3, "automat": [3, 6], "boot": [3, 6, 8], "conf": [3, 6, 8], "xml": 3, "re": [3, 7, 9], "folder": [3, 9], "similar": 3, "utf": [3, 12], "8": [3, 12], "overlai": [3, 8], "default_rdfm_server_address": 3, "6000": 3, "likewis": 3, "interv": [3, 6, 8], "done": [3, 5, 7, 9, 11], "similarli": [3, 9], "default_update_check_interval_second": 3, "240": 3, "note": [3, 7], "These": [3, 6, 8], "startup": [3, 6], "them": [3, 6, 8, 9, 11], "onc": [3, 5, 6, 11], "newli": [3, 9, 10], "provis": 3, "possibl": [3, 4, 6], "simpli": [3, 11], "drawer": 3, "select": [3, 7, 9], "context": 3, "menu": 3, "url": [3, 5, 6, 7, 8, 9, 11], "scheme": [3, 4, 6], "second": [3, 6], "maximum": [3, 6, 8, 9], "amount": [3, 9], "concurr": 3, "shell": [3, 7, 12], "session": [3, 9, 11], "revers": [3, 9, 11], "forc": 3, "outsid": [3, 9], "want": [3, 8], "com": [3, 4, 6, 7, 8, 9], "update_check": 3, "its": [3, 4, 5, 6, 7, 8, 9, 10, 11], "androidmanifest": 3, "like": [3, 4, 5, 7, 8, 9, 12], "so": [3, 4, 5, 7], "configint": 3, "startupd": 3, "mcontext": 3, "sendbroadcast": 3, "order": [3, 4, 6, 8, 10], "between": [3, 4, 6, 8, 10, 11], "differ": [3, 4, 7, 8, 9, 10], "step": [3, 10], "configurationset": 3, "extra": 3, "wish": [3, 8], "putextra": 3, "ota_server_address": 3, "support": [3, 4, 6, 8, 9, 10, 11], "preference_": 3, "prefix": 3, "asid": 3, "configurationget": 3, "now": [3, 9, 10], "listen": [3, 9], "configurationrespons": 3, "broadcast": 3, "bundl": 3, "gradl": 3, "purpos": [3, 4, 6, 9], "class": 3, "thei": [3, 6, 8], "prevent": 3, "regular": 3, "howev": [3, 4, 9, 11], "buildabl": 3, "aforement": 3, "adb": 3, "With": [3, 4], "am": 3, "stop": [3, 8, 9], "mainact": 3, "view": [3, 10], "logcat": 3, "pid": 3, "pidof": 3, "easi": [4, 6], "modif": [4, 8, 9], "partit": [4, 6, 8, 10], "consist": [4, 10, 11, 12], "well": [4, 5, 7], "checksum": 4, "certain": [4, 10, 11], "rather": [4, 8], "itself": [4, 8, 9, 10, 11], "reduc": 4, "size": [4, 8], "improv": 4, "effici": 4, "process": [4, 6, 8, 10], "combin": 4, "board": [4, 8], "act": [4, 8], "logic": [4, 8], "without": [4, 6, 8, 9, 12], "need": [4, 6, 8], "whole": 4, "robust": [4, 6, 9], "rollback": [4, 6, 8], "proper": [4, 6, 9, 10], "layout": [4, 6], "bootload": [4, 6], "b": [4, 6, 8], "your": [4, 6, 8, 9], "yocto": [4, 6], "project": [4, 5, 6], "recommend": [4, 5, 6, 8, 9], "meta": [4, 6], "layer": [4, 6], "bsp": [4, 6], "go": [4, 6, 8, 9], "compil": [4, 6, 8], "c": [4, 6, 8, 9], "liblzma": [4, 6, 8], "dev": [4, 5, 6, 8, 9], "libglib2": [4, 6], "git": [4, 6, 7, 8, 9], "github": [4, 6, 7, 8, 9], "subcommand": [4, 7], "command": [4, 5, 6, 7, 8, 9], "help": [4, 7, 8, 9], "h": [4, 7, 8, 12], "show": [4, 7, 8], "my": 4, "output": [4, 6, 8, 11], "top": 4, "word": 4, "ll": 4, "bin": [4, 7, 8], "individu": [4, 12], "ident": [4, 9, 10], "respect": [4, 10], "ouptput": 4, "instead": [4, 6, 8, 11], "apart": 4, "dest": 4, "dir": [4, 6], "destin": 4, "where": [4, 8, 9, 12], "determin": [4, 6, 8, 9], "whether": [4, 8, 9, 11], "backup": 4, "origin": [4, 5, 9], "tmp": [4, 9], "extens": [4, 9], "txt": [4, 8], "makefil": 4, "abl": 5, "render": 5, "commun": [5, 8, 11], "through": [5, 8], "poll": [5, 6, 8, 11], "dynam": 5, "detect": 5, "ui": 5, "accordingli": [5, 11], "simultan": 5, "sure": [5, 7, 8, 9], "detail": [5, 7, 8, 9, 11], "send": [5, 11], "env": 5, "vite_server_url": 5, "rdfm_disable_encrypt": [5, 9], "rdfm_disable_api_auth": [5, 9], "befor": [5, 7, 8, 9, 11], "npm": 5, "static": [5, 9], "dist": [5, 9], "alongsid": [5, 8], "vite_rdfm_backend": 5, "backend": [5, 9], "rdfm_include_frontend_endpoint": [5, 9], "compos": [5, 6, 9], "consequ": [5, 9], "serv": [5, 6, 9], "mai": [5, 7, 9, 10, 11, 12], "deploi": [5, 9], "independ": 5, "rdfm_enable_cor": [5, 9], "cor": [5, 9], "rdfm_frontend_app_url": [5, 9], "redirect": [5, 9], "cross": 5, "vite": 5, "featur": [5, 8], "hot": 5, "modul": 5, "replac": [5, 8], "section": [5, 7, 9], "That": 5, "behavior": 5, "vite_login_url": 5, "oidc": 5, "login": [5, 9], "vite_logout_url": 5, "logout": [5, 9], "vite_oauth2_cli": 5, "oauth2": [5, 7, 9], "prettier": 5, "background": 6, "along": [6, 7, 9], "maintain": [6, 9, 12], "u": 6, "libssl": [6, 8], "Then": [6, 8, 9], "wai": [6, 8, 10], "offer": 6, "complet": [6, 8, 9, 12], "platform": 6, "dual": 6, "setup": [6, 9], "includ": [6, 8, 9], "dedic": [6, 9], "dockerfil": [6, 9], "sudo": [6, 7], "t": [6, 8, 9, 11], "rdfmbuilder": 6, "later": 6, "rm": 6, "etc": [6, 8], "interest": 6, "var": 6, "lib": 6, "high": [6, 9], "level": [6, 7], "overlaid": 6, "number": 6, "retri": [6, 8, 11], "ssl": 6, "cach": [6, 9], "reconnect": 6, "count": 6, "log": [6, 8, 9], "entri": [6, 8], "sent": [6, 11, 12], "fifti": 6, "logger": 6, "lai": 6, "under": [6, 8, 9], "execut": [6, 7, 9, 11], "establish": [6, 7, 9, 11], "invok": 6, "predefin": [6, 8], "captur": 6, "flexibl": 6, "mechan": 6, "collect": 6, "runtim": 6, "dictionari": 6, "consid": [6, 8, 10], "rfc": [6, 9, 11], "email": 6, "1000": 6, "sinc": [6, 8], "give": 6, "capac": 6, "arbitrari": [6, 10], "rw": 6, "denot": 6, "uniqu": [6, 8, 11], "overwrit": 6, "counterpart": [6, 8], "ran": 6, "millisecond": 6, "kill": 6, "timeout": 6, "often": 6, "necessari": 6, "f": [6, 9], "yml": [6, 9], "rdfm_client_server_url": 6, "rdfm_client_server_cert": 6, "verif": [6, 8], "rdfm_client_devtyp": 6, "advertis": [6, 8, 11], "x86_64": [6, 7], "rdfm_client_part_a": 6, "rdfm_client_part_b": 6, "zero": 6, "unit": [6, 8], "script": [6, 8, 9], "proceed": 7, "pipx": 7, "debian": 7, "bookworm": 7, "apt": 7, "arch": [7, 8], "pacman": 7, "prefer": [7, 8, 9], "mode": [7, 11], "virtual": 7, "home": [7, 8, 9], "venv": 7, "immedi": [7, 11], "config": [7, 8, 9], "credenti": [7, 9], "cli": 7, "flag": [7, 8, 9], "keycloak": 7, "auth_url": 7, "8080": [7, 9], "realm": [7, 9], "master": [7, 9], "openid": [7, 9], "client_id": [7, 9], "client_secret": [7, 9], "rdswdyumot7uxxmqmmq2y4vq1ezxqobi": 7, "explan": [7, 10], "field": [7, 9, 10, 11, 12], "secret": [7, 9], "pleas": [7, 8, 9], "introspect": [7, 9], "see": [7, 9, 10], "poetri": 7, "messag": [7, 11, 12], "exit": 7, "cert": [7, 9], "crt": [7, 9], "veri": 7, "long": 7, "mani": [7, 8, 10], "package1": 7, "package2": 7, "clear": 7, "smp": 8, "serial": 8, "udp": 8, "ble": 8, "transport": 8, "proxi": [8, 9], "push": 8, "down": 8, "correct": [8, 10], "properli": 8, "both": [8, 9, 10], "correctli": 8, "work": [8, 9], "method": [8, 9, 11, 12], "choic": 8, "reboot": 8, "come": 8, "reliabl": 8, "mcuboot": 8, "written": 8, "secondari": 8, "flash": 8, "while": [8, 10], "leav": 8, "primari": 8, "intact": 8, "swap": 8, "success": [8, 10], "perman": [8, 11], "back": [8, 9], "restor": 8, "previou": [8, 9], "offici": 8, "websit": 8, "imgtool": 8, "py": [8, 11], "zephyrproject": 8, "workspac": 8, "pip3": 8, "keygen": 8, "k": 8, "2048": 8, "3072": 8, "ecdsa": 8, "p256": 8, "ed25519": 8, "document": [8, 9], "besid": 8, "devicetre": 8, "boot_partit": 8, "slot0_partit": 8, "priamri": 8, "slot": 8, "slot1_partit": 8, "choos": 8, "scratch": 8, "algorithm": 8, "scratch_partit": 8, "those": [8, 9], "look": [8, 11, 12], "dt": 8, "fix": 8, "doesn": 8, "would": [8, 9], "directli": [8, 9, 11, 12], "sampl": 8, "stm32f746g_disco": 8, "mem": 8, "node": [8, 10], "quadspi": 8, "flash0": 8, "cell": 8, "label": 8, "reg": 8, "0x00000000": 8, "dt_size_k": 8, "64": 8, "40000": 8, "0x00040000": 8, "256": [8, 11], "80000": 8, "0x00080000": 8, "c0000": 8, "0x000c0000": 8, "alias": 8, "spi": 8, "chosen": 8, "boot_signature_key_fil": 8, "previous": [8, 11], "boot_signature_typ": 8, "boot_signature_type_rsa": 8, "boot_signature_type_rsa_len": 8, "boot_signature_type_ecdsa_p256": 8, "boot_signature_type_ed25519": 8, "boot_image_upgrade_mod": 8, "boot_swap_using_mov": 8, "boot_swap_using_scratch": 8, "stm32_disco": 8, "west": 8, "d": 8, "ddtc_overlay_fil": 8, "dconfig_boot_signature_keyfil": 8, "dconfig_boot_signature_type_rsa": 8, "y": 8, "dconfig_boot_signature_type_rsa_len": 8, "dconfig_boot_swap_using_scratch": 8, "produc": 8, "subsystem": 8, "o": 8, "learn": 8, "smp_svr": 8, "mcuboot_bootloader_mod": 8, "ve": 8, "mcuboot_bootloader_mode_swap_without_scratch": 8, "mcuboot_bootloader_mode_swap_scratch": 8, "bluetooth": 8, "smb": 8, "subsi": 8, "dextra_conf_fil": 8, "dconfig_mcuboot_bootloader_mode_swap_scratch": 8, "hex": 8, "ship": 8, "togeth": 8, "major": 8, "minor": 8, "revis": 8, "final": [8, 9], "mcuboot_signature_key_fil": 8, "mcuboot_imgtool_sign_vers": 8, "here": [8, 9], "dconfig_mcuboot_signature_key_fil": 8, "dconfig_imgtool_sign_vers": 8, "4": 8, "yourself": 8, "unsign": 8, "insert": 8, "g": 8, "might": 8, "network": [8, 9, 11], "stack": 8, "misconfigur": 8, "kind": 8, "watch": 8, "mark": 8, "decis": 8, "fall": [8, 9], "fresh": 8, "caus": [8, 9], "revert": 8, "dfu": 8, "sy": 8, "void": 8, "run_self_test": 8, "boot_is_img_confirm": 8, "goe": 8, "sys_reboot": 8, "sys_reboot_cold": 8, "boot_write_img_confirm": 8, "overrid": 8, "overwritten": 8, "full": 8, "key_dir": 8, "rel": 8, "absolut": 8, "update_interv": 8, "suffix": 8, "m": 8, "mean": 8, "limit": [8, 9, 11], "displai": 8, "device_typ": 8, "privat": [8, 9, 11], "self_confirm": 8, "global": 8, "member": 8, "device_index": 8, "index": 8, "hci0": 8, "peer_nam": 8, "config_bt_device_nam": 8, "ttyusb0": 8, "tty": 8, "usbseri": 8, "baud": 8, "speed": [8, 9], "baudrat": 8, "mtu": 8, "transmiss": 8, "packet": 8, "ipv4": 8, "ipv6": 8, "port": [8, 9, 12], "ip": [8, 9], "perspect": 8, "synchron": 8, "exact": 8, "tri": 8, "went": 8, "roll": 8, "localhost": [8, 9], "dev_typ": 8, "zeph": 8, "15": 8, "test0": 8, "ser": 8, "ttyacm0": 8, "115200": 8, "128": 8, "gr1": 8, "group1": 8, "udpl": 8, "left": [8, 9], "192": 8, "168": 8, "1337": 8, "udpr": 8, "right": [8, 9], "bleh": 8, "ble_head": 8, "own": [8, 9], "find": 8, "correspond": [8, 9, 10], "save": [8, 9], "0600": 8, "core": 9, "grant": 9, "crucial": 9, "secur": [9, 11, 12], "world": 9, "suitabl": 9, "latest": 9, "restart": 9, "unless": 9, "rdfm_jwt_secret": 9, "replace_with_custom_jwt_secret": 9, "rdfm_db_connstr": 9, "sqlite": 9, "databas": [9, 11], "db": 9, "rdfm_hostnam": 9, "rdfm_api_port": 9, "rdfm_local_package_dir": 9, "rdfm_wsgi_serv": 9, "werkzeug": 9, "volum": 9, "pkg": 9, "issu": 9, "jwt": 9, "kept": 9, "easili": 9, "guessabl": 9, "random": 9, "hexadecim": 9, "sqlalchemi": 9, "postgresql": 9, "engin": 9, "were": 9, "verifi": [9, 11], "front": 9, "freeli": 9, "wsgi": 9, "hostnam": 9, "construct": 9, "rdfm_server_cert": 9, "mount": 9, "certgen": 9, "sh": 9, "rdfm_server_kei": 9, "gunicorn": 9, "readi": 9, "rdfm_wsgi_max_connect": 9, "worker": 9, "persist": [9, 11, 12], "4000": 9, "know": 9, "omit": 9, "rdfm_oauth_url": 9, "7662": 9, "rdfm_login_url": 9, "page": 9, "rdfm_logout_url": 9, "revok": [9, 11], "rdfm_oauth_client_id": 9, "rdfm_oauth_client_sec": 9, "rdfm_storage_driv": 9, "rdfm_s3_bucket": 9, "bucket": 9, "rdfm_s3_access_key_id": 9, "rdfm_s3_access_secret_kei": 9, "temporari": 9, "desir": 9, "sever": 9, "NO": 9, "neg": 9, "affect": 9, "few": 9, "solut": 9, "minio": 9, "debug": 9, "circumst": 9, "restrict": 9, "extern": 9, "task": 9, "deleg": 9, "conjunct": 9, "rule": 9, "admin": 9, "breakdown": 9, "replace_with_rdfm_introspection_secret": 9, "quai": 9, "io": 9, "keycloak_admin": 9, "keycloak_admin_password": 9, "opt": 9, "theme": 9, "further": 9, "navig": 9, "consol": 9, "initi": 9, "press": 9, "anyth": 9, "On": [9, 11], "account": 9, "role": 9, "tab": 9, "copi": 9, "rememb": 9, "ctrl": 9, "four": 9, "against": [9, 10], "repres": [9, 11], "click": 9, "fill": 9, "usernam": 9, "password": 9, "map": 9, "There": 9, "button": 9, "implicit": 9, "standard": [9, 11], "panel": 9, "infer": 9, "most": [9, 10], "equal": 9, "dropdown": 9, "explicitli": 9, "particular": 9, "trust": 9, "curl": 9, "cacert": 9, "behind": 9, "edg": [9, 10], "workload": 9, "percentag": 9, "drop": 9, "heavi": 9, "load": 9, "adequ": 9, "compar": 9, "aspect": 9, "practic": 9, "simplic": 9, "pre": 9, "pretti": 9, "much": 9, "everywher": 9, "never": [9, 11], "prior": 9, "monorepositori": 9, "brief": 10, "entiti": [10, 11], "activ": 10, "blob": 10, "enforc": 10, "mandatori": [10, 11, 12], "filter": 10, "search": 10, "incompat": 10, "claus": 10, "complex": 10, "involv": 10, "intermedi": 10, "syntax": 10, "treat": 10, "version1": 10, "bring": 10, "resolv": 10, "graph": 10, "reach": 10, "queri": 10, "shortest": 10, "lead": 10, "p0": 10, "foo": [10, 11], "p1": 10, "bar": 10, "p2": 10, "baz": 10, "v3": 10, "per": 10, "d0": 10, "d1": [10, 12], "d2": [10, 12], "shall": [10, 11], "v4": 10, "v5": 10, "becom": 10, "candid": 10, "e6e2531": 10, "2f646ac": 10, "6d9aee4": 10, "themselv": [10, 11], "wide": 10, "rang": 10, "variou": 11, "1694681536": 11, "payload": 11, "byte": 11, "pkc": 11, "sha": 11, "digest": 11, "rsassa": 11, "pkcs1": 11, "v1_5": 11, "8017": 11, "calcul": 11, "attach": 11, "base64": 11, "wasn": 11, "gracefulli": 11, "period": 11, "assumpt": 11, "usabl": 11, "defens": 11, "approach": 11, "reauthent": 11, "regularli": 11, "transient": 11, "temperatur": 11, "sensor": 11, "advis": 11, "three": 11, "subset": 11, "interfac": 11, "hash": 11, "sane": 11, "impos": 11, "transfer": 11, "handshak": 11, "exactli": 11, "w": 11, "capabilityreport": 11, "notifi": 11, "orient": 11, "method_nam": 11, "arg0": 11, "arg1": 11, "unspecifi": 11, "common": 11, "request_model": 11, "what": 11, "assum": 11, "spawn": 11, "shell_attach": 11, "react": 11, "input": 11, "raw": 11, "central": 12, "visual": 12, "summari": 12, "internet": 12, "distinguish": 12, "capability_report": 12, "alert": 12}, "objects": {"": [[0, 0, 1, "post--api-v1-auth-device", "/api/v1/auth/device", ""], [0, 1, 1, "get--api-v1-auth-pending", "/api/v1/auth/pending", ""], [0, 0, 1, "post--api-v1-auth-register", "/api/v1/auth/register", ""], [0, 1, 1, "get--api-v1-devices", "/api/v1/devices", ""], [0, 1, 1, "get--api-v1-devices-(int-identifier)", "/api/v1/devices/(int:identifier)", ""], [0, 1, 1, "get--api-v1-groups", "/api/v1/groups", ""], [0, 0, 1, "post--api-v1-groups", "/api/v1/groups", ""], [0, 2, 1, "delete--api-v1-groups-(int-identifier)", "/api/v1/groups/(int:identifier)", ""], [0, 1, 1, "get--api-v1-groups-(int-identifier)", "/api/v1/groups/(int:identifier)", ""], [0, 3, 1, "patch--api-v1-groups-(int-identifier)-devices", "/api/v1/groups/(int:identifier)/devices", ""], [0, 0, 1, "post--api-v1-groups-(int-identifier)-package", "/api/v1/groups/(int:identifier)/package", ""], [0, 0, 1, "post--api-v1-groups-(int-identifier)-policy", "/api/v1/groups/(int:identifier)/policy", ""], [0, 1, 1, "get--api-v1-packages", "/api/v1/packages", ""], [0, 0, 1, "post--api-v1-packages", "/api/v1/packages", ""], [0, 2, 1, "delete--api-v1-packages-(int-identifier)", "/api/v1/packages/(int:identifier)", ""], [0, 1, 1, "get--api-v1-packages-(int-identifier)", "/api/v1/packages/(int:identifier)", ""], [0, 0, 1, "post--api-v1-update-check", "/api/v1/update/check", ""], [0, 1, 1, "get--api-v2-devices", "/api/v2/devices", ""], [0, 1, 1, "get--api-v2-devices-(int-identifier)", "/api/v2/devices/(int:identifier)", ""], [0, 1, 1, "get--api-v2-groups", "/api/v2/groups", ""], [0, 0, 1, "post--api-v2-groups", "/api/v2/groups", ""], [0, 2, 1, "delete--api-v2-groups-(int-identifier)", "/api/v2/groups/(int:identifier)", ""], [0, 1, 1, "get--api-v2-groups-(int-identifier)", "/api/v2/groups/(int:identifier)", ""], [0, 3, 1, "patch--api-v2-groups-(int-identifier)-devices", "/api/v2/groups/(int:identifier)/devices", ""], [0, 0, 1, "post--api-v2-groups-(int-identifier)-package", "/api/v2/groups/(int:identifier)/package", ""], [0, 0, 1, "post--api-v2-groups-(int-identifier)-policy", "/api/v2/groups/(int:identifier)/policy", ""], [0, 0, 1, "post--api-v2-groups-(int-identifier)-priority", "/api/v2/groups/(int:identifier)/priority", ""], [0, 1, 1, "get--local_storage-(path-name)", "/local_storage/(path:name)", ""]]}, "objtypes": {"0": "http:post", "1": "http:get", "2": "http:delete", "3": "http:patch"}, "objnames": {"0": ["http", "post", "HTTP post"], "1": ["http", "get", "HTTP get"], "2": ["http", "delete", "HTTP delete"], "3": ["http", "patch", "HTTP patch"]}, "titleterms": {"rdfm": [0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "server": [0, 3, 5, 6, 9, 11, 12], "api": [0, 9, 12], "refer": 0, "authent": [0, 9, 11], "error": 0, "handl": 0, "packag": [0, 7, 9, 10], "group": [0, 4, 7, 8, 10], "legaci": 0, "updat": [0, 3, 8, 10, 11], "devic": [0, 3, 6, 7, 8, 10, 11, 12], "manag": [0, 7, 9, 11], "author": [0, 7], "document": 1, "introduct": [2, 3, 4, 5, 6, 7, 8, 9], "android": 3, "client": [3, 6, 8, 9], "integr": [3, 6, 11], "app": 3, "copi": 3, "sourc": [3, 4, 6, 8], "configur": [3, 5, 6, 7, 8, 9], "makefil": 3, "build": [3, 4, 5, 6, 7, 8], "us": [3, 6, 9], "http": [3, 9, 12], "request": 3, "system": [3, 12], "version": [3, 7], "time": 3, "runtim": 3, "option": 3, "avail": [3, 7], "intent": 3, "check": [3, 11], "extern": 3, "via": [3, 9], "develop": [3, 5, 6, 9], "restart": 3, "fetch": 3, "log": 3, "artifact": 4, "util": [4, 7], "get": [4, 6, 8], "start": [4, 6, 8], "from": [4, 6, 8], "requir": [4, 6, 8], "step": [4, 6, 8], "basic": [4, 9], "usag": [4, 7], "creat": [4, 7], "full": 4, "rootf": 4, "delta": [4, 10], "zephyr": [4, 8], "mcuboot": 4, "singl": 4, "file": 4, "run": [4, 5, 6, 9], "test": [4, 6], "frontend": [5, 9], "applic": [5, 8, 9], "format": 5, "linux": 6, "instal": [6, 7], "note": 6, "docker": [6, 9], "default": 6, "config": 6, "rootfsparta": 6, "string": 6, "rootfspartb": 6, "overlai": 6, "devicetypefil": 6, "updatepollintervalsecond": 6, "int": 6, "retrypollintervalsecond": 6, "servercertif": 6, "serverurl": 6, "httpcacheen": 6, "bool": 6, "reconnectretrycount": 6, "reconnectretrytim": 6, "telemetryen": 6, "telemetrybatchs": 6, "telemetri": 6, "name": 6, "path": 6, "arg": 6, "tick": 6, "demo": [6, 9], "guid": 6, "wheel": 7, "list": 7, "resourc": 7, "upload": 7, "delet": 7, "assign": [7, 10], "set": [7, 8, 9], "": 7, "target": [7, 8], "mcumgr": 8, "up": [8, 9], "bootload": 8, "gener": 8, "imag": 8, "sign": 8, "kei": [8, 10], "automat": 8, "manual": [8, 10], "self": 8, "confirm": 8, "search": 8, "locat": [8, 9], "valu": 8, "exampl": [8, 9, 10], "rest": [9, 12], "environ": 9, "variabl": 9, "storag": 9, "store": 9, "local": 9, "s3": 9, "compat": 9, "keycloak": 9, "servic": 9, "ad": 9, "an": 9, "user": 9, "deploy": 9, "product": 9, "consider": 9, "ota": 10, "concept": 10, "polici": 10, "resolut": 10, "scenario": 10, "simpl": 10, "downgrad": 10, "sequenti": 10, "updatess": 10, "flow": 11, "websocket": 11, "protocol": [11, 12], "capabl": 11, "shell": 11, "architectur": 12}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.todo": 2, "sphinx": 58}, "alltitles": {"RDFM Server API Reference": [[0, "rdfm-server-api-reference"]], "API Authentication": [[0, "api-authentication"]], "Error Handling": [[0, "error-handling"]], "Packages API": [[0, "packages-api"]], "Group API": [[0, "group-api"]], "Group API (legacy)": [[0, "group-api-legacy"]], "Update API": [[0, "update-api"]], "Device Management API": [[0, "device-management-api"]], "Device Management API (legacy)": [[0, "device-management-api-legacy"]], "Device Authorization API": [[0, "device-authorization-api"]], "RDFM Documentation": [[1, "rdfm-documentation"]], "Introduction": [[2, "introduction"], [3, "introduction"], [4, "introduction"], [5, "introduction"], [6, "introduction"], [7, "introduction"], [8, "introduction"], [9, "introduction"]], "RDFM Android Device Client": [[3, "rdfm-android-device-client"]], "Integrating the app": [[3, "integrating-the-app"]], "Copying the sources": [[3, "copying-the-sources"]], "Configuring the device Makefile": [[3, "configuring-the-device-makefile"]], "Building the app": [[3, "building-the-app"]], "Using HTTPS for server requests": [[3, "using-https-for-server-requests"]], "System versioning": [[3, "system-versioning"]], "Configuring the app": [[3, "configuring-the-app"]], "Build-time app configuration": [[3, "build-time-app-configuration"]], "Runtime app configuration": [[3, "runtime-app-configuration"]], "Configuration options": [[3, "configuration-options"]], "Available intents": [[3, "available-intents"]], "Update check intent": [[3, "update-check-intent"]], "External configuration via intents": [[3, "external-configuration-via-intents"]], "Development": [[3, "development"]], "Restarting the app": [[3, "restarting-the-app"]], "Fetching app logs": [[3, "fetching-app-logs"]], "RDFM Artifact utility": [[4, "rdfm-artifact-utility"]], "Getting started": [[4, "getting-started"], [6, "getting-started"], [8, "getting-started"]], "Building from source": [[4, "building-from-source"]], "Requirements": [[4, "requirements"], [6, "requirements"], [8, "requirements"]], "Steps": [[4, "steps"], [6, "steps"], [8, "steps"]], "Basic usage": [[4, "basic-usage"]], "Creating a full-rootfs artifact": [[4, "creating-a-full-rootfs-artifact"]], "Creating a delta rootfs artifact": [[4, "creating-a-delta-rootfs-artifact"]], "Creating a Zephyr MCUboot artifact": [[4, "creating-a-zephyr-mcuboot-artifact"]], "Creating a Zephyr MCUboot group artifact": [[4, "creating-a-zephyr-mcuboot-group-artifact"]], "Creating a single file artifact": [[4, "creating-a-single-file-artifact"]], "Running tests": [[4, "running-tests"], [6, "running-tests"]], "RDFM Frontend": [[5, "rdfm-frontend"]], "Building the application": [[5, "building-the-application"]], "Running development server": [[5, "running-development-server"]], "Configuration": [[5, "configuration"], [7, "configuration"]], "Formatting": [[5, "formatting"]], "RDFM Linux Device Client": [[6, "rdfm-linux-device-client"]], "Installing from source": [[6, "installing-from-source"]], "Installation notes": [[6, "installation-notes"]], "Building using Docker": [[6, "building-using-docker"]], "Configuring the client": [[6, "configuring-the-client"]], "RDFM default config": [[6, "rdfm-default-config"]], "RootfsPartA string": [[6, "rootfsparta-string"]], "RootfsPartB string": [[6, "rootfspartb-string"]], "RDFM overlay config": [[6, "rdfm-overlay-config"]], "DeviceTypeFile string": [[6, "devicetypefile-string"]], "UpdatePollIntervalSeconds int": [[6, "updatepollintervalseconds-int"]], "RetryPollIntervalSeconds int": [[6, "retrypollintervalseconds-int"]], "ServerCertificate string": [[6, "servercertificate-string"]], "ServerURL string": [[6, "serverurl-string"]], "HttpCacheEnabled bool": [[6, "httpcacheenabled-bool"]], "ReconnectRetryCount int": [[6, "reconnectretrycount-int"]], "ReconnectRetryTime int": [[6, "reconnectretrytime-int"]], "TelemetryEnable bool": [[6, "telemetryenable-bool"]], "TelemetryBatchSize int": [[6, "telemetrybatchsize-int"]], "RDFM telemetry config": [[6, "rdfm-telemetry-config"]], "name string": [[6, "name-string"]], "path string": [[6, "path-string"]], "args []string": [[6, "args-string"]], "tick int": [[6, "tick-int"]], "Testing server-device integration with a demo Linux device client": [[6, "testing-server-device-integration-with-a-demo-linux-device-client"]], "Developer Guide": [[6, "developer-guide"]], "RDFM Manager utility": [[7, "rdfm-manager-utility"]], "Installation": [[7, "installation"]], "Building the wheel": [[7, "building-the-wheel"]], "Usage": [[7, "usage"]], "Listing available resources": [[7, "listing-available-resources"]], "Uploading packages": [[7, "uploading-packages"]], "Deleting packages": [[7, "deleting-packages"]], "Creating groups": [[7, "creating-groups"]], "Deleting groups": [[7, "deleting-groups"]], "Assign package to a group": [[7, "assign-package-to-a-group"]], "Assign devices to a group": [[7, "assign-devices-to-a-group"]], "Setting a group\u2019s target version": [[7, "setting-a-group-s-target-version"]], "Authorizing a device": [[7, "authorizing-a-device"]], "RDFM MCUmgr Device Client": [[8, "rdfm-mcumgr-device-client"]], "Building client from source": [[8, "building-client-from-source"]], "Setting up target device": [[8, "setting-up-target-device"]], "Setting up the bootloader": [[8, "setting-up-the-bootloader"]], "Generating image signing key": [[8, "generating-image-signing-key"]], "Building the bootloader": [[8, "building-the-bootloader"]], "Setting up the Zephyr application": [[8, "setting-up-the-zephyr-application"]], "Building the image": [[8, "building-the-image"]], "Signing the image": [[8, "signing-the-image"]], "Automatically": [[8, "automatically"]], "Manually": [[8, "manually"]], "Self-confirmed updates": [[8, "self-confirmed-updates"]], "Configuring MCUmgr client": [[8, "configuring-mcumgr-client"]], "Search locations": [[8, "search-locations"]], "Configuration values": [[8, "configuration-values"]], "Device groups": [[8, "device-groups"]], "Example configuration": [[8, "example-configuration"]], "Device keys": [[8, "device-keys"]], "RDFM Management Server": [[9, "rdfm-management-server"]], "REST API": [[9, "rest-api"]], "Setting up a Dockerized development environment": [[9, "setting-up-a-dockerized-development-environment"]], "Configuration via environment variables": [[9, "configuration-via-environment-variables"]], "Configuring package storage location": [[9, "configuring-package-storage-location"]], "Storing packages locally": [[9, "storing-packages-locally"]], "Storing packages on S3-compatible storage": [[9, "storing-packages-on-s3-compatible-storage"]], "Configuring API authentication": [[9, "configuring-api-authentication"]], "Basic configuration": [[9, "basic-configuration"]], "API authentication using Keycloak": [[9, "api-authentication-using-keycloak"]], "Running the services": [[9, "running-the-services"]], "Keycloak configuration": [[9, "keycloak-configuration"]], "Adding an API client": [[9, "adding-an-api-client"]], "Adding a User": [[9, "adding-a-user"]], "Configuring frontend application": [[9, "configuring-frontend-application"]], "Configuring HTTPS": [[9, "configuring-https"]], "HTTPS demo deployment": [[9, "https-demo-deployment"]], "Production deployments": [[9, "production-deployments"]], "Production considerations": [[9, "production-considerations"]], "Production example deployment": [[9, "production-example-deployment"]], "RDFM OTA Manual": [[10, "rdfm-ota-manual"]], "Key concepts": [[10, "key-concepts"]], "Devices": [[10, "devices"]], "Packages": [[10, "packages"]], "Groups": [[10, "groups"]], "Update policy": [[10, "update-policy"]], "Update resolution": [[10, "update-resolution"]], "Example scenario: simple update assignment": [[10, "example-scenario-simple-update-assignment"]], "Example scenario: downgrades": [[10, "example-scenario-downgrades"]], "Example scenario: sequential updates": [[10, "example-scenario-sequential-updates"]], "Example scenario: delta updatess": [[10, "example-scenario-delta-updatess"]], "Server Integration flows": [[11, "server-integration-flows"]], "Device authentication": [[11, "device-authentication"]], "Device update check": [[11, "device-update-check"]], "Management WebSocket": [[11, "management-websocket"]], "RDFM Management Protocol": [[11, "rdfm-management-protocol"]], "Capabilities": [[11, "capabilities"]], "Capability - shell": [[11, "capability-shell"]], "System Architecture": [[12, "system-architecture"]], "HTTP REST API": [[12, "http-rest-api"]], "Device-server RDFM Protocol": [[12, "device-server-rdfm-protocol"]]}, "indexentries": {}, "docurls": ["api.html", "index.html", "introduction.html", "rdfm_android_device_client.html", "rdfm_artifact.html", "rdfm_frontend.html", "rdfm_linux_device_client.html", "rdfm_manager.html", "rdfm_mcumgr_device_client.html", "rdfm_mgmt_server.html", "rdfm_ota_manual.html", "server_operation.html", "system_overview.html"]}) \ No newline at end of file diff --git a/server_operation.html b/server_operation.html new file mode 100644 index 0000000..b282fe6 --- /dev/null +++ b/server_operation.html @@ -0,0 +1,699 @@ + + + + + + + + + + + + + + + + Server Integration flows - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

Server Integration flows

+

This chapter describes the various integration flows between device clients and the RDFM Management server.

+

Device authentication

+

At the start of their execution, all RDFM-compatible device clients shall authenticate with the server. +This shall be done by utilizing the /api/v1/auth/device endpoint. +For details on the request schema, refer to the Server API Reference chapter. +An example request made to this endpoint is shown below:

+
{
+    "metadata": {
+        "rdfm.hardware.devtype": "device-type",
+        "rdfm.software.version": "foo",
+        "rdfm.hardware.macaddr": "00:11:22:33:44:55",
+    }
+    "public_key": "<RSA public key of the device in PEM format>",
+    "timestamp": 1694681536,
+}
+
+
+

The JSON payload bytes must be signed by the device client with its securely stored RSA private key using PKCS #1 v1.5 signature with SHA-256 digest (function RSASSA-PKCS1-V1_5-SIGN defined in RFC 8017) +The calculated signature must then be attached, encoded as base64, to the authorization request in the header X-RDFM-Device-Signature. +If the server successfully validates the attached signature, the device will be registered in the server’s database, if it wasn’t previously registered already. +The device-specified MAC address is used as a unique identifier for this specific device.

+

Before the device is authorized to access the RDFM API, it must be accepted first by an administrative entity interacting via a separate API with the RDFM server. +If the device was not accepted, or its acceptation status was revoked, the above request shall fail with the 401 Unauthorized HTTP status code. The device client must handle this status code gracefully, for example by retrying the attempted request after a certain time has passed.

+

Once the device is accepted into the RDFM server, the above request shall return a device-specific app token, that can be used to interact with device-side API endpoints. +The app token is not permanent, and will expire after a certain time period. +The device client must not make any assumptions about the length of the usability period, and instead should take a defensive approach to any requests made to the device-side API and reauthenticate when a response with the 401 status code is received.

+

Device update check

+

Once authorized, a device client will have access to the device-side API of the RDFM server. +The device client is expected to regularly poll for updates by utilizing the /api/v1/update/check endpoint.

+

In the update check request, the device client must provide all of its local metadata. +The metadata, which consists of simple key/value pairs, uniquely describes the set of software and/or hardware present on the device, but may also represent other transient +properties not persisted in storage, such as temperature sensor values.

+

When making the update check, the device client is advised to provide all of its metadata to the server in the update request. +At the time of writing, below three metadata properties are mandatory and must be present in all update checks:

+
    +
  • rdfm.software.version - version identifier of the currently running software package

  • +
  • rdfm.hardware.devtype - device type, used for limiting package compatibility only to a subset of devices

  • +
  • rdfm.hardware.macaddr - MAC address of the device’s main network interface

  • +
+

For future compatibility, device clients are advised to provide all of their metadata, not only the mandatory keys, in the update check request. +For more details on the structure of an update check request, consult the Update API Reference

+

When a new package is available, the response shall be as described in the API Reference, and a one-time download URL to the package is generated. +The device client shall use this URL to download and install, or in the case of clients capable of stream installation, directly install the package. +The device client MUST verify the hash of the package as described in the update check response.

+

Additionally, the device client MUST verify whether the package contents look sane before attempting to install it. +The server shall never return a package that is not of the same device type as the one advertised by the client. +However, the server itself currently imposes no limitations on the binary contents of the packages themselves.

+

Management WebSocket

+

If supported, the device may also connect to a device management WebSocket. +This provides additional management functionality of registered devices, such as reverse shell and file transfer. +To connect to the WebSocket, a device token is required to be provided in the Authorization header of the WebSocket handshake. +The format of the header is exactly the same as in other device routes and is described in the API Reference chapter.

+

The general management flow is as follows:

+
    +
  1. Device connects to the management WebSocket: /api/v1/devices/ws

  2. +
  3. Device sends a CapabilityReport message indicating the capabilities it supports

  4. +
  5. Device reads incoming management messages from the server and handles them accordingly

  6. +
  7. Device may also send messages to the server to notify of certain situations

  8. +
+

RDFM Management Protocol

+

The management protocol is message-oriented and all messages are expected to be sent in WebSocket text mode. +Each message is a JSON object in the form:

+
{
+    "method": "<method_name>",
+    "arg0": "...",
+    "arg1": {"...": "..."},
+    "...": "..."
+}
+
+
+

The type of message sent is identified by the method field. +The rest of the object fields are unspecified and depend on the specific message type. +Schema for messages used by the server can be found in common/communication/src/request_models.py. +On error during handling of a request, the server may return a custom WebSocket status code. +A list of status codes used by the server can be found in common/communication/src/rdfm/ws.py.

+

Capabilities

+

A capability indicates what management functionality is supported by a device. +The device should report its capabilities using the CapabilityReport message immediately after connecting to the server. +By default it is assumed that the device does not provide any capabilities.

+

Capability - shell

+

This capability indicates that a device supports spawning a reverse shell. +The following methods must be supported by the device:

+
    +
  • shell_attach

  • +
+

A device with the shell capability should react to shell_attach messages by connecting to a shell WebSocket at /api/v1/devices/<shell_attach.mac_addr>/shell/attach/<shell_attach.uuid>. +This establishes a connection between the requesting manager and the device. +This WebSocket can then be used to stream the contents of the shell session and receive user input. +The format of messages sent over this endpoint is implementation defined. +However, generally the shell output/input are simply sent as binary WebSocket messages containing the standard output/input as raw bytes.

+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/system_overview.html b/system_overview.html new file mode 100644 index 0000000..1678fad --- /dev/null +++ b/system_overview.html @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + System Architecture - RDFM Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + +
+
+ + + +

System Architecture

+

The reference architecture of an RDFM system consists of:

+
    +
  • RDFM Management Server - handles device connections, packages, deployment, remote device management

  • +
  • Devices - devices connect to a central management server and utilize the exposed REST API and device-server RDFM protocol for providing remote management functionality

  • +
  • Users - individual users that are authenticated and allowed read-only/read-write access to resources exposed by the server

  • +
+

The system architecture can be visualized as follows:

+
+Architecture summary +
+

Figure 1 +Summary of the system architecture

+
+

HTTP REST API

+

For functionality not requiring a persistent connection, the server exposes an HTTP API. A complete list of available endpoints can be found +in the RDFM Server API Reference chapter. The clients use this API to perform update checks.

+

Device-server RDFM Protocol

+

The devices also maintain a persistent connection to the RDFM Management Server by utilizing JSON-based messages sent over a WebSocket route. +This is used to securely expose additional management functionality without directly exposing device ports to the Internet.

+

Each message sent using the RDFM protocol is structured as follows:

+
0                            h
++----------------------------+
+| utf-8 encoded JSON message |
++----------------------------+
+
+
+

The message is a UTF-8 encoded JSON object, where each message is distinguished by the mandatory 'method' field.

+

An example request sent to the server may look like:

+

{'method': 'capability_report', 'capabilities': {'shell': True}}

+

A response from the server may look like:

+

{'method': 'alert', 'alert': {'devices': ['d1', 'd2']}}

+ + +
+
+ + + Last update: + 2024-11-05 + + +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file