feat(iot): repackaged Odoo iot modules + Fusion Plating sensor wrapper
Phase A of the IoT initiative — gets the server-side infrastructure
in place before the Raspberry Pi hardware arrives, so the iot admin
UI + /fp/iot/ingest endpoint are ready to accept the first real
temperature reading as soon as the Pi is wired up.
New top-level folder: fusion_iot/
1. **iot_base/** — Odoo S.A. iot_base module, copied from
RePackaged-Odoo verbatim. LGPL-3 upstream, no changes needed.
2. **iot/** — Odoo S.A. iot module, repackaged:
- `models/update.py` neutralised (removed the publisher_warranty
IoT-Box-counting report that phones home to odoo.com for
enterprise licence enforcement)
- `iot_handlers/lib/load_worldline_library.sh` deleted (proprietary
Worldline payment lib fetch from download.odoo.com, not needed)
- `wizard/add_iot_box.py._connect_iot_box_with_pairing_code` —
upstream called odoo.com's iot-proxy to resolve pairing codes;
replaced with a no-op. Pi-side iot_drivers proxy registers
directly with this Odoo server instead.
- Manifest rebranded with an explicit changelog preamble.
3. **fusion_plating_iot/** — new plating-specific wrapper:
- `fp.tank.sensor` — maps an iot.device (or a direct-HTTP-ingest
sensor) to a fusion.plating.tank + fusion.plating.bath.parameter.
Supports DS18B20, PT100/1000, pH, conductivity, level. Per-sensor
alert_min/max overrides.
- `fp.tank.reading` — append-only time-series. On create, evaluates
against sensor's alert range. On in-spec → out-of-spec TRANSITION,
auto-raises a fusion.plating.quality.hold (once per excursion,
no spam during sustained out-of-spec).
- `POST /fp/iot/ingest` — shared-secret HTTP endpoint for sensors
bypassing the Pi proxy. Token via X-FP-IOT-Token header OR body.
Accepts single-reading or batch payloads.
- Menu under Plating → Operations → Sensors & Readings.
- Tank form inherits get a Sensors tab inline.
Deployed to entech. Verified end-to-end:
- Install: iot_base + iot + fusion_plating_iot all 'installed'
- Smoke test: in-spec → out-of-spec → hold raised (HOLD-0010);
continued excursion → NO duplicate hold; back-in-spec → NEW
excursion → NEW hold (HOLD-0011) ✓
- HTTP endpoint: correct token → 200 accepted; wrong token → 401;
unknown device_serial → 404; batch payload → 200 accepted=N ✓
Phase B (when Raspberry Pi hardware arrives): DS18B20 iot_handler
driver for the Pi-side iot_drivers proxy + systemd service on
vanilla Raspberry Pi OS + first live reading from physical probe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BIN
fusion_iot/iot/static/src/._@types
Executable file
BIN
fusion_iot/iot/static/src/._backend
Executable file
BIN
fusion_iot/iot/static/src/._client_action
Executable file
BIN
fusion_iot/iot/static/src/._img
Executable file
BIN
fusion_iot/iot/static/src/._iot_device_hook.js
Normal file
BIN
fusion_iot/iot/static/src/._iot_report_action.js
Normal file
BIN
fusion_iot/iot/static/src/._network_utils
Executable file
BIN
fusion_iot/iot/static/src/._overrides
Executable file
BIN
fusion_iot/iot/static/src/._select_printer_wizard.js
Normal file
BIN
fusion_iot/iot/static/src/._view_widgets
Executable file
BIN
fusion_iot/iot/static/src/@types/._services.d.ts
vendored
Normal file
7
fusion_iot/iot/static/src/@types/services.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module "services" {
|
||||
import { IotWebsocketService } from "@iot/iot_websocket_service";
|
||||
|
||||
export interface Services {
|
||||
iot_websocket: typeof IotWebsocketService
|
||||
}
|
||||
}
|
||||
BIN
fusion_iot/iot/static/src/backend/._iot_device_form.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
|
||||
export class AddIoTBoxFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
this.iotBoxesBeforeConnection = [];
|
||||
this.newIoTBoxes = []; // List of new IoT boxes found
|
||||
this.iotCheckTimer = null; // Timer to manage polling
|
||||
|
||||
onMounted(async () => {
|
||||
await this.initializeIoTConnection();
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.onWillUnmount();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a loop to check for new IoT Boxes every 10 seconds.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initializeIoTConnection() {
|
||||
this.iotBoxesBeforeConnection = await this.orm.call("iot.box", "search_read", [[], ["identifier"]]);
|
||||
|
||||
// Set a timer to check for new IoT Boxes every 5 seconds
|
||||
this.iotCheckTimer = setInterval(async () => {
|
||||
if (await this.lookForNewIoTBox()) {
|
||||
this.notifyIoTBoxFound(true);
|
||||
clearInterval(this.iotCheckTimer);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Set a timeout to stop the polling after 2 minutes
|
||||
setTimeout(() => this.notifyIoTBoxFound(false), 60 * 2 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for new IoT Boxes that have been connected since the last check.
|
||||
* @returns {Promise<boolean>} True if a new IoT Box has been found, false otherwise.
|
||||
*/
|
||||
async lookForNewIoTBox() {
|
||||
const iotBoxesAfterConnection = await this.orm.call("iot.box", "search_read", [[], ["identifier"]]);
|
||||
this.newIoTBoxes = iotBoxesAfterConnection.filter(
|
||||
(afterBox) => !this.iotBoxesBeforeConnection.some(
|
||||
beforeBox => beforeBox.identifier === afterBox.identifier
|
||||
)
|
||||
);
|
||||
|
||||
return this.newIoTBoxes.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the user if a new IoT Box has been found. If no new IoT Box has been found, notify the user.
|
||||
* @param {boolean} found Whether a new IoT Box has been found.
|
||||
*/
|
||||
notifyIoTBoxFound(found) {
|
||||
if (found) {
|
||||
this.env.services.action.doAction({ type: "ir.actions.act_window_close" });
|
||||
this.notification.add(_t("New IoT Box connected!"), { type: "success" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the timer when the component is unmounted.
|
||||
*/
|
||||
onWillUnmount() {
|
||||
if (this.iotCheckTimer) {
|
||||
clearInterval(this.iotCheckTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const addIoTBox = { ...formView, Controller: AddIoTBoxFormController };
|
||||
|
||||
registry.category("views").add('add_iot_box_wizard', addIoTBox);
|
||||
139
fusion_iot/iot/static/src/backend/iot_device_form.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useSubEnv } from "@odoo/owl";
|
||||
import { PRINTER_MESSAGES, FDM_MESSAGES } from "@iot/network_utils/iot_http_service";
|
||||
import { printReport } from "@iot/iot_report_action";
|
||||
|
||||
class IoTDeviceController extends formView.Controller {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.iotHttp = useService("iot_http");
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
|
||||
useSubEnv({ onClickViewButton: this.onClickButtonTest.bind(this) });
|
||||
}
|
||||
|
||||
async onWillSaveRecord(record) {
|
||||
if (["keyboard", "scanner"].includes(record.data.type)) {
|
||||
await this.updateKeyboardLayout(record.data);
|
||||
} else if (record.data.type === "display") {
|
||||
await this.updateDisplayUrl(record.data);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send an action to the device to update the keyboard layout
|
||||
*/
|
||||
async updateKeyboardLayout(data) {
|
||||
const { iot_id, identifier, keyboard_layout, is_scanner } = data;
|
||||
// IMPROVEMENT: Perhaps combine the call to update_is_scanner and update_layout in just one remote call to the iotbox.
|
||||
this.iotHttp.action(
|
||||
iot_id.id,
|
||||
identifier,
|
||||
{
|
||||
action: "update_is_scanner",
|
||||
is_scanner
|
||||
},
|
||||
() => {},
|
||||
() => {
|
||||
this.notification.add(_t("Failed to update scanner mode on the device."), {
|
||||
type: "danger"
|
||||
});
|
||||
}
|
||||
);
|
||||
if (keyboard_layout) {
|
||||
const [keyboard] = await this.model.orm.read(
|
||||
"iot.keyboard.layout",
|
||||
[keyboard_layout[0]],
|
||||
["layout", "variant"]
|
||||
);
|
||||
return this.iotHttp.action(
|
||||
iot_id.id,
|
||||
identifier,
|
||||
{
|
||||
action: "update_layout",
|
||||
layout: keyboard.layout,
|
||||
variant: keyboard.variant,
|
||||
},
|
||||
() => {},
|
||||
() => {
|
||||
this.notification.add(_t("Failed to update keyboard layout on the device."), {
|
||||
type: "danger"
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.iotHttp.action(iot_id.id, identifier, { action: "update_layout" });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send an action to the device to update the screen url
|
||||
*/
|
||||
async updateDisplayUrl(data) {
|
||||
const { iot_id, identifier, display_url } = data;
|
||||
return this.iotHttp.action(iot_id.id, identifier, { action: "update_url", url: display_url });
|
||||
}
|
||||
|
||||
onDeviceEvent(event, type) {
|
||||
const errorMessages = type === "printer" ? PRINTER_MESSAGES : FDM_MESSAGES;
|
||||
// Parse blackbonse response
|
||||
if (type === "fiscal_data_module") {
|
||||
const errorCode = event.message ? event.message.substring(0, 3) : event.result?.error?.errorCode;
|
||||
if (FDM_MESSAGES[errorCode] && !["000", "102"].includes(errorCode)) {
|
||||
event.message = errorCode
|
||||
event.status = "error";
|
||||
}
|
||||
}
|
||||
const errorMessage = errorMessages[event.message] ?? event.message;
|
||||
const defaultMessage = type === "printer" ? _t("Test page printed") : _t('Fiscal Data Module is connected and operational');
|
||||
switch (event.status) {
|
||||
case "error":
|
||||
this.notification.add(errorMessage, { type: "danger" });
|
||||
return;
|
||||
case "warning":
|
||||
this.notification.add(errorMessage, { type: "warning" });
|
||||
return;
|
||||
case "disconnected":
|
||||
this.notification.add(_t("Device is disconnected"), { type: "danger" });
|
||||
return;
|
||||
default:
|
||||
this.notification.add(defaultMessage, { type: "info" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async onClickButtonTest(params) {
|
||||
if (params.clickParams.name === "test_device") {
|
||||
const { iot_id, identifier, type, subtype } = this.model.root.data;
|
||||
|
||||
if (type === "printer" && subtype === "office_printer") {
|
||||
// We print a "real" pdf report: the external report sample
|
||||
const reportId = (await this.orm.searchRead(
|
||||
"ir.actions.report",
|
||||
[["report_type", "=", "qweb-pdf"], ["report_name", "=", "web.preview_externalreport"]],
|
||||
["id"],
|
||||
{ limit: 1 }
|
||||
))[0].id;
|
||||
const deviceId = this.model.root._config.resId;
|
||||
return printReport(this.env, [reportId, [deviceId], null], [deviceId]);
|
||||
}
|
||||
|
||||
return this.iotHttp.action(
|
||||
iot_id.id,
|
||||
identifier,
|
||||
{ action: "status" },
|
||||
(event) => this.onDeviceEvent(event, type),
|
||||
(event) => this.onDeviceEvent(event, type),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const iotDeviceFormView = {
|
||||
...formView,
|
||||
Controller: IoTDeviceController,
|
||||
};
|
||||
|
||||
registry.category("views").add("iot_device_form", iotDeviceFormView);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { discoverIotBoxes } from "@iot/client_action/discover_iot_boxes";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
|
||||
export class NoIoTBoxFoundFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.retryDiscoverInterval = null;
|
||||
|
||||
onMounted(() => {
|
||||
this.startCountdown(15);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.retryDiscoverInterval) {
|
||||
clearInterval(this.retryDiscoverInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a countdown. When it reaches 0 we look for new IoT Boxes again
|
||||
*/
|
||||
startCountdown(seconds) {
|
||||
const countdownSpinner = document.getElementById("discover_retry_spinner");
|
||||
const countdownEl = document.getElementById("discover_retry_countdown");
|
||||
const textToDisplay = _t("Retrying in ")
|
||||
let timeLeft = seconds;
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = `${textToDisplay}${timeLeft}s`;
|
||||
this.retryDiscoverInterval = setInterval(async () => {
|
||||
timeLeft--;
|
||||
countdownEl.textContent = `${textToDisplay}${timeLeft}s`;
|
||||
// Look for new IoT Boxes again
|
||||
if (timeLeft <= 0) {
|
||||
countdownEl.textContent = `${textToDisplay}0s`;
|
||||
const nextAction = await discoverIotBoxes(this.env)
|
||||
if (!nextAction.no_iot_found_found) {
|
||||
await this.actionService.doAction(nextAction);
|
||||
}
|
||||
timeLeft = seconds + 1;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
// Clear the countdown and interval
|
||||
setTimeout(() => {
|
||||
if (countdownEl) {
|
||||
countdownSpinner.classList.add("o_hidden");
|
||||
countdownEl.textContent = null;
|
||||
clearInterval(this.retryDiscoverInterval);
|
||||
}
|
||||
}, 60 * 5 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export const noIoTBoxFound = { ...formView, Controller: NoIoTBoxFoundFormController };
|
||||
|
||||
registry.category("views").add('no_iot_box_found_wizard', noIoTBoxFound);
|
||||
BIN
fusion_iot/iot/static/src/client_action/._discover_iot_boxes.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
|
||||
export const IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY = "odoo-iot-linked_reports";
|
||||
|
||||
export function removeIoTReportIdFromBrowserLocalStorage(report_id) {
|
||||
const links = JSON.parse(browser.localStorage.getItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY));
|
||||
delete links[report_id];
|
||||
if (Object.keys(links).length === 0) {
|
||||
// If the list is empty, remove the entry
|
||||
browser.localStorage.removeItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY);
|
||||
} else {
|
||||
// Replace the entry in LocalStorage by the same object with the key 'report_id' removed
|
||||
browser.localStorage.setItem(
|
||||
IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(links)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the report_id in the browser LocalStorage
|
||||
* @param report_id The report_id to set
|
||||
* @param value The value to set
|
||||
*/
|
||||
export function setReportIdInBrowserLocalStorage(report_id, value) {
|
||||
let links = JSON.parse(browser.localStorage.getItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY));
|
||||
if (links === null || typeof links !== "object") {
|
||||
links = {};
|
||||
}
|
||||
links[report_id] = value;
|
||||
browser.localStorage.setItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY, JSON.stringify(links));
|
||||
}
|
||||
|
||||
class IoTReportLocalStorage extends Component {
|
||||
static template = "iot.delete_printer";
|
||||
static props = { ...standardActionServiceProps };
|
||||
|
||||
setup() {
|
||||
const links = JSON.parse(browser.localStorage.getItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY));
|
||||
this.state = useState({ reportList: (links ? Object.keys(links) : []) });
|
||||
this.orm = useService("orm");
|
||||
onWillStart(async () => {
|
||||
this.state.reportList = await this.orm.searchRead("ir.actions.report", [
|
||||
["id", "in", this.state.reportList],
|
||||
]);
|
||||
});
|
||||
}
|
||||
removeFromLocal(event, id) {
|
||||
removeIoTReportIdFromBrowserLocalStorage(id);
|
||||
this.state.reportList = this.state.reportList.filter((report) => report.id !== id);
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("iot_delete_linked_devices_action", IoTReportLocalStorage);
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="iot.delete_printer">
|
||||
<div class="container mt-4">
|
||||
<h2>Reports with linked printers</h2>
|
||||
<div class="mt-3">
|
||||
<p t-if="!state.reportList.length">No report to display</p>
|
||||
<div class="card mb-3" t-foreach="state.reportList" t-as="item" t-key="item.id">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title m-0" t-out="item.name" />
|
||||
<button class="btn btn-primary" t-on-click="(ev) => this.removeFromLocal(ev, item.id)">Reset Linked Printers</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const IOT_PROXY_DISCOVER_BOXES_ENDPOINT =
|
||||
"";
|
||||
|
||||
export async function discoverIotBoxes(env) {
|
||||
const orm = env.services.orm;
|
||||
const discoveredBoxes = [];
|
||||
|
||||
try {
|
||||
const response = await rpc(IOT_PROXY_DISCOVER_BOXES_ENDPOINT);
|
||||
discoveredBoxes.push(...response);
|
||||
} catch (error) {
|
||||
console.warn("Failed to retrieve local IoT boxes: " + error);
|
||||
}
|
||||
|
||||
return orm.call("iot.box", "connect_iot_box", [discoveredBoxes]);
|
||||
}
|
||||
|
||||
registry.category("actions").add("discover_iot_boxes", discoverIotBoxes);
|
||||
BIN
fusion_iot/iot/static/src/img/._barcode-scanner.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._box.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._caliper.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._camera.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._desktop.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._fdm.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._iot_power.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._keyboard.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._network_light.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._pairing_code.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._payment-terminal.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._printer.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._rulers.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._scales.svg
Normal file
BIN
fusion_iot/iot/static/src/img/._unsupported.svg
Normal file
1
fusion_iot/iot/static/src/img/barcode-scanner.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
fusion_iot/iot/static/src/img/box.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280"><g id="m"><g><polygon points="256.7625 72.6032 143.6068 6.4158 23.2375 75.9016 136.7016 142.089 256.7625 72.6032" style="fill:#fff;"/><polygon points="136.7016 273.5842 23.2375 207.3968 23.2375 75.9016 136.7016 142.089 136.7016 273.5842" style="fill:#c1dbf6; stroke:#c1dbf6; stroke-miterlimit:10;"/><polygon points="136.7016 273.5842 256.7625 204.0984 256.7625 72.6032 136.7016 142.089 136.7016 273.5842" style="fill:#fff;"/><polygon points="206.4073 43.3576 87.226 113.2832 103.2781 122.7385 222.8992 53.0328 206.4073 43.3576" style="fill:#fbdbd0;"/><polygon points="204.5382 42.588 188.8254 32.9127 69.644 102.8383 85.6961 112.2936 204.5382 42.588" style="fill:#fbdbd0;"/><line x1="87.226" y1="112.2936" x2="204.5382" y2="42.588" style="fill:none; stroke:#374874; stroke-linecap:round; stroke-miterlimit:10; stroke-width:2px;"/><path d="m90.0846,164.5179l-34.3031-19.5703c-3.7382-2.1989-6.157-6.157-6.157-10.5548h0c0-4.8376,5.2774-7.9161,9.4554-5.4973l36.2821,20.8897c3.0785,1.7591,5.0575,5.2774,5.0575,8.7957h0c.2199,5.2774-5.7172,8.5758-10.3349,5.9371Z" style="fill:#374874; stroke:#374874; stroke-miterlimit:10; stroke-width:1.7657px;"/><path d="m143.2984,6.4158l113.4641,66.1874v131.4952l-120.0609,69.4858L23.2375,207.3968V75.9015L143.2984,6.4158m.0079-4.6262l-2.0116,1.1642L21.2339,72.4396l-1.9964,1.1554v136.0993l1.9845,1.1577,113.4641,66.1874,2.0076,1.171,2.0116-1.1642,120.0609-69.4858,1.9964-1.1554V70.3057l-1.9845-1.1577L145.3139,2.9607l-2.0076-1.1711h0Z" style="fill:#374874;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
fusion_iot/iot/static/src/img/caliper.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
fusion_iot/iot/static/src/img/camera.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
1
fusion_iot/iot/static/src/img/desktop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280"><g id="bk"><path d="m159.0514,166.4647v45.4805c0,8.0118,5.6248,11.2592,12.5632,7.2533l7.3486-4.2428c1.4494-.8367,3.2685-.2011,3.2685,1.4726s.8052,2.4082-.6442,3.2451l-76.6594,44.0872c-1.4494.8369-2.6243.1585-2.6243-1.5151s1.175-3.7087,2.6243-4.5456l7.3485-4.2427c6.9386-4.0058,12.5632-13.7483,12.5632-21.7601v-45.4805l34.2114-19.7519Z" style="fill:#fff;"/><path d="m51.9497,261.9687c-6.2441-.001-11.1357-5.1182-11.1357-11.6504v-117.2051c0-4.9541,2.7656-9.8799,6.8809-12.2588L222.1928,20.1084c1.7764-1.0254,3.7627-1.5654,5.748-1.5654,6.2451,0,11.1367,5.1172,11.1367,11.6504v117.2051c0,4.957-2.7676,9.8848-6.8877,12.2617l-174.4912,100.7422c-1.7764,1.0254-3.7637,1.5664-5.749,1.5664h0Z" style="fill:#fff;"/><path d="m159.0514,211.9454c0,8.0116,5.6248,11.2588,12.5632,7.2532l7.3486-4.2431c.7123-.4109,1.3563-.4524,1.8292-.1938-.0071-.0182-.0127-.0306-.0127-.0306l-15.1936-7.5875-6.5347,3.773v1.0287Z" style="fill:#fbdbd0;"/><path d="m102.3039,262.2455c0-1.6735,1.175-3.7087,2.6244-4.5455l7.3485-4.2427c6.9386-4.006,12.5633-13.7486,12.5633-21.7601v-12.024l-13.7338,7.6997c-1.3291,7.1812-6.3386,14.8845-12.2856,18.3182l-6.8245,3.9399c-1.4814.8555-2.9737,4.866-2.9737,4.866,0,0,13.0602,9.0804,14.0115,9.4334.0079.0027.015-.0009.0227.0009-.4645-.2786-.7526-.8596-.7526-1.6859Z" style="fill:#fbdbd0;"/><path d="m54.6983,246.1829l174.4948-100.7447c1.0407-.6007,1.8842-2.2125,1.8842-3.5998V24.6334c0-1.3875-.8435-2.0253-1.8842-1.4244L54.6983,123.9535c-1.0405.6009-1.884,2.2125-1.884,3.6v117.205c0,1.3875.8435,2.0251,1.884,1.4244Z" style="fill:#374874;"/><path d="m135.6312,207.6134l12.6291-7.2915c.5638-.3255,1.021-1.1989,1.021-1.9507h0c0-.7519-.4572-1.0974-1.021-.7718l-12.6291,7.2915c-.5639.3255-1.021,1.1989-1.021,1.9507h0c0,.7519.4571,1.0974,1.021.7718Z" style="fill:#150035;"/><path d="m135.224,208.4898c.2583,0,.5263-.0745.7844-.2238l12.6286-7.2907c.7972-.4605,1.3981-1.5797,1.3981-2.6037,0-.6715-.266-1.2105-.7294-1.4786-.4222-.2454-.9396-.2247-1.4217.0521l-12.6295,7.2926c-.7972.4605-1.3981,1.5797-1.3981,2.6037,0,.6724.2661,1.2115.7294,1.4786.1965.1138.4133.1697.6382.1697Z" style="fill:#374874;"/><path d="m36.8518,124.6135v124.6879c0,2.482.7746,4.2728,2.0156,5.1551,5.9679,5.1524,11.24,6.5979,11.2486,6.6002-1.5801-.694-2.6042-2.6316-2.6042-5.4853v-124.6874c0-2.7634.9576-5.7785,2.4525-8.2537l-11.0756-5.5377c-1.2529,2.3238-2.0367,5.0247-2.0367,7.5209Z" style="fill:#c1dbf6;"/><path d="m223.332,10.1785c-.02-.0109-.0414-.0182-.0617-.0287-.0105-.0046-.0206-.0091-.0312-.0141l-.0007.0009c-1.1282-.5513-2.5562-.4505-4.1129.4487L43.4462,112.0136c-1.7698,1.0219-3.3732,2.8824-4.5577,5.079l11.0756,5.5377c1.1323-1.8746,2.5722-3.4401,4.1419-4.3462L229.7854,16.8553c1.6782-.9686,3.2052-1.0078,4.3692-.3032h0s-4.4295-3.5094-10.8226-6.3736Z" style="fill:#fbdbd0;"/><path d="m219.1256,10.5855l10.6598,6.27,6.5945,4.9854v124.688c0,4.8555-2.9525,10.4966-6.5945,12.5991l-70.7339,40.8389v10.9497l6.5347-3.7729s15.1948,7.6138,15.1951,7.6138h0c.4954.2656,2.2558,4.0791.8063,4.916,0,0-77.5577,44.2529-78.5317,44.2529-.0032,0-.0056-.0005-.0071-.0015h0c-.0005-.0005-14.0264-9.4277-14.0264-9.4277,0,0,1.4923-4.0107,2.9737-4.8662l6.8245-3.9399c5.8607-3.3838,10.8033-10.9136,12.218-18.0044l-56.9325,32.8706c-.9247.5337-1.8032.7842-2.6006.7842-.4977,0-.964-.0977-1.3901-.2852h-.0002.0002s.0003.0005.0004.0005c-.0297-.0083-5.2926-1.458-11.2484-6.6001-1.241-.8823-2.0156-2.6733-2.0156-5.1553v-124.688c0-2.4961.7838-5.1968,2.0367-7.5205,1.1845-2.1968,2.7879-4.0571,4.5577-5.0791L219.1256,10.5855M50.1158,261.0567h.0001-.0001M219.1444,5.9556l-2.0187,1.166L41.4462,108.5498c-2.337,1.3491-4.4957,3.709-6.0786,6.6445-1.6224,3.0093-2.5159,6.354-2.5159,9.4189v124.688c0,3.6382,1.2484,6.5703,3.5214,8.2861,5.8486,5.0127,11.0894,6.8408,12.4113,7.2466.8602.3369,1.774.5073,2.721.5073,1.5347,0,3.0822-.4438,4.5997-1.3193l46.0001-26.5586c-1.5844,2.0049-3.402,3.6768-5.2846,4.7637l-6.8245,3.9395c-2.2551,1.3027-3.8129,4.4907-4.7227,6.9355l-1.0997,2.9556,2.6172,1.7593c11.7263,7.8818,13.7325,9.23,14.1479,9.502l-.0018.0029.9698.603,1.149.0015q1.3527,0,14.636-7.4248c6.5602-3.6738,15.6337-8.7954,26.9681-15.2222,19.2927-10.9395,38.7161-22.021,38.91-22.1318,1.6778-.9688,2.6108-2.7329,2.5776-4.8501-.0278-1.772-1.1071-5.7949-3.4759-7.0649l-1.4295-.7666-.0619.0176c-3.751-1.8794-13.8021-6.916-13.8021-6.916l-1.9262-.9653-1.8658,1.0776-.5347.3086v-1.7119l68.734-39.6846c4.8995-2.8281,8.5944-9.7339,8.5944-16.063V19.8506l-1.5878-1.2007-6.5944-4.9854-.1847-.1396-.1996-.1172-10.6598-6.27-2.0092-1.1821h0Z" style="fill:#374874;"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
19
fusion_iot/iot/static/src/img/fdm.svg
Normal file
|
After Width: | Height: | Size: 46 KiB |
23
fusion_iot/iot/static/src/img/iot_power.svg
Normal file
|
After Width: | Height: | Size: 47 KiB |
1
fusion_iot/iot/static/src/img/keyboard.svg
Normal file
|
After Width: | Height: | Size: 284 KiB |
22
fusion_iot/iot/static/src/img/network_light.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="128" height="128" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46 48.6602L32 56.6602V40.6602L46 32.6602V48.6602Z" fill="white"/>
|
||||
<path d="M22 13.2228V14.3745L4 24.6602V56.6602L32 40.6602V18.518L33 17.9346V40.6602C33 41.0191 32.8077 41.3504 32.4961 41.5285L4.49614 57.5285C4.18664 57.7053 3.80639 57.7041 3.49807 57.5252C3.18976 57.3462 3 57.0167 3 56.6602V24.6602C3 24.3014 3.19229 23.97 3.50386 23.792L22 13.2228Z" fill="#374874"/>
|
||||
<path d="M46 32.6602L25 20.6602L11 28.6602L32 40.6602L46 32.6602Z" fill="white" stroke="#374874" stroke-linejoin="round"/>
|
||||
<path d="M32 56.6602V40.6602L11 28.6602V44.6602L32 56.6602Z" fill="#C1DBF6" stroke="#374874" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.2348 56.526L46 32.8782V32.6602L32 40.6602V56.6602L32.2348 56.526Z" fill="#FBDBD0"/>
|
||||
<path d="M46 48.6602L32 56.6602V40.6602L46 32.6602V48.6602Z" stroke="#374874" stroke-linejoin="round"/>
|
||||
<circle cx="3.05662" cy="3.05662" r="3.55662" transform="matrix(-0.866025 0.5 0 1 30.3038 8.66986)" fill="white" stroke="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.6567 16.3114C26.1948 17.1555 25.0096 16.4712 25.0096 14.7831C25.0096 13.5978 25.5939 12.2327 26.4481 11.2324C28.1248 10.9129 29.4509 11.6785 30.0125 13.2903C29.5733 14.5302 28.6833 15.7187 27.6567 16.3114Z" fill="#FBDBD0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.6567 8.43023C28.2242 7.52524 29.7275 7.37632 30.8359 8.01625C31.9443 8.65618 32.567 10.0325 32.567 11.8425C32.567 12.1187 32.7609 12.2306 33 12.0925C33.2391 11.9544 33.433 11.6187 33.433 11.3425C33.433 9.26731 32.7191 7.68928 31.4483 6.95559C30.1775 6.22189 28.4539 6.39263 26.6567 7.43023C26.4176 7.5683 26.2237 7.90409 26.2237 8.18023C26.2237 8.45637 26.4176 8.5683 26.6567 8.43023Z" fill="#FBDBD0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.6567 5.58493C28.8778 4.30263 31.0078 4.09163 32.5783 4.99836C34.1488 5.90508 35.0311 7.85526 35.0311 10.4199C35.0311 10.696 35.225 10.8079 35.4641 10.6699C35.7033 10.5318 35.8971 10.196 35.8971 9.91987C35.8971 7.09004 34.9236 4.93819 33.1907 3.9377C31.4578 2.9372 29.1074 3.17002 26.6567 4.58493C26.4176 4.72301 26.2237 5.05879 26.2237 5.33493C26.2237 5.61108 26.4176 5.72301 26.6567 5.58493Z" fill="#FBDBD0"/>
|
||||
<path d="M36 46.4502L37 43.9502L38.5 41.9502L40 40.9502H41.5C43 41.6169 46 43.0502 46 43.4502C46 43.8502 48.3333 44.9502 49.5 45.4502L52 46.9502L52.5 50.4502L48.5 54.9502H46.5L38 49.9502L36.5 48.9502L36 46.4502Z" fill="white"/>
|
||||
<path d="M45.6754 52.3307C45.6754 50.2991 47.1105 47.8243 48.8808 46.8023C50.6506 45.7802 52.0857 46.5986 52.0857 48.6294C52.0857 50.6602 50.6506 53.1357 48.8808 54.1578C47.1106 55.1799 45.6754 54.3618 45.6754 52.3307Z" fill="#C1DBF6"/>
|
||||
<path d="M42.3076 47.6844C41.8588 48.5764 41.5897 49.5392 41.5897 50.4188C41.5897 51.4176 41.9368 52.1231 42.5 52.4606" stroke="#374874" stroke-linecap="round"/>
|
||||
<path d="M39.8076 46.4502C39.3588 47.3421 39.0897 48.3049 39.0897 49.1846C39.0897 50.1834 39.4368 50.8888 40 51.2264" stroke="#374874" stroke-linecap="round"/>
|
||||
<path d="M44.7951 43.8904C45.6492 43.3971 46.4255 43.5969 47 43.8904" stroke="#374874" stroke-linecap="round"/>
|
||||
<path d="M42.7951 42.7499C43.6492 42.2566 44.4255 42.4564 45 42.7499" stroke="#374874" stroke-linecap="round"/>
|
||||
<path d="M41.0596 41.1822C40.6344 41.1822 40.1563 41.3078 39.6509 41.5995C37.8806 42.6215 36.4459 45.0967 36.4459 47.1279C36.4459 48.0927 36.7699 48.7831 37.2995 49.1331L37.3538 49.1671C37.368 49.1757 46.026 54.0325 46.4926 54.3144C46.6253 54.3946 46.7089 54.4389 46.7089 54.4389C46.9277 54.5441 47.1745 54.5985 47.4429 54.5985C47.8753 54.5985 48.3637 54.4572 48.8808 54.1587C50.6506 53.1366 52.0857 50.6618 52.0857 48.6302C52.0857 47.5628 51.6888 46.8309 51.0556 46.5247L42.0244 41.4352C41.7525 41.2752 41.4256 41.1822 41.0596 41.1822ZM41.0596 40.2679C41.5725 40.2679 42.0664 40.399 42.4881 40.6472L42.4941 40.6507L42.5 40.6543L51.4978 45.7234C52.4532 46.2091 53 47.2652 53 48.6302C53 50.9884 51.3915 53.7645 49.338 54.9504C48.6916 55.3236 48.0541 55.5128 47.443 55.5128C47.038 55.5128 46.6578 55.4288 46.3128 55.263L46.2963 55.255L46.28 55.2464C46.2692 55.2407 46.1699 55.1876 46.02 55.097C45.5552 54.8162 36.9036 49.9632 36.8819 49.9502L36.875 49.9461L36.868 49.9416L36.8137 49.9076L36.8045 49.9018L36.7954 49.8958C35.9805 49.3572 35.5316 48.3742 35.5316 47.1279C35.5316 44.7695 37.1402 41.9933 39.1937 40.8077C39.8143 40.4495 40.442 40.2679 41.0596 40.2679Z" fill="#374874"/>
|
||||
<path d="M50.6716 48.2371L60.4262 53.6881L60.7131 54.2619V55.6964L60.1393 56.844L59.5655 57.7047L58.4179 58.5654H57.2703L48.6633 53.6881L47.8026 52.8274L47.5157 51.9667L47.8026 50.5323L48.6633 49.3847L49.524 48.524L50.6716 48.2371Z" fill="#374874"/>
|
||||
<path d="M56.7972 57.0623C56.7972 55.8966 57.6206 54.4766 58.6364 53.8901C59.6519 53.3036 60.4754 53.7732 60.4754 54.9385C60.4754 56.1038 59.6519 57.5242 58.6364 58.1107C57.6207 58.6972 56.7972 58.2278 56.7972 57.0623Z" fill="#374874"/>
|
||||
<path d="M50.4189 48.4748C50.1749 48.4748 49.9006 48.5468 49.6106 48.7142C48.5948 49.3007 47.7716 50.7209 47.7716 51.8864C47.7716 52.44 47.9575 52.8362 48.2614 53.037L48.2925 53.0565C48.3007 53.0615 56.9983 58.0386 57.266 58.2004C57.3422 58.2464 57.3902 58.2719 57.3902 58.2719C57.5157 58.3322 57.6573 58.3634 57.8114 58.3634C58.0595 58.3634 58.3397 58.2823 58.6364 58.111C59.6519 57.5246 60.4754 56.1045 60.4754 54.9388C60.4754 54.3263 60.2476 53.9064 59.8843 53.7307L50.9725 48.6199C50.8165 48.5281 50.629 48.4748 50.4189 48.4748ZM50.4189 47.9502C50.7132 47.9502 50.9967 48.0254 51.2386 48.1678L51.242 48.1698L51.2454 48.1719L60.138 53.2709C60.6862 53.5496 61 54.1556 61 54.9388C61 56.2919 60.077 57.8849 58.8988 58.5653C58.5278 58.7795 58.162 58.888 57.8114 58.888C57.579 58.888 57.3609 58.8398 57.1629 58.7447L57.1534 58.7401L57.1441 58.7352C57.1379 58.7319 57.0809 58.7014 56.9949 58.6494C56.7282 58.4883 48.0342 53.5133 48.0218 53.5059L48.0178 53.5035L48.0138 53.5009L47.9826 53.4814L47.9774 53.4781L47.9721 53.4746C47.5045 53.1656 47.247 52.6015 47.247 51.8864C47.247 50.5332 48.17 48.9402 49.3483 48.2599C49.7044 48.0544 50.0646 47.9502 50.4189 47.9502Z" fill="#374874"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
17
fusion_iot/iot/static/src/img/pairing_code.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="128" height="128" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.5 5L52 3L14.5 24L10 27.5V59.5L12 60L53 36.5L54 35.5L54.5 33.5V5Z" fill="white"/>
|
||||
<path d="M36.3546 48.4447C36.3546 50.2759 37.6403 51.0181 39.2262 50.1025L40.9059 49.1327C41.0687 49.0388 41.2159 49.0293 41.324 49.0884C41.3223 49.0842 41.3211 49.0814 41.3211 49.0814L37.8482 47.3471L36.3546 48.2095V48.4447Z" fill="#FBDBD0"/>
|
||||
<path d="M23.3837 59.9418C23.3837 59.5593 23.6523 59.0941 23.9836 58.9028L25.6633 57.9331C27.2492 57.0174 28.5349 54.7905 28.5349 52.9593V50.211L25.3957 51.9709C25.0919 53.6123 23.9469 55.3731 22.5876 56.1579L21.0277 57.0585C20.6891 57.254 20.348 58.1707 20.348 58.1707C20.348 58.1707 23.3332 60.2462 23.5506 60.3269C23.5524 60.3275 23.554 60.3267 23.5558 60.3271C23.4496 60.2635 23.3837 60.1307 23.3837 59.9418Z" fill="#FBDBD0"/>
|
||||
<path d="M36.3546 38.0491V48.4446C36.3546 50.2759 37.6403 51.0181 39.2262 50.1025L40.9059 49.1327C41.2372 48.9415 41.653 49.0868 41.653 49.4693C41.653 49.8519 41.837 50.0198 41.5057 50.2111L23.9836 60.2881C23.6523 60.4794 23.3837 60.3244 23.3837 59.9418C23.3837 59.5593 23.6523 59.0941 23.9836 58.9028L25.6632 57.9331C27.2492 57.0175 28.5348 54.7906 28.5348 52.9593V42.5638L36.3546 38.0491Z" fill="white"/>
|
||||
<path d="M12.5025 56.2704L52.387 33.243C52.6249 33.1057 52.8177 32.7373 52.8177 32.4202V5.63051C52.8177 5.31336 52.6249 5.16758 52.387 5.30493L12.5025 28.3322C12.2646 28.4696 12.0718 28.838 12.0718 29.1551V55.9448C12.0718 56.262 12.2646 56.4077 12.5025 56.2704Z" fill="#374874"/>
|
||||
<path d="M31.0014 47.4545L33.8881 45.7879C34.0169 45.7135 34.1214 45.5138 34.1214 45.342C34.1214 45.1701 34.0169 45.0912 33.8881 45.1656L31.0014 46.8322C30.8725 46.9066 30.768 47.1062 30.768 47.2781C30.768 47.45 30.8725 47.5289 31.0014 47.4545Z" fill="#150035"/>
|
||||
<path d="M30.9083 47.6548C30.9674 47.6548 31.0286 47.6378 31.0876 47.6037L33.9742 45.9372C34.1564 45.8319 34.2937 45.5761 34.2937 45.3421C34.2937 45.1886 34.2329 45.0654 34.127 45.0041C34.0305 44.948 33.9123 44.9528 33.8021 45.016L30.9153 46.6829C30.7331 46.7882 30.5957 47.044 30.5957 47.278C30.5957 47.4317 30.6566 47.5549 30.7625 47.616C30.8074 47.642 30.8569 47.6548 30.9083 47.6548Z" fill="#374874"/>
|
||||
<path d="M8.42326 28.4831V56.9832C8.42326 57.5505 8.60032 57.9598 8.88397 58.1615C10.2481 59.3392 11.4531 59.6696 11.4551 59.6701C11.0939 59.5115 10.8598 59.0686 10.8598 58.4163V29.9164C10.8598 29.2847 11.0787 28.5956 11.4204 28.0298L8.88884 26.764C8.60246 27.2952 8.42326 27.9125 8.42326 28.4831Z" fill="#C1DBF6"/>
|
||||
<path d="M51.0473 2.3265C51.0427 2.324 51.0378 2.32234 51.0332 2.31994C51.0308 2.31888 51.0285 2.31786 51.0261 2.31671L51.0259 2.31692C50.768 2.19091 50.4416 2.21395 50.0858 2.41948L9.93055 25.6031C9.52603 25.8367 9.15954 26.2619 8.88879 26.764L11.4204 28.0298C11.6792 27.6013 12.0083 27.2435 12.3671 27.0363L52.5224 3.85262C52.906 3.63123 53.255 3.62227 53.521 3.78332C53.521 3.78332 52.5086 2.98117 51.0473 2.3265Z" fill="#FBDBD0"/>
|
||||
<path d="M50.0858 2.41953L52.5224 3.85267L54.0297 4.99219V33.4923C54.0297 34.6021 53.3548 35.8915 52.5224 36.3721L36.3546 45.7067V48.2095L37.8483 47.3471C37.8483 47.3471 41.3214 49.0874 41.3214 49.0874C41.4347 49.1481 41.837 50.0198 41.5057 50.2111C41.5057 50.2111 23.7783 60.326 23.5556 60.326C23.5549 60.326 23.5544 60.3259 23.554 60.3257C23.5539 60.3256 20.348 58.1708 20.348 58.1708C20.348 58.1708 20.6891 57.254 21.0277 57.0585L22.5876 56.158C23.9272 55.3845 25.0569 53.6634 25.3802 52.0427L12.3671 59.5559C12.1557 59.6779 11.9549 59.7352 11.7727 59.7352C11.6589 59.7352 11.5523 59.7129 11.4549 59.67C11.4482 59.6681 10.2453 59.3369 8.88397 58.1615C8.60032 57.9598 8.42327 57.5505 8.42327 56.9832V28.4831C8.42327 27.9125 8.60242 27.2952 8.8888 26.7641C9.15954 26.262 9.52603 25.8367 9.93056 25.6031L50.0858 2.41953ZM50.0901 1.36127L49.6287 1.62778L9.47341 24.8114C8.93924 25.1197 8.44583 25.6591 8.08402 26.3301C7.71319 27.018 7.50896 27.7825 7.50896 28.483V56.9831C7.50896 57.8147 7.79431 58.4849 8.31385 58.8771C9.65067 60.0228 10.8486 60.4407 11.1507 60.5334C11.3473 60.6105 11.5562 60.6494 11.7727 60.6494C12.1234 60.6494 12.4772 60.548 12.824 60.3479L23.3383 54.2773C22.9762 54.7356 22.5607 55.1177 22.1304 55.3662L20.5705 56.2666C20.0551 56.5644 19.699 57.2931 19.4911 57.8519L19.2397 58.5274L19.8379 58.9296C22.5182 60.7311 22.9768 61.0393 23.0717 61.1015L23.0713 61.1021L23.293 61.2399L23.5556 61.2403C23.7617 61.2403 24.8769 60.6746 26.901 59.5432C28.4005 58.7035 30.4744 57.5328 33.0651 56.0638C37.4749 53.5634 41.9145 51.0305 41.9588 51.0051C42.3423 50.7837 42.5556 50.3805 42.548 49.8965C42.5416 49.4915 42.2949 48.572 41.7535 48.2817L41.4268 48.1065L41.4126 48.1105C40.5552 47.6809 38.2578 46.5297 38.2578 46.5297L37.8176 46.3091L37.3911 46.5554L37.2689 46.6259V46.2346L52.9795 37.1639C54.0994 36.5174 54.944 34.939 54.944 33.4923V4.53727L54.581 4.26282L53.0737 3.1233L53.0315 3.09139L52.9859 3.0646L50.5494 1.63146L50.0901 1.36127Z" fill="#374874"/>
|
||||
<path d="M23.3134 34.0182V41.2909L22.5585 41.7445V35.395L22.522 35.417L21 37.5104V36.6156L22.5585 34.4717L23.3134 34.0182Z" fill="white"/>
|
||||
<path d="M25.1062 40.2136V39.5744L27.1639 35.7102C27.4054 35.2573 27.6043 34.8703 27.7605 34.5492C27.9168 34.2256 28.0325 33.9407 28.1075 33.6943C28.1847 33.4444 28.2232 33.2082 28.2232 32.9856C28.2232 32.73 28.1705 32.5403 28.0649 32.4167C27.9614 32.2918 27.8194 32.2328 27.6388 32.2395C27.4582 32.2462 27.2553 32.3173 27.03 32.4526C26.7905 32.5965 26.5815 32.7801 26.403 33.0034C26.2264 33.2231 26.0894 33.4652 25.992 33.7297C25.8966 33.993 25.849 34.2631 25.849 34.5401L25.1306 34.9717C25.1306 34.5456 25.2148 34.1209 25.3832 33.6978C25.5517 33.2746 25.781 32.8858 26.0712 32.5316C26.3634 32.1761 26.6911 31.8892 27.0544 31.6709C27.4196 31.4514 27.7433 31.3469 28.0254 31.3574C28.3074 31.3678 28.5286 31.4776 28.6889 31.6866C28.8492 31.8957 28.9294 32.1873 28.9294 32.5613C28.9294 32.8288 28.8878 33.1154 28.8046 33.4211C28.7234 33.7232 28.5814 34.0915 28.3785 34.5259C28.1776 34.9567 27.8985 35.5032 27.5414 36.1652L26.1412 38.7537V38.8105L29.039 37.0693V37.8506L25.1062 40.2136Z" fill="white"/>
|
||||
<path d="M32.4025 35.929C32.0007 36.1704 31.6426 36.3051 31.328 36.3332C31.0155 36.3599 30.7669 36.2856 30.5823 36.1101C30.3996 35.931 30.3002 35.6558 30.2839 35.2844L31.051 34.8235C31.0673 35.0481 31.1362 35.209 31.258 35.3063C31.3798 35.4013 31.5391 35.4358 31.7359 35.4098C31.9327 35.3839 32.1509 35.299 32.3903 35.1551C32.6582 34.9941 32.8956 34.797 33.1026 34.5637C33.3096 34.3305 33.4719 34.0814 33.5896 33.8166C33.7073 33.5517 33.7662 33.2914 33.7662 33.0358C33.7662 32.7682 33.7094 32.5668 33.5957 32.4315C33.4821 32.2938 33.3157 32.2328 33.0965 32.2485C32.8774 32.2642 32.6095 32.3671 32.2929 32.5574L31.7937 32.8573V32.0761L32.2929 31.7761C32.5405 31.6273 32.7576 31.4448 32.9443 31.2285C33.1331 31.0109 33.2802 30.7757 33.3857 30.5229C33.4933 30.2689 33.547 30.014 33.547 29.7584C33.547 29.5121 33.5004 29.3259 33.407 29.1997C33.3137 29.0735 33.1818 29.0107 33.0113 29.0114C32.8429 29.0108 32.644 29.0794 32.4147 29.2171C32.1996 29.3464 31.9967 29.5145 31.8059 29.7214C31.6172 29.9248 31.463 30.1489 31.3432 30.3936C31.2235 30.636 31.1586 30.881 31.1484 31.1286L30.4179 31.5675C30.4301 31.179 30.5285 30.7861 30.7131 30.3887C30.8978 29.9889 31.1393 29.6189 31.4376 29.2787C31.7379 28.9372 32.0677 28.6586 32.4269 28.4428C32.8124 28.2111 33.1432 28.1035 33.4192 28.12C33.6952 28.1341 33.9072 28.2446 34.0554 28.4515C34.2035 28.6584 34.2776 28.9335 34.2776 29.2768C34.2776 29.6864 34.1852 30.091 34.0006 30.4908C33.8179 30.8894 33.5694 31.2388 33.2548 31.5391V31.5959C33.6485 31.4351 33.9559 31.4457 34.1771 31.6276C34.3983 31.8072 34.5089 32.1278 34.5089 32.5895C34.5089 32.9849 34.4166 33.3954 34.2319 33.8213C34.0493 34.2435 33.7997 34.6397 33.4831 35.0098C33.1665 35.38 32.8063 35.6864 32.4025 35.929Z" fill="white"/>
|
||||
<path d="M35.6078 32.4122V31.6878L38.3473 24.9849L38.7978 24.7142V25.8363L38.4934 26.0192L36.4235 31.084V31.1408L40.1128 28.9241V29.7053L35.6078 32.4122ZM38.5421 32.1405V24.8678L39.2605 24.4362V31.7089L38.5421 32.1405Z" fill="white"/>
|
||||
<path d="M43.2016 29.4403C42.8444 29.6549 42.5228 29.7653 42.2367 29.7715C41.9505 29.7777 41.7212 29.6882 41.5487 29.503C41.3762 29.3178 41.2819 29.0455 41.2656 28.6859L41.9962 28.2469C42.0246 28.5589 42.1524 28.7544 42.3797 28.8332C42.609 28.9085 42.883 28.8505 43.2016 28.659C43.4573 28.5054 43.6845 28.299 43.8834 28.0398C44.0843 27.7794 44.2416 27.4932 44.3552 27.181C44.4709 26.8653 44.5287 26.5524 44.5287 26.2423C44.5287 25.925 44.4689 25.6781 44.3491 25.5015C44.2314 25.3212 44.0691 25.2211 43.8621 25.2011C43.6551 25.181 43.4187 25.2497 43.1529 25.407C42.9621 25.5193 42.7663 25.6713 42.5654 25.863C42.3645 26.0524 42.1991 26.2405 42.0692 26.4275L41.3631 26.7524L41.7405 22.946L44.9792 21V21.7812L42.3736 23.3468L42.1545 25.6234L42.191 25.6015C42.3188 25.4063 42.4792 25.2117 42.6719 25.0178C42.8647 24.8238 43.0656 24.664 43.2746 24.5384C43.6561 24.3092 43.996 24.2115 44.2943 24.2453C44.5947 24.2756 44.8301 24.423 45.0005 24.6875C45.173 24.9508 45.2593 25.318 45.2593 25.7891C45.2593 26.2531 45.17 26.7211 44.9914 27.193C44.8149 27.6613 44.5713 28.094 44.2609 28.4913C43.9504 28.8862 43.5973 29.2025 43.2016 29.4403Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |
65
fusion_iot/iot/static/src/img/payment-terminal.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.1975 55.8797L21.2168 33.599C19.5277 31.8868 17.2915 30.8213 14.8976 30.5881L6.45126 29.7654C5.94413 29.716 5.50488 30.1146 5.50488 30.6241V32.9498C5.50488 36.3255 7.91158 39.2216 11.2303 39.8393L14.6401 40.474C16.0514 40.7367 17.3846 41.3162 18.5394 42.1689L41.4257 59.0676C41.7978 59.3423 42.3291 59.1912 42.5009 58.7618L43.3504 56.6379C43.4547 56.3773 43.3947 56.0796 43.1975 55.8797Z" fill="#C1DBF6"/>
|
||||
<path d="M42.5009 58.7618L43.3505 56.6379C43.4202 56.4637 43.4149 56.2734 43.3458 56.1065L59.028 47.1269C59.247 47.4855 59.3891 48.0016 59.2563 48.4799C59.1026 49.0339 58.1719 50.1703 58.1719 50.1703L42.4395 59.2022C42.3154 59.2735 42.1689 59.2953 42.0294 59.2631L41.7424 59.1971C42.0521 59.2422 42.3742 59.0785 42.5009 58.7618Z" fill="#FBDBD0"/>
|
||||
<path d="M41.6813 59.1829L41.6246 59.1699L41.6263 59.167C41.6445 59.1729 41.6628 59.1786 41.6813 59.1829Z" fill="#C1DBF6"/>
|
||||
<path d="M43.1975 55.8798L21.2168 33.599C19.5277 31.8868 17.2915 30.8213 14.8976 30.5882L6.45126 29.7655C6.22216 29.7432 6.00952 29.8156 5.84237 29.945L5.50488 29.7855L22.0194 20.3502L30.1213 21.1394C32.5702 21.3779 34.8579 22.4678 36.5859 24.2194L58.3978 46.3291C59.049 46.8533 59.2563 47.7038 59.2563 47.7038L43.3468 56.1088C43.3126 56.025 43.2635 55.9467 43.1975 55.8798Z" fill="white"/>
|
||||
<path d="M40.5028 30.7151L34.8061 25.1019L22.1959 32L27.873 37.6679L40.5028 30.7151Z" fill="#374874"/>
|
||||
<path d="M7.39734 17.5112C7.39734 17.5112 5.22856 18.7869 5.80267 21.4022L12.4365 17.5112C12.4365 17.5112 9.75748 15.9165 7.39734 17.5112Z" fill="#FBDBD0"/>
|
||||
<path d="M17.7947 28.355L9.65649 26.8879L16.8166 22.4669C16.8166 22.4669 19.2831 26.6115 17.7947 28.355Z" fill="#FBDBD0"/>
|
||||
<path d="M28.9893 22.8822L17.6937 29.0354L16.6465 28.379L27.6957 22.2383L28.9893 22.8822Z" fill="#374874"/>
|
||||
<path d="M17.7947 28.3551C17.7947 28.3551 18.0498 23.7624 13.7123 19.4248C10.3316 16.1717 7.39734 17.5112 7.39734 17.5112L17.7975 11.579C17.7975 11.579 20.5183 9.99002 25.0026 15.5338C27.1205 18.6558 27.6179 22.8694 27.6179 22.8694L17.7947 28.3551Z" fill="white"/>
|
||||
<path d="M42.8202 37.3184L41.3092 35.7455V35.0101L41.713 35.3893L42.9043 36.7573C42.9941 36.8442 43.1292 36.8646 43.2407 36.8081L45.31 35.5311V36.2965L43.15 37.3649C43.0411 37.4194 42.9098 37.4009 42.8202 37.3184Z" fill="#C1DBF6"/>
|
||||
<path d="M39.5032 39.126L37.9922 37.5531V36.8177L38.396 37.1969L39.5873 38.5649C39.6771 38.6518 39.8122 38.6722 39.9237 38.6157L41.9929 37.3387V38.1041L39.833 39.1726C39.7241 39.227 39.5928 39.2085 39.5032 39.126Z" fill="#C1DBF6"/>
|
||||
<path d="M36.0428 40.9336L34.5317 39.3607V38.6253L34.9355 39.0044L36.1268 40.3725C36.2167 40.4594 36.3517 40.4798 36.4632 40.4233L38.5325 39.1462V39.9117L36.3726 40.9801C36.2636 41.0346 36.1324 41.016 36.0428 40.9336Z" fill="#C1DBF6"/>
|
||||
<path d="M32.5823 42.7411L31.0713 41.1682V40.4329L31.4751 40.812L32.6664 42.18C32.7562 42.2669 32.8913 42.2873 33.0028 42.2308L35.072 40.9538V41.7192L32.9121 42.7877C32.8032 42.8421 32.6719 42.8236 32.5823 42.7411Z" fill="#C1DBF6"/>
|
||||
<path d="M39.5433 33.1428L40.3298 34.046C40.4197 34.1328 40.5547 34.1532 40.6662 34.0968L42.7355 32.8198V33.5852L40.5756 34.6536C40.4667 34.708 40.3354 34.6895 40.2458 34.607L39.5832 33.8966L39.5433 33.1428Z" fill="#C1DBF6"/>
|
||||
<path d="M36.1946 34.9139L37.0129 35.8536C37.1027 35.9404 37.2378 35.9608 37.3493 35.9044L39.4186 34.6274V35.3928L37.2587 36.4612C37.1497 36.5157 37.0185 36.4971 36.9289 36.4146L36.1434 35.58L36.1946 34.9139Z" fill="#C1DBF6"/>
|
||||
<path d="M32.7476 36.7369L33.5524 37.6611C33.6422 37.748 33.7773 37.7684 33.8888 37.7119L35.9581 36.4348V37.2003L33.7982 38.2687C33.6892 38.3232 33.5579 38.3047 33.4683 38.2222L32.7445 37.4497L32.7476 36.7369Z" fill="#C1DBF6"/>
|
||||
<path d="M29.3007 38.5601L30.0919 39.4686C30.1818 39.5556 30.3169 39.576 30.4284 39.5196L32.4976 38.2424V39.0079L30.3377 40.0764C30.2288 40.1308 30.0975 40.1123 30.0079 40.0298L29.3294 39.3032L29.3007 38.5601Z" fill="#C1DBF6"/>
|
||||
<path d="M45.563 40.0297L44.052 38.4569V37.7215L44.4558 38.1006L45.6471 39.4687C45.7369 39.5556 45.872 39.576 45.9835 39.5195L48.0528 38.2424V39.0079L45.8928 40.0763C45.7839 40.1308 45.6526 40.1122 45.563 40.0297Z" fill="#C1DBF6"/>
|
||||
<path d="M42.2461 41.8374L40.7351 40.2645V39.5291L41.1389 39.9082L42.3302 41.2763C42.42 41.3632 42.5551 41.3836 42.6666 41.3271L44.7359 40.0501V40.8155L42.576 41.8839C42.467 41.9384 42.3357 41.9199 42.2461 41.8374Z" fill="#C1DBF6"/>
|
||||
<path d="M38.7857 43.6449L37.2747 42.0721V41.3367L37.6785 41.7158L38.8697 43.0838C38.9596 43.1707 39.0946 43.1911 39.2062 43.1347L41.2754 41.8576V42.623L39.1155 43.6915C39.0066 43.7459 38.8753 43.7274 38.7857 43.6449Z" fill="#C1DBF6"/>
|
||||
<path d="M35.3252 45.4525L33.8142 43.8796V43.1442L34.218 43.5234L35.4093 44.8914C35.4991 44.9783 35.6342 44.9987 35.7457 44.9422L37.815 43.6652V44.4306L35.6551 45.499C35.5461 45.5535 35.4148 45.535 35.3252 45.4525Z" fill="#C1DBF6"/>
|
||||
<path d="M48.306 42.7411L46.7949 41.1682V40.4329L47.1987 40.812L48.39 42.18C48.4798 42.2669 48.6149 42.2873 48.7264 42.2308L50.7957 40.9538V41.7192L48.6358 42.7877C48.5268 42.8421 48.3956 42.8236 48.306 42.7411Z" fill="#C1DBF6"/>
|
||||
<path d="M44.9891 44.5487L43.478 42.9759V42.2405L43.8818 42.6196L45.0731 43.9876C45.1629 44.0745 45.298 44.0949 45.4095 44.0385L47.4788 42.7614V43.5269L45.3189 44.5953C45.2099 44.6498 45.0787 44.6312 44.9891 44.5487Z" fill="#C1DBF6"/>
|
||||
<path d="M41.5285 46.3563L40.0175 44.7834V44.048L40.4213 44.4272L41.6125 45.7952C41.7024 45.8821 41.8374 45.9025 41.949 45.846L44.0182 44.569V45.3344L41.8583 46.4028C41.7494 46.4573 41.6181 46.4388 41.5285 46.3563Z" fill="#C1DBF6"/>
|
||||
<path d="M38.068 48.1638L36.557 46.591V45.8556L36.9608 46.2347L38.1521 47.6027C38.2419 47.6896 38.377 47.7101 38.4885 47.6536L40.5578 46.3765V47.142L38.3979 48.2104C38.2889 48.2649 38.1576 48.2463 38.068 48.1638Z" fill="#C1DBF6"/>
|
||||
<path d="M51.0488 45.4525L49.5377 43.8796V43.1442L49.9415 43.5234L51.1328 44.8914C51.2226 44.9783 51.3577 44.9987 51.4692 44.9422L53.5385 43.6652V44.4306L51.3786 45.499C51.2696 45.5535 51.1384 45.535 51.0488 45.4525Z" fill="#C1DBF6"/>
|
||||
<path d="M47.7319 47.26L46.2208 45.6872V44.9518L46.6246 45.3309L47.8159 46.6989C47.9057 46.7858 48.0408 46.8062 48.1523 46.7498L50.2216 45.4727V46.2382L48.0617 47.3066C47.9527 47.3611 47.8215 47.3425 47.7319 47.26Z" fill="#C1DBF6"/>
|
||||
<path d="M44.2714 49.0677L42.7604 47.4948V46.7594L43.1642 47.1385L44.3554 48.5066C44.4453 48.5935 44.5804 48.6139 44.6919 48.5574L46.7611 47.2803V48.0458L44.6012 49.1142C44.4923 49.1687 44.361 49.1501 44.2714 49.0677Z" fill="#C1DBF6"/>
|
||||
<path d="M40.811 50.8752L39.2999 49.3023V48.567L39.7037 48.9461L40.895 50.3141C40.9848 50.401 41.1199 50.4214 41.2314 50.3649L43.3007 49.0879V49.8533L41.1408 50.9218C41.0318 50.9762 40.9006 50.9577 40.811 50.8752Z" fill="#C1DBF6"/>
|
||||
<path d="M53.7917 48.1638L52.2806 46.591V45.8556L52.6844 46.2347L53.8757 47.6027C53.9656 47.6896 54.1006 47.7101 54.2121 47.6536L56.2814 46.3765V47.142L54.1215 48.2104C54.0125 48.2649 53.8813 48.2463 53.7917 48.1638Z" fill="#C1DBF6"/>
|
||||
<path d="M50.4747 49.9714L48.9636 48.3985V47.6631L49.3674 48.0423L50.5587 49.4103C50.6485 49.4972 50.7836 49.5176 50.8951 49.4611L52.9644 48.1841V48.9495L50.8045 50.018C50.6955 50.0724 50.5643 50.0539 50.4747 49.9714Z" fill="#C1DBF6"/>
|
||||
<path d="M47.0142 51.779L45.5032 50.2061V49.4708L45.907 49.8499L47.0982 51.2179C47.1881 51.3048 47.3231 51.3252 47.4347 51.2688L49.5039 49.9917V50.7571L47.344 51.8256C47.2351 51.88 47.1038 51.8615 47.0142 51.779Z" fill="#C1DBF6"/>
|
||||
<path d="M43.5538 53.5866L42.0427 52.0137V51.2783L42.4465 51.6575L43.6378 53.0255C43.7276 53.1124 43.8627 53.1328 43.9742 53.0763L46.0435 51.7993V52.5647L43.8836 53.6331C43.7746 53.6876 43.6434 53.6691 43.5538 53.5866Z" fill="#C1DBF6"/>
|
||||
<path d="M45.224 35.3725L43.7182 33.8667C43.627 33.7755 43.4869 33.7546 43.373 33.8152L41.4647 34.8314C41.289 34.9249 41.2555 35.1627 41.3986 35.3011L42.9043 36.7573C42.9941 36.8442 43.1292 36.8647 43.2407 36.8082L45.1491 35.8416C45.3272 35.7514 45.3651 35.5136 45.224 35.3725Z" fill="white"/>
|
||||
<path d="M41.9071 37.1801L40.4014 35.6743C40.3101 35.5831 40.17 35.5622 40.0561 35.6228L38.1478 36.6389C37.9721 36.7325 37.9386 36.9702 38.0817 37.1086L39.5874 38.5649C39.6772 38.6518 39.8123 38.6722 39.9238 38.6157L41.8322 37.6491C42.0103 37.559 42.0482 37.3212 41.9071 37.1801Z" fill="white"/>
|
||||
<path d="M38.4467 38.9876L36.9409 37.4818C36.8497 37.3906 36.7095 37.3697 36.5956 37.4304L34.6873 38.4465C34.5116 38.54 34.4782 38.7778 34.6213 38.9162L36.1269 40.3725C36.2168 40.4594 36.3518 40.4798 36.4633 40.4233L38.3718 39.4567C38.5498 39.3665 38.5878 39.1287 38.4467 38.9876Z" fill="white"/>
|
||||
<path d="M34.9861 40.7952L33.4803 39.2894C33.3891 39.1982 33.249 39.1773 33.1351 39.2379L31.2268 40.254C31.051 40.3476 31.0176 40.5853 31.1607 40.7237L32.6663 42.18C32.7562 42.2669 32.8913 42.2873 33.0028 42.2308L34.9112 41.2642C35.0892 41.1741 35.1272 40.9363 34.9861 40.7952Z" fill="white"/>
|
||||
<path d="M42.0398 32.0515L42.6495 32.6611C42.7906 32.8022 42.7526 33.04 42.5746 33.1301L40.6661 34.0968C40.5546 34.1533 40.4196 34.1328 40.3298 34.046L39.5822 33.3229C39.516 33.2589 39.5316 33.149 39.613 33.106L41.7028 32.0007C41.8141 31.9418 41.9508 31.9624 42.0398 32.0515Z" fill="white"/>
|
||||
<path d="M38.6881 33.8242L39.3326 34.4688C39.4737 34.6099 39.4357 34.8476 39.2577 34.9378L37.3493 35.9044C37.2377 35.9609 37.1027 35.9405 37.0128 35.8535L36.2296 35.096C36.1635 35.032 36.1791 34.9221 36.2604 34.8791L38.351 33.7734C38.4623 33.7145 38.599 33.7351 38.6881 33.8242Z" fill="white"/>
|
||||
<path d="M35.2394 35.6435L35.8722 36.2762C36.0133 36.4173 35.9753 36.6551 35.7973 36.7453L33.8888 37.712C33.7773 37.7685 33.6422 37.7481 33.5524 37.6612L32.7919 36.9255C32.722 36.858 32.7385 36.7418 32.8244 36.6964L34.9092 35.5938C35.0183 35.5361 35.1521 35.5562 35.2394 35.6435Z" fill="white"/>
|
||||
<path d="M31.7937 37.4658L32.4117 38.0838C32.5528 38.2249 32.5148 38.4627 32.3368 38.5529L30.4285 39.5195C30.3169 39.576 30.1818 39.5556 30.0919 39.4686L29.3531 38.7541C29.28 38.6834 29.2972 38.5619 29.3872 38.5143L31.4635 37.4161C31.5725 37.3584 31.7065 37.3786 31.7937 37.4658Z" fill="white"/>
|
||||
<path d="M47.9668 38.0839L46.461 36.5781C46.3698 36.4869 46.2297 36.466 46.1158 36.5266L44.2075 37.5427C44.0318 37.6363 43.9983 37.874 44.1414 38.0124L45.6471 39.4687C45.7369 39.5556 45.872 39.576 45.9835 39.5195L47.8919 38.5529C48.07 38.4628 48.1079 38.225 47.9668 38.0839Z" fill="white"/>
|
||||
<path d="M44.6499 39.8914L43.1441 38.3857C43.0529 38.2944 42.9128 38.2735 42.7989 38.3342L40.8906 39.3503C40.7149 39.4439 40.6814 39.6816 40.8245 39.82L42.3302 41.2763C42.42 41.3632 42.5551 41.3836 42.6666 41.3271L44.575 40.3605C44.7531 40.2703 44.791 40.0325 44.6499 39.8914Z" fill="white"/>
|
||||
<path d="M41.1895 41.699L39.6837 40.1932C39.5925 40.102 39.4523 40.0811 39.3384 40.1417L37.4301 41.1578C37.2544 41.2514 37.221 41.4891 37.3641 41.6275L38.8697 43.0838C38.9596 43.1707 39.0946 43.1911 39.2061 43.1347L41.1146 42.1681C41.2926 42.0779 41.3306 41.8401 41.1895 41.699Z" fill="white"/>
|
||||
<path d="M37.729 43.5065L36.2232 42.0008C36.132 41.9095 35.9919 41.8886 35.878 41.9493L33.9697 42.9654C33.794 43.059 33.7605 43.2967 33.9036 43.4351L35.4093 44.8914C35.4991 44.9783 35.6342 44.9987 35.7457 44.9422L37.6541 43.9756C37.8322 43.8854 37.8701 43.6476 37.729 43.5065Z" fill="white"/>
|
||||
<path d="M50.7097 40.7952L49.204 39.2894C49.1127 39.1982 48.9726 39.1773 48.8587 39.2379L46.9504 40.254C46.7747 40.3476 46.7412 40.5853 46.8844 40.7237L48.39 42.18C48.4798 42.2669 48.6149 42.2873 48.7264 42.2308L50.6348 41.2642C50.8129 41.1741 50.8508 40.9363 50.7097 40.7952Z" fill="white"/>
|
||||
<path d="M47.3928 42.6028L45.8871 41.097C45.7958 41.0058 45.6557 40.9849 45.5418 41.0455L43.6335 42.0616C43.4578 42.1552 43.4243 42.393 43.5675 42.5314L45.0731 43.9876C45.1629 44.0745 45.298 44.0949 45.4095 44.0385L47.3179 43.0719C47.496 42.9817 47.5339 42.7439 47.3928 42.6028Z" fill="white"/>
|
||||
<path d="M43.9324 44.4103L42.4266 42.9046C42.3354 42.8134 42.1953 42.7924 42.0814 42.8531L40.1731 43.8692C39.9973 43.9628 39.9639 44.2005 40.107 44.3389L41.6126 45.7952C41.7025 45.8821 41.8375 45.9025 41.9491 45.846L43.8575 44.8794C44.0355 44.7892 44.0735 44.5514 43.9324 44.4103Z" fill="white"/>
|
||||
<path d="M40.4718 46.2179L38.966 44.7121C38.8748 44.6209 38.7347 44.6 38.6208 44.6606L36.7125 45.6768C36.5368 45.7703 36.5033 46.0081 36.6464 46.1465L38.1521 47.6027C38.2419 47.6896 38.377 47.7101 38.4885 47.6536L40.3969 46.687C40.575 46.5968 40.6129 46.359 40.4718 46.2179Z" fill="white"/>
|
||||
<path d="M53.4525 43.5065L51.9468 42.0008C51.8555 41.9095 51.7154 41.8886 51.6015 41.9493L49.6932 42.9654C49.5175 43.059 49.484 43.2967 49.6271 43.4351L51.1328 44.8914C51.2226 44.9783 51.3577 44.9987 51.4692 44.9422L53.3776 43.9756C53.5557 43.8854 53.5936 43.6476 53.4525 43.5065Z" fill="white"/>
|
||||
<path d="M50.1356 45.3141L48.6299 43.8084C48.5386 43.7172 48.3985 43.6963 48.2846 43.7569L46.3763 44.773C46.2006 44.8666 46.1671 45.1043 46.3103 45.2427L47.8159 46.699C47.9057 46.7859 48.0408 46.8063 48.1523 46.7498L50.0607 45.7832C50.2388 45.6931 50.2767 45.4552 50.1356 45.3141Z" fill="white"/>
|
||||
<path d="M46.6752 47.1217L45.1694 45.6159C45.0782 45.5247 44.9381 45.5038 44.8242 45.5644L42.9159 46.5806C42.7401 46.6741 42.7067 46.9119 42.8498 47.0503L44.3554 48.5066C44.4453 48.5935 44.5803 48.6139 44.6919 48.5574L46.6003 47.5908C46.7783 47.5006 46.8163 47.2628 46.6752 47.1217Z" fill="white"/>
|
||||
<path d="M43.2147 48.9293L41.709 47.4235C41.6177 47.3323 41.4776 47.3114 41.3637 47.372L39.4554 48.3881C39.2797 48.4817 39.2462 48.7194 39.3894 48.8578L40.895 50.3141C40.9848 50.401 41.1199 50.4214 41.2314 50.3649L43.1398 49.3983C43.3179 49.3082 43.3558 49.0704 43.2147 48.9293Z" fill="white"/>
|
||||
<path d="M56.1954 46.2179L54.6897 44.7121C54.5985 44.6209 54.4583 44.6 54.3444 44.6606L52.4361 45.6768C52.2604 45.7703 52.227 46.0081 52.3701 46.1465L53.8757 47.6027C53.9655 47.6896 54.1006 47.7101 54.2121 47.6536L56.1206 46.687C56.2986 46.5968 56.3365 46.359 56.1954 46.2179Z" fill="white"/>
|
||||
<path d="M52.8784 48.0255L51.3727 46.5198C51.2814 46.4285 51.1413 46.4076 51.0274 46.4683L49.1191 47.4844C48.9434 47.5779 48.9099 47.8157 49.0531 47.9541L50.5587 49.4104C50.6485 49.4973 50.7836 49.5177 50.8951 49.4612L52.8035 48.4946C52.9816 48.4044 53.0195 48.1666 52.8784 48.0255Z" fill="white"/>
|
||||
<path d="M49.418 49.8331L47.9122 48.3273C47.821 48.2361 47.6809 48.2152 47.567 48.2758L45.6587 49.2919C45.4829 49.3855 45.4495 49.6232 45.5926 49.7616L47.0982 51.2179C47.1881 51.3048 47.3231 51.3252 47.4347 51.2687L49.3431 50.3021C49.5211 50.212 49.5591 49.9742 49.418 49.8331Z" fill="white"/>
|
||||
<path d="M45.9575 51.6406L44.4518 50.1349C44.3605 50.0436 44.2204 50.0227 44.1065 50.0834L42.1982 51.0995C42.0225 51.1931 41.989 51.4308 42.1322 51.5692L43.6378 53.0255C43.7276 53.1124 43.8627 53.1328 43.9742 53.0763L45.8826 52.1097C46.0607 52.0195 46.0986 51.7817 45.9575 51.6406Z" fill="white"/>
|
||||
<path d="M18.8349 11.3681C20.0144 11.3681 22.1523 12.01 25.0026 15.5338C26.1845 17.276 26.8613 19.3576 27.2291 20.8577L30.1214 21.1394C32.5703 21.3779 34.8579 22.4679 36.5859 24.2194L58.3978 46.3291C58.6907 46.565 58.8932 46.8663 59.028 47.127C59.247 47.4856 59.389 48.0016 59.2563 48.4799C59.1026 49.0339 58.1719 50.1703 58.1719 50.1703L42.4395 59.2022C42.3528 59.252 42.2553 59.2777 42.1568 59.2777C42.1142 59.2777 42.0715 59.2729 42.0294 59.2631L41.7424 59.1971C41.7505 59.1982 41.7586 59.1975 41.7667 59.1984C41.738 59.1947 41.7093 59.1897 41.6807 59.1828L41.6581 59.1762C41.6585 59.1763 41.6589 59.1764 41.6593 59.1765C41.6586 59.1763 41.6579 59.1762 41.6573 59.176L41.6334 59.1691C41.5613 59.147 41.4908 59.1157 41.4258 59.0676L18.5394 42.169C17.3846 41.3163 16.0514 40.7368 14.6402 40.4741L11.2304 39.8394C7.91164 39.2217 5.50494 36.3256 5.50494 32.9498V30.6241C5.50494 30.3446 5.63968 30.1019 5.84242 29.945L16.5955 23.4491C15.9891 22.1834 15.079 20.7915 13.7122 19.4248C13.0133 18.7522 12.3341 18.2784 11.6953 17.946L5.80261 21.4023C5.2285 18.787 7.39728 17.5112 7.39728 17.5112C7.5734 17.3922 7.75134 17.293 7.92978 17.2075L17.7974 11.5791C17.7974 11.5791 18.1587 11.3681 18.8349 11.3681ZM5.84277 29.9448C5.84274 29.9448 5.84279 29.9447 5.84277 29.9448V29.9448ZM41.6263 59.167L41.6334 59.1691C41.6392 59.1708 41.6451 59.1726 41.6509 59.1743C41.6427 59.1719 41.6344 59.1697 41.6263 59.167ZM41.6509 59.1743C41.6531 59.1748 41.6552 59.1754 41.6573 59.176L41.6581 59.1762C41.6557 59.1755 41.6533 59.1749 41.6509 59.1743ZM18.8349 10.4538C17.9558 10.4538 17.4324 10.7335 17.3364 10.7895L7.50405 16.3978C7.29161 16.5011 7.09609 16.613 6.90809 16.7384C5.95662 17.3119 4.34814 19.0403 4.90965 21.5982L5.17952 22.8277L6.26524 22.1908L11.7027 19.0017C12.1689 19.2905 12.6279 19.6509 13.0719 20.0774C14.0042 21.0107 14.7811 22.0279 15.3891 23.1097L5.36997 29.1623L5.32487 29.1895L5.28293 29.2219C4.843 29.5624 4.5907 30.0735 4.5907 30.6241V32.9498C4.5907 36.7646 7.31276 40.0402 11.0631 40.7383L14.4729 41.373C15.7407 41.609 16.9591 42.1386 17.9964 42.9045L40.8827 59.8031C41.0184 59.9034 41.1734 59.9818 41.3437 60.0364C41.3641 60.043 41.3846 60.0491 41.4051 60.0548L41.4065 60.0553L41.4077 60.0556L41.4255 60.0608L41.4454 60.0666L41.4656 60.0715C41.4891 60.0772 41.5131 60.0824 41.5375 60.0871L41.5373 60.0881L41.8243 60.1542C41.9331 60.1793 42.045 60.192 42.1568 60.192C42.4155 60.192 42.6707 60.1239 42.895 59.9951L58.6272 50.9632L58.7729 50.8796L58.8793 50.7497C59.1263 50.4481 59.9469 49.4108 60.1374 48.7244C60.318 48.0734 60.2014 47.3114 59.8257 46.6793C59.6083 46.2667 59.336 45.9219 59.0155 45.6533L37.2368 23.5774C35.3622 21.6772 32.8667 20.4882 30.2101 20.2295L27.9529 20.0097C27.5465 18.5373 26.8628 16.6474 25.7592 15.0206L25.7377 14.9888L25.7135 14.9589C22.7016 11.2354 20.3105 10.4538 18.8349 10.4538Z" fill="#374874"/>
|
||||
<path d="M19.8042 24.8183C19.7247 24.8183 19.6477 24.777 19.6055 24.7031C19.5428 24.5935 19.581 24.4538 19.6906 24.3913L25.4952 21.0746C25.6048 21.0112 25.7443 21.05 25.8071 21.1596C25.8698 21.2692 25.8316 21.4089 25.722 21.4714L19.9174 24.7882C19.8816 24.8087 19.8426 24.8183 19.8042 24.8183Z" fill="#374874"/>
|
||||
<path d="M21.5676 21.306C21.4881 21.306 21.4111 21.2647 21.3689 21.1908C21.3062 21.0812 21.3444 20.9415 21.454 20.879L24.9049 18.9071C25.0145 18.8439 25.154 18.8826 25.2167 18.9922C25.2795 19.1018 25.2413 19.2415 25.1317 19.304L21.6808 21.2759C21.6451 21.2964 21.606 21.306 21.5676 21.306Z" fill="#374874"/>
|
||||
<path d="M20.3267 18.9487C20.2473 18.9487 20.1703 18.9074 20.1281 18.8335C20.0654 18.7239 20.1035 18.5844 20.2131 18.5216L23.789 16.4781C23.8986 16.4147 24.0381 16.4536 24.1009 16.5632C24.1636 16.6728 24.1254 16.8123 24.0158 16.875L20.4399 18.9185C20.4042 18.9391 20.3651 18.9487 20.3267 18.9487Z" fill="#374874"/>
|
||||
<path d="M17.7182 20.5393C17.6388 20.5393 17.5617 20.498 17.5196 20.4241C17.4569 20.3145 17.495 20.1748 17.6046 20.1122L19.1473 19.2308C19.2566 19.1676 19.3964 19.2062 19.4591 19.3158C19.5218 19.4254 19.4836 19.5652 19.374 19.6277L17.8314 20.5091C17.7957 20.5297 17.7566 20.5393 17.7182 20.5393Z" fill="#374874"/>
|
||||
<path d="M18.327 23.1362C18.2475 23.1362 18.1705 23.0949 18.1283 23.021C18.0656 22.9114 18.1038 22.7717 18.2134 22.7092L19.756 21.8277C19.8654 21.7645 20.0051 21.8032 20.0679 21.9128C20.1306 22.0224 20.0924 22.1621 19.9828 22.2246L18.4402 23.1061C18.4045 23.1266 18.3654 23.1362 18.327 23.1362Z" fill="#374874"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
17
fusion_iot/iot/static/src/img/printer.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
57
fusion_iot/iot/static/src/img/rulers.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M42.8964 20.9462C42.8701 20.9311 42.8415 20.9174 42.8111 20.9054C42.7875 20.896 42.7628 20.8878 42.7373 20.8805C42.7298 20.8783 42.7219 20.8767 42.7143 20.8749C42.7111 20.8741 42.708 20.8733 42.7048 20.8725C42.6941 20.8698 42.6837 20.8669 42.6728 20.8647C42.6531 20.8605 42.633 20.8571 42.6127 20.8542C42.5935 20.8514 42.5741 20.8493 42.5544 20.8477C42.5352 20.8461 42.5158 20.845 42.4964 20.8445C42.4949 20.8445 42.4935 20.8446 42.492 20.8445C42.4911 20.8445 42.4902 20.8446 42.4893 20.8446C42.4719 20.8442 42.4544 20.8442 42.4368 20.8448C42.4156 20.8456 42.3943 20.847 42.3732 20.8491C42.3487 20.8515 42.3244 20.8549 42.3002 20.8592C42.292 20.8607 42.2841 20.8636 42.2758 20.8654C42.2677 20.8671 42.2601 20.8693 42.252 20.8713C42.2318 20.8762 42.2112 20.8801 42.1916 20.8864C42.1444 20.9016 42.0993 20.9213 42.0575 20.9456L31.7854 26.915C31.7829 26.9165 31.7805 26.918 31.7781 26.9194C31.7672 26.926 31.7595 26.9335 31.7497 26.9404C31.7347 26.951 31.7189 26.9613 31.7065 26.9725C31.7053 26.9735 31.7035 26.9745 31.7024 26.9755C31.6884 26.9883 31.6762 27.0016 31.6655 27.0152C31.6631 27.0181 31.6615 27.0213 31.6594 27.0242C31.657 27.0274 31.655 27.0306 31.6528 27.034C31.6491 27.0395 31.6448 27.045 31.6416 27.0507C31.6353 27.0618 31.6301 27.073 31.6258 27.0843C31.6252 27.0859 31.625 27.0876 31.6244 27.0892C31.6237 27.0913 31.6233 27.0934 31.6226 27.0955C31.6203 27.1029 31.6178 27.1102 31.6163 27.1175C31.614 27.1287 31.6128 27.14 31.6124 27.1513C31.6124 27.153 31.6124 27.1548 31.6124 27.1565L31.6089 28.3811C31.6089 28.3606 31.6128 28.3402 31.6192 28.32C31.6198 28.318 31.6202 28.3159 31.621 28.3137C31.6275 28.2949 31.6373 28.2766 31.6494 28.2586C31.6516 28.2553 31.6536 28.252 31.6559 28.2488C31.6689 28.231 31.6846 28.2136 31.7031 28.197C31.7155 28.1858 31.7313 28.1755 31.7462 28.1651C31.7582 28.1566 31.7683 28.1476 31.782 28.1397L42.054 22.1702C42.1132 22.1358 42.1797 22.1125 42.2485 22.0959C42.2565 22.094 42.2643 22.0916 42.2724 22.0899C42.3422 22.0752 42.4144 22.0679 42.4858 22.0692C42.4868 22.0692 42.4877 22.0692 42.4886 22.0692C42.562 22.0707 42.6341 22.0806 42.7013 22.097C42.7045 22.0978 42.7077 22.0986 42.7109 22.0994C42.7781 22.1166 42.8405 22.1405 42.893 22.1708C42.9965 22.2306 43.0636 22.3138 43.0633 22.4127L43.0668 21.1882C43.067 21.0892 42.9999 21.006 42.8964 20.9462Z" fill="#C1DBF6"/>
|
||||
<path d="M51.0548 45.192C51.0544 45.1928 51.0537 45.1934 51.0533 45.1942C51.0359 45.2232 51.0111 45.2493 50.9823 45.2732C50.9749 45.2794 50.9665 45.285 50.9584 45.2908C50.938 45.3056 50.9146 45.3185 50.8905 45.331C50.8688 45.3421 50.846 45.3524 50.8218 45.3615C50.7958 45.3714 50.7693 45.3806 50.741 45.3879C50.7271 45.3916 50.7135 45.3954 50.6992 45.3984C50.6554 45.4077 50.6101 45.4146 50.5636 45.4178C50.5595 45.4181 50.5554 45.4184 50.5512 45.4186C50.4995 45.4214 50.447 45.4205 50.395 45.4152C50.3921 45.4149 50.3892 45.4145 50.3863 45.4142C50.3337 45.4084 50.2817 45.3983 50.2318 45.3835C50.2286 45.3825 50.2256 45.3813 50.2225 45.3803C50.1723 45.3647 50.124 45.3449 50.0797 45.3194L19.0409 27.3992C18.9239 27.3316 18.8655 27.2429 18.8658 27.1545L18.8623 28.379C18.8621 28.4674 18.9204 28.5561 19.0374 28.6237L50.0763 46.544C50.104 46.56 50.1333 46.5741 50.1636 46.5861C50.1816 46.5933 50.2003 46.5991 50.219 46.6049C50.2222 46.606 50.2251 46.6072 50.2283 46.6081C50.2309 46.6089 50.2333 46.6099 50.2358 46.6107C50.2558 46.6164 50.2762 46.6214 50.2968 46.6257C50.3156 46.6296 50.3346 46.6329 50.3538 46.6356C50.3634 46.637 50.3732 46.6378 50.3829 46.6389C50.3858 46.6392 50.3886 46.6395 50.3915 46.6398C50.3977 46.6404 50.4037 46.6414 50.4099 46.6419C50.4289 46.6434 50.4479 46.6445 50.4669 46.6449C50.4869 46.6454 50.5069 46.6453 50.5267 46.6446C50.5337 46.6444 50.5406 46.6436 50.5476 46.6432C50.5519 46.643 50.5561 46.6426 50.5604 46.6423C50.5711 46.6416 50.582 46.6412 50.5926 46.6402C50.6193 46.6376 50.6456 46.6339 50.6713 46.6292C50.6798 46.6276 50.6874 46.6248 50.6957 46.623C50.7102 46.62 50.7236 46.6162 50.7377 46.6126C50.7539 46.6083 50.771 46.6054 50.7865 46.6003C50.7981 46.5965 50.8071 46.5904 50.8183 46.5862C50.8428 46.5769 50.8652 46.5667 50.8871 46.5554C50.8979 46.5499 50.9111 46.5463 50.9211 46.5403C50.9337 46.5328 50.9435 46.5237 50.9549 46.5154C50.9631 46.5095 50.9714 46.5041 50.9789 46.4978C50.9835 46.4939 50.9895 46.4908 50.9938 46.4868C51.0072 46.4746 51.0192 46.4617 51.0298 46.4483C51.0373 46.4389 51.0437 46.429 51.0497 46.4189C51.0502 46.4181 51.0509 46.4173 51.0514 46.4165C51.0519 46.4156 51.0526 46.4148 51.0531 46.4139C51.0592 46.4031 51.0645 46.3919 51.0688 46.3805C51.073 46.3694 51.0762 46.358 51.0786 46.3464C51.0808 46.3353 51.0821 46.3241 51.0826 46.3126L51.0861 45.088C51.0846 45.1252 51.0735 45.1599 51.0548 45.192Z" fill="#FBDBD0"/>
|
||||
<path d="M51.086 45.0879C51.0743 45.388 50.448 45.532 50.0797 45.3194L19.0409 27.3991C18.8087 27.265 18.8075 27.048 19.0362 26.9151L51.6133 7.98338C51.8646 7.83732 52.2375 7.86146 52.4527 7.98569C52.559 8.04708 52.6268 8.13293 52.6223 8.23476L51.086 45.0879ZM41.5749 33.0505C41.9432 33.2632 42.5695 33.117 42.5812 32.8168L43.0665 21.197C43.0709 21.0941 43.003 21.0078 42.8964 20.9462C42.6815 20.8222 42.3096 20.799 42.0574 20.9456L31.7854 26.9151C31.5548 27.0491 31.5541 27.265 31.7862 27.399L41.5749 33.0505Z" fill="white"/>
|
||||
<path d="M52.0244 7.883C52.1837 7.883 52.3399 7.92063 52.4527 7.98575C52.559 8.04714 52.6269 8.13299 52.6224 8.23485L51.0861 45.0879L51.0826 46.3125C51.0821 46.324 51.0808 46.3353 51.0786 46.3463C51.0762 46.3579 51.073 46.3693 51.0688 46.3804C51.0645 46.3918 51.0592 46.403 51.0531 46.4139C51.0526 46.4148 51.0519 46.4156 51.0514 46.4164C51.0509 46.4172 51.0502 46.4181 51.0497 46.4188C51.0437 46.4289 51.0373 46.4388 51.0298 46.4483C51.0192 46.4617 51.0072 46.4745 50.9938 46.4868C50.9895 46.4907 50.9835 46.4939 50.9788 46.4978C50.9714 46.504 50.963 46.5094 50.9549 46.5154C50.9435 46.5236 50.9337 46.5327 50.9211 46.5403C50.911 46.5463 50.8979 46.5499 50.8871 46.5554C50.8652 46.5667 50.8427 46.5768 50.8183 46.5861C50.8071 46.5903 50.7981 46.5964 50.7865 46.6003C50.771 46.6053 50.7538 46.6082 50.7376 46.6125C50.7236 46.6162 50.7102 46.6199 50.6957 46.623C50.6874 46.6248 50.6797 46.6276 50.6712 46.6292C50.6455 46.6339 50.6192 46.6375 50.5926 46.6402C50.5819 46.6412 50.5711 46.6416 50.5603 46.6423C50.556 46.6426 50.5519 46.643 50.5475 46.6432C50.5406 46.6435 50.5337 46.6444 50.5266 46.6446C50.5148 46.645 50.5029 46.6451 50.4909 46.6451C50.4829 46.6451 50.4749 46.6451 50.4668 46.6449C50.4478 46.6444 50.4288 46.6434 50.4098 46.6418C50.4037 46.6413 50.3976 46.6404 50.3915 46.6398C50.3885 46.6395 50.3857 46.6391 50.3828 46.6388C50.3731 46.6377 50.3634 46.6369 50.3538 46.6355C50.3346 46.6328 50.3156 46.6295 50.2967 46.6257C50.2761 46.6214 50.2558 46.6163 50.2358 46.6106C50.2332 46.6099 50.2309 46.6089 50.2283 46.608C50.2251 46.6071 50.2222 46.6059 50.219 46.6049C50.2003 46.5991 50.1816 46.5932 50.1636 46.586C50.1333 46.574 50.104 46.56 50.0763 46.5439L19.0374 28.6237C18.9204 28.5561 18.8621 28.4673 18.8623 28.379L18.8658 27.1545C18.8658 27.1548 18.866 27.1552 18.866 27.1556C18.8657 27.0684 18.9224 26.9813 19.0363 26.9152L51.6133 7.98344C51.7329 7.91395 51.8799 7.883 52.0244 7.883ZM52.0244 6.96872C51.7074 6.96872 51.4064 7.04625 51.154 7.19295L18.5769 26.1247C18.1864 26.3514 17.9529 26.7351 17.9517 27.1518L17.948 28.3764C17.9469 28.7977 18.1831 29.186 18.58 29.4153L49.6191 47.3357C49.6842 47.3733 49.7539 47.407 49.8259 47.4357C49.8587 47.4488 49.8937 47.4609 49.9293 47.4723L49.9389 47.4757L49.9495 47.4795L49.9869 47.4904C50.0249 47.5013 50.067 47.5117 50.1095 47.5206C50.1506 47.529 50.1876 47.5354 50.225 47.5407C50.2412 47.5431 50.2602 47.5453 50.2792 47.5473L50.2822 47.5476L50.2903 47.5485L50.2917 47.5487L50.3048 47.5502L50.3356 47.5531C50.3717 47.5561 50.4086 47.558 50.4455 47.5589C50.4597 47.5593 50.4754 47.5594 50.491 47.5594C50.5123 47.5594 50.5336 47.5591 50.5547 47.5584C50.5744 47.5578 50.5931 47.5567 50.6118 47.5552L50.6249 47.5543C50.6418 47.5534 50.6603 47.5521 50.6786 47.5504C50.7352 47.5447 50.7857 47.5376 50.8352 47.5287C50.8651 47.523 50.8896 47.5173 50.9127 47.5112C50.9291 47.5074 50.9453 47.5033 50.9617 47.4989C50.9999 47.4901 51.0351 47.4808 51.0685 47.4699C51.1147 47.4547 51.1515 47.4392 51.1798 47.4262C51.21 47.4139 51.2395 47.4007 51.269 47.3865C51.3018 47.3725 51.3428 47.3532 51.3882 47.3262C51.4362 47.2975 51.4724 47.271 51.4997 47.2498C51.5076 47.2441 51.5153 47.2384 51.5229 47.2327C51.5445 47.2172 51.5753 47.1937 51.6086 47.1635C51.6615 47.1149 51.7073 47.0658 51.7479 47.0141C51.7687 46.988 51.7892 46.9594 51.8092 46.9286L51.8126 46.9238L51.8247 46.907L51.8508 46.8605C51.8783 46.812 51.9036 46.7577 51.9244 46.7026C51.9458 46.6458 51.9629 46.5863 51.975 46.5261C51.9869 46.4667 51.9939 46.4066 51.9962 46.3469L51.9968 46.331L51.9968 46.315L52.0003 45.1083L53.5359 8.27288C53.5552 7.83555 53.3212 7.43137 52.9099 7.19389C52.6583 7.04863 52.3438 6.96872 52.0244 6.96872Z" fill="#374874"/>
|
||||
<path d="M42.4698 22.5262L42.4714 22.5263L42.4792 22.5267L42.487 22.5265C42.5085 22.5273 42.5305 22.5295 42.5532 22.5333L42.1296 32.6752C42.0943 32.6853 42.0456 32.6941 41.9882 32.6941C41.9153 32.6941 41.8463 32.6794 41.8035 32.6547L33.3354 27.7657L42.2837 22.5655C42.2906 22.5615 42.3114 22.551 42.3541 22.5407L42.3612 22.5387L42.3664 22.5374C42.4015 22.53 42.4363 22.5262 42.4698 22.5262ZM42.4698 22.0691C42.4036 22.0691 42.337 22.0765 42.2723 22.09C42.2643 22.0917 42.2565 22.0941 42.2485 22.0959C42.1797 22.1126 42.1131 22.1359 42.054 22.1703L32.4233 27.767L41.5749 33.0506C41.6953 33.1201 41.8432 33.1513 41.9882 33.1513C42.2869 33.1513 42.5733 33.019 42.5812 32.8169L43.0211 22.2848C42.9913 22.2412 42.9477 22.2026 42.8929 22.1709C42.8404 22.1406 42.7781 22.1167 42.7108 22.0995C42.7077 22.0987 42.7044 22.0979 42.7013 22.0971C42.634 22.0807 42.562 22.0708 42.4886 22.0693H42.4858C42.4805 22.0692 42.4752 22.0691 42.4698 22.0691Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1458 26.1477C20.1773 26.093 20.2471 26.0741 20.3018 26.1055L22.1464 27.165C22.2011 27.1964 22.22 27.2663 22.1886 27.321C22.1572 27.3757 22.0873 27.3946 22.0326 27.3632L20.188 26.3037C20.1333 26.2723 20.1144 26.2024 20.1458 26.1477Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.5205 25.3571C21.5521 25.3025 21.622 25.2838 21.6766 25.3154L22.5432 25.8165C22.5979 25.8481 22.6165 25.918 22.585 25.9726C22.5534 26.0273 22.4835 26.0459 22.4288 26.0143L21.5622 25.5133C21.5076 25.4817 21.4889 25.4118 21.5205 25.3571Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8951 24.5669C22.9267 24.5122 22.9966 24.4935 23.0513 24.5251L23.9179 25.0262C23.9725 25.0578 23.9912 25.1277 23.9596 25.1823C23.928 25.237 23.8581 25.2557 23.8034 25.2241L22.9369 24.723C22.8822 24.6914 22.8635 24.6215 22.8951 24.5669Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.2698 23.7766C24.3014 23.7219 24.3713 23.7033 24.4259 23.7349L25.2925 24.2359C25.3471 24.2675 25.3658 24.3374 25.3342 24.3921C25.3026 24.4467 25.2327 24.4654 25.1781 24.4338L24.3115 23.9327C24.2569 23.9011 24.2382 23.8312 24.2698 23.7766Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.6444 22.9863C25.676 22.9317 25.7459 22.913 25.8005 22.9446L26.6671 23.4456C26.7218 23.4772 26.7404 23.5471 26.7089 23.6018C26.6773 23.6564 26.6074 23.6751 26.5527 23.6435L25.6861 23.1424C25.6315 23.1108 25.6128 23.0409 25.6444 22.9863Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0187 22.1963C27.0502 22.1416 27.12 22.1227 27.1748 22.1541L29.0194 23.2136C29.0741 23.245 29.093 23.3149 29.0615 23.3696C29.0301 23.4243 28.9603 23.4432 28.9055 23.4118L27.0609 22.3523C27.0062 22.3209 26.9873 22.251 27.0187 22.1963Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.3933 21.4062C28.4246 21.3514 28.4945 21.3324 28.5493 21.3638L29.2101 21.742C29.2649 21.7734 29.2838 21.8432 29.2525 21.898C29.2211 21.9528 29.1513 21.9717 29.0965 21.9404L28.4357 21.5621C28.3809 21.5308 28.3619 21.4609 28.3933 21.4062Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.7679 20.6159C29.7993 20.5611 29.8691 20.5421 29.9239 20.5735L30.5847 20.9518C30.6395 20.9831 30.6585 21.0529 30.6271 21.1077C30.5958 21.1625 30.5259 21.1815 30.4712 21.1501L29.8103 20.7718C29.7556 20.7405 29.7366 20.6707 29.7679 20.6159Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.1426 19.8256C31.1739 19.7708 31.2437 19.7518 31.2985 19.7832L31.9593 20.1614C32.0141 20.1928 32.0331 20.2626 32.0018 20.3174C31.9704 20.3722 31.9006 20.3912 31.8458 20.3598L31.185 19.9816C31.1302 19.9502 31.1112 19.8804 31.1426 19.8256Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5172 19.0353C32.5486 18.9805 32.6184 18.9615 32.6732 18.9929L33.334 19.3712C33.3888 19.4025 33.4077 19.4723 33.3764 19.5271C33.345 19.5819 33.2752 19.6009 33.2204 19.5695L32.5596 19.1913C32.5048 19.1599 32.4858 19.0901 32.5172 19.0353Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8918 18.2449C33.9232 18.1902 33.9931 18.1713 34.0478 18.2027L35.8924 19.2622C35.9471 19.2936 35.966 19.3634 35.9346 19.4182C35.9031 19.4729 35.8333 19.4918 35.7785 19.4604L33.934 18.4009C33.8792 18.3695 33.8604 18.2996 33.8918 18.2449Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.2663 17.4547C35.2977 17.4 35.3675 17.381 35.4223 17.4123L36.0831 17.7906C36.1379 17.822 36.1569 17.8918 36.1255 17.9466C36.0942 18.0013 36.0244 18.0203 35.9696 17.989L35.3088 17.6107C35.254 17.5794 35.235 17.5095 35.2663 17.4547Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.641 16.6645C36.6723 16.6097 36.7422 16.5907 36.7969 16.6221L37.4578 17.0003C37.5125 17.0317 37.5315 17.1015 37.5002 17.1563C37.4688 17.2111 37.399 17.23 37.3442 17.1987L36.6834 16.8204C36.6286 16.7891 36.6096 16.7192 36.641 16.6645Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.0156 15.8742C38.047 15.8194 38.1168 15.8004 38.1716 15.8318L38.8324 16.21C38.8872 16.2414 38.9062 16.3112 38.8748 16.366C38.8434 16.4208 38.7736 16.4398 38.7188 16.4084L38.058 16.0301C38.0032 15.9988 37.9843 15.929 38.0156 15.8742Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.3902 15.0839C39.4216 15.0291 39.4914 15.0101 39.5462 15.0415L40.207 15.4198C40.2618 15.4511 40.2808 15.521 40.2495 15.5757C40.2181 15.6305 40.1483 15.6495 40.0935 15.6181L39.4327 15.2399C39.3779 15.2085 39.3589 15.1387 39.3902 15.0839Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.7648 14.2936C40.7963 14.2389 40.8661 14.22 40.9209 14.2514L42.7655 15.3109C42.8202 15.3423 42.8391 15.4121 42.8076 15.4669C42.7762 15.5216 42.7063 15.5405 42.6516 15.5091L40.807 14.4496C40.7523 14.4182 40.7334 14.3483 40.7648 14.2936Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.1394 13.5035C42.1707 13.4487 42.2406 13.4297 42.2953 13.461L42.9562 13.8393C43.011 13.8707 43.03 13.9405 42.9986 13.9953C42.9672 14.05 42.8974 14.069 42.8426 14.0377L42.1818 13.6594C42.127 13.6281 42.108 13.5582 42.1394 13.5035Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.514 12.7132C43.5454 12.6584 43.6152 12.6394 43.67 12.6708L44.3308 13.049C44.3856 13.0804 44.4046 13.1502 44.3732 13.205C44.3419 13.2598 44.2721 13.2787 44.2173 13.2474L43.5564 12.8691C43.5017 12.8378 43.4827 12.768 43.514 12.7132Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.8887 11.9229C44.92 11.8681 44.9898 11.8491 45.0446 11.8805L45.7054 12.2587C45.7602 12.2901 45.7792 12.3599 45.7478 12.4147C45.7165 12.4695 45.6467 12.4885 45.5919 12.4571L44.9311 12.0788C44.8763 12.0475 44.8573 11.9777 44.8887 11.9229Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M46.2633 11.1326C46.2946 11.0778 46.3645 11.0588 46.4192 11.0902L47.0801 11.4685C47.1349 11.4998 47.1538 11.5696 47.1225 11.6244C47.0911 11.6792 47.0213 11.6982 46.9665 11.6668L46.3057 11.2886C46.2509 11.2572 46.2319 11.1874 46.2633 11.1326Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.6379 10.3422C47.6693 10.2874 47.7392 10.2686 47.7939 10.3L49.6385 11.3594C49.6932 11.3909 49.7121 11.4607 49.6807 11.5155C49.6492 11.5702 49.5794 11.5891 49.5246 11.5576L47.6801 10.4982C47.6253 10.4668 47.6064 10.3969 47.6379 10.3422Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.0124 9.55203C49.0438 9.49726 49.1136 9.47827 49.1684 9.50963L49.8292 9.88791C49.884 9.91927 49.903 9.9891 49.8716 10.0439C49.8403 10.0987 49.7704 10.1176 49.7157 10.0863L49.0548 9.708C49.0001 9.67664 48.9811 9.60681 49.0124 9.55203Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.3871 8.76175C50.4184 8.70697 50.4882 8.68799 50.543 8.71934L51.2039 9.09761C51.2586 9.12896 51.2776 9.19879 51.2463 9.25357C51.2149 9.30835 51.1451 9.32733 51.0903 9.29598L50.4295 8.91771C50.3747 8.88636 50.3557 8.81653 50.3871 8.76175Z" fill="#374874"/>
|
||||
<path d="M4.69935 36.0053C4.67016 35.9884 4.64459 35.9702 4.62271 35.951C4.55704 35.8935 4.52429 35.8272 4.52447 35.761L4.521 36.9856C4.52075 37.0739 4.57908 37.1624 4.69585 37.2298L36.291 55.4712L36.2945 54.2467L4.69935 36.0053Z" fill="#FBDBD0"/>
|
||||
<path d="M44.4492 49.5077C44.6794 49.6405 44.6826 49.8565 44.4519 49.9905L37.13 54.2455C36.8994 54.3795 36.5246 54.3796 36.2944 54.2466L4.69928 36.0052C4.46717 35.8712 4.46598 35.6541 4.69661 35.5201L12.0185 31.2651C12.2491 31.1311 12.6219 31.1322 12.854 31.2662L44.4492 49.5077Z" fill="white"/>
|
||||
<path d="M44.6133 49.8114C44.6126 49.8135 44.6122 49.8156 44.6114 49.8178C44.605 49.8365 44.5953 49.8547 44.5834 49.8727C44.5813 49.8758 44.5792 49.8791 44.5769 49.8822C44.564 49.9 44.5485 49.9173 44.5301 49.9338C44.5181 49.9446 44.5028 49.9545 44.4884 49.9647C44.4762 49.9733 44.4658 49.9825 44.452 49.9906L37.1301 54.2456C37.0766 54.2767 37.0141 54.2989 36.9487 54.3155C36.9393 54.3179 36.9299 54.3205 36.9203 54.3226C36.8545 54.337 36.7855 54.3454 36.7161 54.3457C36.7135 54.3458 36.7109 54.3458 36.7084 54.3458C36.6354 54.3456 36.5625 54.3376 36.4937 54.3219C36.4911 54.3212 36.4885 54.3206 36.4859 54.32C36.4164 54.3034 36.3509 54.2793 36.2945 54.2467L36.291 55.4712C36.3174 55.4865 36.3456 55.5 36.3754 55.5117C36.3983 55.5207 36.4221 55.5287 36.4466 55.5358C36.4583 55.5391 36.4704 55.5416 36.4823 55.5445C36.485 55.5452 36.4876 55.5458 36.4904 55.5465C36.4963 55.5478 36.502 55.5496 36.508 55.5507C36.5272 55.5548 36.5467 55.5582 36.5664 55.5609C36.5854 55.5637 36.6046 55.5658 36.6239 55.5673C36.6432 55.5689 36.6627 55.57 36.6821 55.5705C36.6897 55.5707 36.6972 55.5703 36.7048 55.5704C36.7075 55.5704 36.7101 55.5703 36.7128 55.5703C36.7228 55.5703 36.7328 55.5706 36.7428 55.5702C36.7648 55.5695 36.7868 55.568 36.8085 55.5659C36.8341 55.5633 36.8594 55.5598 36.8843 55.5553C36.8955 55.5533 36.9058 55.5496 36.9169 55.5472C36.9266 55.5451 36.9357 55.5426 36.9453 55.5401C36.9624 55.5357 36.9801 55.5327 36.9966 55.5273C37.0432 55.5122 37.087 55.4932 37.1266 55.4702L44.4485 51.2152C44.4509 51.2137 44.4534 51.2123 44.4557 51.2108C44.4669 51.2042 44.4748 51.1965 44.4848 51.1894C44.4992 51.1792 44.5146 51.1693 44.5267 51.1583C44.5278 51.1573 44.5295 51.1565 44.5306 51.1555C44.5444 51.1427 44.5566 51.1296 44.5672 51.1162C44.5696 51.1131 44.5712 51.1099 44.5734 51.1069C44.5758 51.1036 44.5778 51.1004 44.58 51.0971C44.5835 51.0918 44.5878 51.0865 44.5909 51.081C44.5971 51.07 44.6023 51.0588 44.6065 51.0476C44.6072 51.0459 44.6074 51.0441 44.608 51.0424C44.6088 51.0403 44.6092 51.0381 44.6098 51.036C44.6121 51.0288 44.6146 51.0217 44.616 51.0145C44.6183 51.0032 44.6195 50.992 44.6199 50.9807C44.6199 50.979 44.6199 50.9773 44.62 50.9756L44.6234 49.751C44.6234 49.7712 44.6195 49.7914 44.6133 49.8114Z" fill="#C1DBF6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.462 48.7549C43.4935 48.8096 43.4746 48.8795 43.4198 48.9109L41.5753 49.9704C41.5205 50.0018 41.4507 49.9829 41.4193 49.9282C41.3878 49.8735 41.4067 49.8036 41.4614 49.7722L43.306 48.7127C43.3607 48.6813 43.4306 48.7002 43.462 48.7549Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.0873 47.9643C42.1189 48.019 42.1002 48.0889 42.0455 48.1205L41.179 48.6215C41.1243 48.6531 41.0544 48.6345 41.0228 48.5798C40.9912 48.5252 41.0099 48.4553 41.0645 48.4237L41.9311 47.9226C41.9858 47.891 42.0557 47.9097 42.0873 47.9643Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.7126 47.1742C40.7442 47.2288 40.7255 47.2987 40.6709 47.3303L39.8043 47.8314C39.7497 47.863 39.6798 47.8443 39.6482 47.7896C39.6166 47.735 39.6353 47.6651 39.6899 47.6335L40.5565 47.1324C40.6111 47.1008 40.681 47.1195 40.7126 47.1742Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.3381 46.3838C39.3697 46.4384 39.351 46.5083 39.2964 46.5399L38.4298 47.041C38.3752 47.0726 38.3053 47.0539 38.2737 46.9992C38.2421 46.9446 38.2608 46.8747 38.3154 46.8431L39.182 46.342C39.2366 46.3104 39.3065 46.3291 39.3381 46.3838Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.9635 45.5935C37.9951 45.6481 37.9764 45.718 37.9218 45.7496L37.0552 46.2507C37.0005 46.2823 36.9306 46.2636 36.899 46.209C36.8674 46.1543 36.8861 46.0844 36.9408 46.0528L37.8073 45.5517C37.862 45.5201 37.9319 45.5388 37.9635 45.5935Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.589 44.8035C36.6204 44.8582 36.6015 44.9281 36.5468 44.9595L34.7022 46.019C34.6475 46.0504 34.5776 46.0315 34.5462 45.9768C34.5148 45.9221 34.5337 45.8522 34.5884 45.8208L36.433 44.7613C36.4877 44.7299 36.5575 44.7487 36.589 44.8035Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.2145 44.0135C35.2459 44.0682 35.2269 44.1381 35.1721 44.1694L34.5113 44.5477C34.4565 44.579 34.3867 44.5601 34.3553 44.5053C34.324 44.4505 34.343 44.3807 34.3977 44.3493L35.0586 43.9711C35.1133 43.9397 35.1832 43.9587 35.2145 44.0135Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8399 43.2232C33.8712 43.278 33.8523 43.3478 33.7975 43.3791L33.1367 43.7574C33.0819 43.7888 33.0121 43.7698 32.9807 43.715C32.9493 43.6602 32.9683 43.5904 33.0231 43.559L33.6839 43.1808C33.7387 43.1494 33.8085 43.1684 33.8399 43.2232Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.4653 42.4329C32.4966 42.4877 32.4776 42.5575 32.4228 42.5889L31.762 42.9671C31.7072 42.9985 31.6374 42.9795 31.6061 42.9247C31.5747 42.87 31.5937 42.8001 31.6485 42.7688L32.3093 42.3905C32.3641 42.3591 32.4339 42.3781 32.4653 42.4329Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.0906 41.6426C31.122 41.6974 31.103 41.7672 31.0482 41.7986L30.3874 42.1768C30.3326 42.2082 30.2628 42.1892 30.2314 42.1344C30.2001 42.0796 30.2191 42.0098 30.2738 41.9785L30.9347 41.6002C30.9894 41.5688 31.0593 41.5878 31.0906 41.6426Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.7159 40.8522C29.7474 40.9069 29.7285 40.9768 29.6738 41.0082L27.8292 42.0677C27.7745 42.0991 27.7046 42.0802 27.6732 42.0255C27.6417 41.9707 27.6606 41.9009 27.7153 41.8695L29.5599 40.81C29.6146 40.7786 29.6845 40.7975 29.7159 40.8522Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.3415 40.062C28.3728 40.1168 28.3538 40.1867 28.2991 40.218L27.6382 40.5963C27.5835 40.6276 27.5136 40.6086 27.4823 40.5539C27.4509 40.4991 27.4699 40.4293 27.5247 40.3979L28.1855 40.0196C28.2403 39.9883 28.3101 40.0073 28.3415 40.062Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.9668 39.2718C26.9982 39.3265 26.9792 39.3964 26.9244 39.4277L26.2636 39.806C26.2088 39.8374 26.139 39.8184 26.1077 39.7636C26.0763 39.7088 26.0953 39.639 26.1501 39.6076L26.8109 39.2294C26.8657 39.198 26.9355 39.217 26.9668 39.2718Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.5922 38.4815C25.6236 38.5363 25.6046 38.6061 25.5498 38.6374L24.889 39.0157C24.8342 39.0471 24.7644 39.0281 24.733 38.9733C24.7017 38.9185 24.7206 38.8487 24.7754 38.8173L25.4363 38.4391C25.4911 38.4077 25.5609 38.4267 25.5922 38.4815Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.2176 37.6912C24.249 37.746 24.23 37.8158 24.1752 37.8472L23.5143 38.2254C23.4596 38.2568 23.3897 38.2378 23.3584 38.183C23.327 38.1282 23.346 38.0584 23.4008 38.027L24.0616 37.6488C24.1164 37.6174 24.1862 37.6364 24.2176 37.6912Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8429 36.9008C22.8744 36.9555 22.8555 37.0254 22.8007 37.0568L20.9561 38.1162C20.9014 38.1477 20.8316 38.1288 20.8001 38.0741C20.7687 38.0193 20.7876 37.9495 20.8423 37.918L22.6869 36.8586C22.7416 36.8272 22.8115 36.846 22.8429 36.9008Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.4685 36.1106C21.4998 36.1654 21.4808 36.2352 21.426 36.2666L20.7652 36.6449C20.7104 36.6762 20.6406 36.6572 20.6092 36.6024C20.5779 36.5477 20.5969 36.4778 20.6516 36.4465L21.3125 36.0682C21.3673 36.0369 21.4371 36.0559 21.4685 36.1106Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0938 35.3203C20.1252 35.3751 20.1062 35.445 20.0514 35.4763L19.3906 35.8546C19.3358 35.8859 19.266 35.8669 19.2346 35.8122C19.2032 35.7574 19.2222 35.6876 19.277 35.6562L19.9378 35.2779C19.9926 35.2466 20.0624 35.2656 20.0938 35.3203Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7192 34.5301C18.7505 34.5848 18.7315 34.6547 18.6768 34.686L18.0159 35.0643C17.9612 35.0956 17.8913 35.0767 17.86 35.0219C17.8286 34.9671 17.8476 34.8973 17.9024 34.8659L18.5632 34.4877C18.618 34.4563 18.6878 34.4753 18.7192 34.5301Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.3447 33.7398C17.376 33.7946 17.357 33.8644 17.3022 33.8957L16.6414 34.274C16.5866 34.3054 16.5168 34.2864 16.4855 34.2316C16.4541 34.1768 16.4731 34.107 16.5279 34.0756L17.1887 33.6974C17.2435 33.666 17.3133 33.685 17.3447 33.7398Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.97 32.9494C16.0014 33.0041 15.9825 33.0739 15.9278 33.1054L14.0832 34.1648C14.0285 34.1963 13.9586 34.1774 13.9272 34.1226C13.8958 34.0679 13.9146 33.9981 13.9694 33.9666L15.8139 32.9072C15.8687 32.8757 15.9385 32.8946 15.97 32.9494Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5954 32.1592C14.6267 32.214 14.6078 32.2838 14.553 32.3152L13.8921 32.6934C13.8374 32.7248 13.7675 32.7058 13.7362 32.651C13.7048 32.5962 13.7238 32.5264 13.7786 32.4951L14.4394 32.1168C14.4942 32.0854 14.564 32.1044 14.5954 32.1592Z" fill="#374874"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2207 31.3689C13.2521 31.4237 13.2331 31.4935 13.1783 31.5249L12.5175 31.9032C12.4627 31.9345 12.3929 31.9155 12.3616 31.8608C12.3302 31.806 12.3492 31.7362 12.404 31.7048L13.0648 31.3265C13.1196 31.2952 13.1894 31.3142 13.2207 31.3689Z" fill="#374874"/>
|
||||
<path d="M12.4338 31.1652C12.5855 31.1652 12.7375 31.199 12.8541 31.2663L44.4492 49.5077C44.5813 49.584 44.636 49.6875 44.6173 49.7873C44.6196 49.7752 44.6233 49.7631 44.6234 49.7509L44.6199 50.9755C44.6199 50.9772 44.6199 50.9789 44.6198 50.9807C44.6195 50.992 44.6182 51.0032 44.616 51.0144C44.6146 51.0216 44.612 51.0288 44.6098 51.036C44.6091 51.0381 44.6087 51.0402 44.608 51.0423C44.6074 51.0441 44.6071 51.0458 44.6065 51.0475C44.6022 51.0587 44.597 51.0699 44.5908 51.0809C44.5877 51.0864 44.5835 51.0917 44.58 51.0971C44.5778 51.1004 44.5757 51.1036 44.5734 51.1069C44.5712 51.1099 44.5696 51.1131 44.5672 51.1161C44.5566 51.1296 44.5444 51.1427 44.5306 51.1554C44.5295 51.1565 44.5278 51.1573 44.5267 51.1583C44.5146 51.1693 44.4992 51.1791 44.4848 51.1893C44.4747 51.1965 44.4669 51.2042 44.4557 51.2108C44.4533 51.2123 44.4509 51.2137 44.4485 51.2152L37.1266 55.4701C37.087 55.4932 37.0432 55.5121 36.9966 55.5273C36.9801 55.5326 36.9623 55.5357 36.9452 55.5401C36.9356 55.5425 36.9266 55.545 36.9168 55.5472C36.9058 55.5496 36.8955 55.5532 36.8843 55.5553C36.8594 55.5597 36.8341 55.5633 36.8085 55.5658C36.7867 55.568 36.7648 55.5695 36.7428 55.5702C36.7388 55.5703 36.7349 55.5704 36.7309 55.5704C36.7265 55.5704 36.722 55.5704 36.7176 55.5704H36.7128C36.71 55.5704 36.7075 55.5704 36.7048 55.5704H36.7031C36.6981 55.5704 36.693 55.5705 36.6879 55.5705C36.686 55.5705 36.684 55.5705 36.6821 55.5705C36.6626 55.57 36.6432 55.569 36.6238 55.5673C36.6045 55.5658 36.5853 55.5637 36.5664 55.5609C36.5467 55.5581 36.5271 55.5548 36.5079 55.5508C36.5019 55.5496 36.4963 55.5478 36.4903 55.5465C36.4876 55.5459 36.485 55.5452 36.4822 55.5445C36.4703 55.5417 36.4582 55.5391 36.4466 55.5358C36.4221 55.5287 36.3983 55.5208 36.3753 55.5117C36.3456 55.5 36.3173 55.4865 36.291 55.4712L4.69586 37.2298C4.57906 37.1624 4.52073 37.0739 4.52098 36.9856L4.52443 35.761C4.52443 35.7613 4.52457 35.7616 4.52457 35.7618C4.52445 35.6744 4.58169 35.587 4.69661 35.5201L12.0185 31.2651C12.1334 31.1984 12.2834 31.1652 12.4338 31.1652ZM12.4338 30.2509C12.1134 30.2509 11.8109 30.3283 11.5589 30.4748L4.23725 34.7296C3.84541 34.9575 3.61143 35.3417 3.61028 35.7584L3.60669 36.983C3.60551 37.4042 3.8418 37.7924 4.23876 38.0216L35.8338 56.263C35.8986 56.3004 35.9679 56.3338 36.0396 56.3621C36.0893 56.3816 36.14 56.3989 36.1923 56.414C36.2203 56.4219 36.2432 56.4276 36.2663 56.4329L36.2771 56.4356L36.2931 56.4398L36.3205 56.4455C36.3593 56.4537 36.3986 56.4605 36.4384 56.4662C36.4735 56.4712 36.5116 56.4755 36.5498 56.4786C36.5845 56.4815 36.6216 56.4835 36.6586 56.4844L36.6704 56.4847L36.6879 56.4847L36.705 56.4846L36.7174 56.4846H36.7203L36.7308 56.4846C36.7469 56.4846 36.7629 56.4843 36.7789 56.4837C36.8151 56.4825 36.8573 56.4797 36.8993 56.4755C36.9492 56.4705 36.9978 56.4637 37.0457 56.4552C37.0781 56.4492 37.1065 56.4426 37.1343 56.4352C37.1442 56.4328 37.1548 56.4301 37.1654 56.4274L37.1622 56.4282C37.2042 56.4187 37.2425 56.4086 37.2792 56.3967C37.3884 56.3613 37.4921 56.3152 37.5868 56.2601L44.9078 52.0056L44.9203 51.9983L44.9236 51.9963L44.9279 51.9938L44.9346 51.9896C44.9727 51.9662 45.0036 51.9438 45.0268 51.9258C45.0366 51.919 45.049 51.9102 45.0634 51.8994L45.0846 51.8854L45.1348 51.8416L45.1436 51.8338L45.1523 51.8257C45.2009 51.7806 45.2458 51.732 45.2858 51.6812L45.3139 51.6455L45.3337 51.6146C45.3346 51.6133 45.3355 51.6119 45.3365 51.6105C45.3558 51.5825 45.3738 51.554 45.3896 51.5256C45.4153 51.4803 45.4408 51.4261 45.4617 51.3707L45.4808 51.3202L45.4894 51.2859C45.4989 51.2539 45.5063 51.2243 45.5122 51.1949C45.5249 51.1311 45.5319 51.0692 45.5337 51.0078L45.5339 50.9995L45.5341 50.9869L45.5376 49.7534H45.5351C45.5364 49.3381 45.3037 48.9453 44.9064 48.7158L13.3112 30.4745C13.0579 30.3282 12.7545 30.2509 12.4338 30.2509Z" fill="#374874"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
1
fusion_iot/iot/static/src/img/scales.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
7
fusion_iot/iot/static/src/img/unsupported.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M58.6886 16.595L32.8244 1.46649L5.31143 17.349L31.2461 32.4775L58.6886 16.595Z" fill="#FBDBD0"/>
|
||||
<path d="M31.2461 62.5335L5.31143 47.405V17.3489L31.2461 32.4775V62.5335Z" fill="#C1DBF6"/>
|
||||
<path d="M31.2461 62.5335L58.6886 46.6511V16.595L31.2461 32.4775V62.5335Z" fill="white"/>
|
||||
<path d="M32.7539 1.46647L58.6886 16.595V46.6511L31.2461 62.5335L5.31144 47.405V17.3489L32.7539 1.46647ZM32.7557 0.409058L4.39716 16.8217V47.9301L31.2443 63.5909L59.6029 47.1783V16.0699L32.7557 0.409058Z" fill="#374874"/>
|
||||
<path d="M44.0935 43.3884V42.3057C44.0935 41.7879 44.1648 41.2799 44.3075 40.7817C44.4502 40.2757 44.6744 39.7579 44.9802 39.2283C45.2859 38.6987 45.6698 38.1397 46.1318 37.5513C46.5259 37.0413 46.8418 36.5843 47.0796 36.1803C47.3242 35.7645 47.5009 35.3644 47.6096 34.9799C47.7183 34.5955 47.7727 34.1954 47.7727 33.7796V33.756C47.7727 33.2696 47.6708 32.8969 47.4669 32.638C47.2631 32.3791 46.9743 32.2477 46.6006 32.2438C46.2269 32.2399 45.7853 32.385 45.2757 32.6792C44.7661 32.9734 44.3211 33.3402 43.9406 33.7796C43.5601 34.2189 43.2578 34.7073 43.0336 35.2447C42.8161 35.7782 42.6904 36.3333 42.6565 36.9099L42.6361 36.9335L40.9035 37.9455V37.922C40.9443 37.059 41.1345 36.1842 41.4742 35.2977C41.814 34.4111 42.3066 33.5775 42.952 32.7969C43.5975 32.0163 44.3958 31.3514 45.3471 30.8022C46.2032 30.3079 46.9539 30.0627 47.5994 30.0667C48.2517 30.0588 48.7579 30.2726 49.118 30.708C49.4849 31.1317 49.6683 31.7476 49.6683 32.5557V32.5792C49.6683 33.4971 49.4747 34.4013 49.0874 35.2918C48.7069 36.1705 48.1905 37.0257 47.5383 37.8573C47.1102 38.3947 46.7739 38.8674 46.5293 39.2754C46.2915 39.6794 46.1216 40.0638 46.0197 40.4287C45.9246 40.7896 45.877 41.1779 45.877 41.5937V42.3587L44.0935 43.3884ZM45.1228 48.2546C44.7084 48.4939 44.3687 48.537 44.1037 48.384C43.8387 48.231 43.7062 47.9153 43.7062 47.4367C43.7062 46.9503 43.8387 46.4776 44.1037 46.0186C44.3687 45.5596 44.7084 45.2105 45.1228 44.9712C45.5373 44.7319 45.8736 44.6907 46.1318 44.8477C46.3968 45.0006 46.5293 45.3203 46.5293 45.8068C46.5293 46.2853 46.3968 46.7541 46.1318 47.2131C45.8736 47.6681 45.5373 48.0153 45.1228 48.2546Z" fill="#374874"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
52
fusion_iot/iot/static/src/iot_device_hook.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { DeviceController } from '@iot_base/device_controller';
|
||||
import { useEffect } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* Use this hook to be able to interact with an iot device.
|
||||
* @param {{
|
||||
* getIotIp: () => string | undefined,
|
||||
* getIdentifier: () => string | undefined,
|
||||
* onValueChange: (data: any) => void,
|
||||
* onStartListening: (() => void) | undefined,
|
||||
* onStopListening: (() => void) | undefined,
|
||||
* }} param0
|
||||
*/
|
||||
export const useIotDevice = ({ getIotIp, getIdentifier, getLongpollingHasFallback, onValueChange, onStartListening, onStopListening }) => {
|
||||
// set default values for the device
|
||||
getIotIp = getIotIp || (() => {});
|
||||
getIdentifier = getIdentifier || (() => {});
|
||||
getLongpollingHasFallback = getLongpollingHasFallback || (() => false);
|
||||
onValueChange = onValueChange || (() => {});
|
||||
onStartListening = onStartListening || (() => {});
|
||||
onStopListening = onStopListening || (() => {});
|
||||
|
||||
const iotLongpolling = useService('iot_longpolling');
|
||||
let iotDevice = null;
|
||||
|
||||
const startListening = () => {
|
||||
iotDevice.addListener((data) => onValueChange(data), getLongpollingHasFallback());
|
||||
onStartListening();
|
||||
};
|
||||
|
||||
const stopListening = () => {
|
||||
onStopListening();
|
||||
iotDevice.removeListener();
|
||||
};
|
||||
|
||||
useEffect(
|
||||
(iotIp, identifier) => {
|
||||
if (iotIp && identifier) {
|
||||
iotDevice = new DeviceController(iotLongpolling, { iot_ip: iotIp, identifier });
|
||||
startListening();
|
||||
return () => {
|
||||
stopListening();
|
||||
iotDevice = null;
|
||||
};
|
||||
}
|
||||
},
|
||||
() => [getIotIp(), getIdentifier()]
|
||||
);
|
||||
|
||||
return () => iotDevice;
|
||||
};
|
||||
105
fusion_iot/iot/static/src/iot_report_action.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { browser } from "@web/core/browser/browser"
|
||||
import {
|
||||
IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY,
|
||||
setReportIdInBrowserLocalStorage,
|
||||
} from "./client_action/delete_local_storage";
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
|
||||
/**
|
||||
* Method to print the report with the selected devices
|
||||
*
|
||||
* @param env Environment
|
||||
* @param args Arguments to render the report (report_id, active_record_ids, report_data)
|
||||
* @param selected_device_ids Selected device ids (those stored in local storage)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function printReport(env, args, selected_device_ids) {
|
||||
const orm = env.services.orm;
|
||||
const notification = env.services.notification;
|
||||
const iotHttp = env.services.iot_http;
|
||||
|
||||
const [report_id, active_record_ids, report_data] = args;
|
||||
const jobs = await orm.call(
|
||||
"ir.actions.report", "render_document",
|
||||
[report_id, selected_device_ids, active_record_ids, report_data]
|
||||
);
|
||||
|
||||
for (const job of jobs) {
|
||||
const { iotBoxId, deviceIdentifier, deviceName, document } = job;
|
||||
const removeSendingNotification = notification.add(_t("Sending document to printer %s...", deviceName), {
|
||||
type: "info",
|
||||
sticky: true,
|
||||
});
|
||||
|
||||
await iotHttp.action(iotBoxId, deviceIdentifier, { document, print_id: uuid() }, () => {
|
||||
removeSendingNotification?.();
|
||||
notification.add(_t("Started printing operation on printer %s...", deviceName), { type: "success" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSelectedPrintersForReport(reportId, env) {
|
||||
const { orm, action, ui } = env.services;
|
||||
const deviceSettingsByReportId = JSON.parse(browser.localStorage.getItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY));
|
||||
const deviceSettings = deviceSettingsByReportId?.[reportId];
|
||||
|
||||
if (deviceSettings && deviceSettings.skipDialog) {
|
||||
return deviceSettings.selectedDevices;
|
||||
}
|
||||
|
||||
// Open IoT devices selection wizard
|
||||
const openDeviceSelectionWizard = await orm.call("ir.actions.report", "get_action_wizard", [reportId, deviceSettings?.selectedDevices]);
|
||||
await action.doAction(openDeviceSelectionWizard);
|
||||
|
||||
// If the UI is currently blocked, we need to temporarily unblock it or the user won't be able to select the printer
|
||||
const uiWasBlocked = ui.isBlocked;
|
||||
if (uiWasBlocked) {
|
||||
ui.unblock();
|
||||
}
|
||||
|
||||
// Wait for the popup to be closed and a printer selected
|
||||
return new Promise((resolve) => {
|
||||
const onPrinterSelected = (event) => {
|
||||
if (event.detail.reportId === reportId) {
|
||||
const newDeviceSettings = event.detail.deviceSettings;
|
||||
if (newDeviceSettings) {
|
||||
setReportIdInBrowserLocalStorage(reportId, newDeviceSettings);
|
||||
}
|
||||
resolve(newDeviceSettings ? newDeviceSettings.selectedDevices : null);
|
||||
env.bus.removeEventListener("printer-selected", onPrinterSelected);
|
||||
if (uiWasBlocked) {
|
||||
ui.block();
|
||||
}
|
||||
}
|
||||
};
|
||||
env.bus.addEventListener("printer-selected", onPrinterSelected);
|
||||
});
|
||||
}
|
||||
|
||||
async function iotReportActionHandler(action, options, env) {
|
||||
if (action.device_ids && action.device_ids.length) {
|
||||
action.data ??= {};
|
||||
const args = [action.id, action.context.active_ids, action.data];
|
||||
const reportId = action.id;
|
||||
const printerIds = await getSelectedPrintersForReport(reportId, env);
|
||||
|
||||
if (!printerIds) {
|
||||
// If the user does not select any printer, fall back to normal printing
|
||||
return false;
|
||||
}
|
||||
|
||||
env.services.ui.block();
|
||||
// Try longpolling then websocket
|
||||
await printReport(env, args, printerIds);
|
||||
env.services.ui.unblock();
|
||||
|
||||
options.onClose?.();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("ir.actions.report handlers")
|
||||
.add("iot_report_action_handler", iotReportActionHandler);
|
||||
BIN
fusion_iot/iot/static/src/network_utils/._iot_http_service.js
Normal file
BIN
fusion_iot/iot/static/src/network_utils/._iot_webrtc.js
Normal file
BIN
fusion_iot/iot/static/src/network_utils/._iot_websocket.js
Normal file
297
fusion_iot/iot/static/src/network_utils/iot_http_service.js
Normal file
@@ -0,0 +1,297 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { post } from "@iot_base/network_utils/http";
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
import { IotWebsocket } from "@iot/network_utils/iot_websocket";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { IotWebRtc } from "./iot_webrtc";
|
||||
|
||||
export const PRINTER_MESSAGES = {
|
||||
ERROR_FAILED: _t("Failed to initiate print"),
|
||||
ERROR_OFFLINE: _t("Printer is not ready"),
|
||||
ERROR_TIMEOUT: _t("Printing timed out"),
|
||||
ERROR_NO_PAPER: _t("Out of paper"),
|
||||
ERROR_UNREACHABLE: _t("Printer is unreachable"),
|
||||
ERROR_UNKNOWN: _t("Unknown printer error occurred"),
|
||||
WARNING_LOW_PAPER: _t("Paper is low"),
|
||||
};
|
||||
|
||||
export const FDM_MESSAGES = {
|
||||
'000': _t("Blackbox is running and operational"),
|
||||
'001': _t("PIN accepted."),
|
||||
101: _t("Fiscal Data Module memory 90% full."),
|
||||
102: _t("Repeated request. This request was already handled by the fiscal data module."),
|
||||
103: _t("Operation wasn't saved on the blackbox"),
|
||||
199: _t("Unspecified warning."),
|
||||
201: _t("No Vat Signing Card or Vat Signing Card broken."),
|
||||
202: _t("Please activate the Vat Signing Card with PIN."),
|
||||
203: _t("Vat Signing Card blocked."),
|
||||
204: _t("Invalid PIN."),
|
||||
205: _t("Fiscal Data Module memory full."),
|
||||
206: _t("Unknown identifier."),
|
||||
207: _t("Invalid data in message sent to the blackbox."),
|
||||
208: _t("Fiscal Data Module not operational. Please restart the blackbox"),
|
||||
209: _t("Fiscal Data Module real time clock corrupt."),
|
||||
210: _t("Vat Signing Card not compatible with Fiscal Data Module."),
|
||||
299: _t("Unspecified error."),
|
||||
300: _t("Blackbox responded with invalid response. Please check the cable connection and the power supply, then retry. Restart if necessary"),
|
||||
301: _t("Blackbox did not respond to your request. This usually means it has disconnected. Please check its cable connection and its power supply. Restart if necessary."),
|
||||
426: _t("Blackbox driver update required. Please restart your IoT Box to update the blackbox driver."),
|
||||
};
|
||||
|
||||
/**
|
||||
* Class to handle IoT actions
|
||||
* The class is used to send actions to IoT devices and handle fallbacks
|
||||
* in case the request fails: it will try to send the request using
|
||||
* HTTP POST method and then using the websocket.
|
||||
*/
|
||||
export class IotHttpService {
|
||||
longpollingFailedTimestamp = null;
|
||||
webRtcFailedTimestamp = null;
|
||||
connectionStatus = "webrtc"; // webrtc, longpolling, websocket, offline
|
||||
connectionTypes = [
|
||||
this._webRtc.bind(this),
|
||||
this._longpolling.bind(this),
|
||||
this._websocket.bind(this)
|
||||
];
|
||||
cachedIotBoxes = {};
|
||||
|
||||
constructor() {
|
||||
this.setup(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("services").ServiceFactories & { websocket: IotWebsocket } & { webRtc: IotWebRtc } }} services
|
||||
*/
|
||||
setup({ iot_longpolling, websocket, webRtc, notification, orm }) {
|
||||
this.longpolling = iot_longpolling;
|
||||
this.websocket = websocket;
|
||||
this.webRtc = webRtc;
|
||||
this.notification = notification;
|
||||
this.orm = orm;
|
||||
}
|
||||
|
||||
onFailure(_message, deviceIdentifier, _messageId) {
|
||||
this.notification.add(_t("Failed to reach the IoT Box for device: %s", deviceIdentifier), { type: "danger" });
|
||||
}
|
||||
|
||||
cacheIotBoxRecords(boxes) {
|
||||
for (const box of boxes) {
|
||||
this.cachedIotBoxes[box.id] = { ip: box.ip, identifier: box.identifier, version: box.version };
|
||||
}
|
||||
}
|
||||
|
||||
async getIotBoxData(iotBoxId) {
|
||||
const record = await this.orm.searchRead("iot.box", [["id", "=", iotBoxId]], ["id", "ip", "identifier", "version"]);
|
||||
if (!record) {
|
||||
throw new Error(`No IoT Box found`);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
_ensureLongpollingEnabled() {
|
||||
if (
|
||||
this.longpollingFailedTimestamp &&
|
||||
Date.now() - this.longpollingFailedTimestamp < 5 * 60 * 1000
|
||||
) {
|
||||
throw new Error("Longpolling is temporarily disabled due to a recent failure.");
|
||||
}
|
||||
}
|
||||
|
||||
_ensureWebRtcEnabled() {
|
||||
if (
|
||||
this.webRtcFailedTimestamp &&
|
||||
Date.now() - this.webRtcFailedTimestamp < 20 * 60 * 1000
|
||||
) {
|
||||
throw new Error("WebRTC is temporarily disabled due to a recent failure.");
|
||||
}
|
||||
}
|
||||
|
||||
async _webRtc({ identifier, version, deviceIdentifier, data, messageId, onSuccess, onFailure, messageType }) {
|
||||
if (/\d{4}\.\d{2}\.\d{2}/.test(version)) {
|
||||
throw new Error("IoT box does not support WebRTC, skipping.");
|
||||
}
|
||||
this._ensureWebRtcEnabled();
|
||||
try {
|
||||
await this.webRtc.onMessage(identifier, deviceIdentifier, messageId, onSuccess, onFailure);
|
||||
if (data) {
|
||||
await this.webRtc.sendMessage(identifier, {
|
||||
device_identifier: deviceIdentifier,
|
||||
data,
|
||||
}, messageId, messageType);
|
||||
}
|
||||
} catch (error) {
|
||||
this.webRtcFailedTimestamp = Date.now();
|
||||
throw error;
|
||||
}
|
||||
this.connectionStatus = "webrtc";
|
||||
}
|
||||
|
||||
async _longpolling({ ip, deviceIdentifier, data, messageId, onSuccess, onFailure }) {
|
||||
this._ensureLongpollingEnabled();
|
||||
try {
|
||||
this.longpolling.onMessage(ip, deviceIdentifier, onSuccess, onFailure, messageId);
|
||||
if (data) {
|
||||
const response =
|
||||
await this.longpolling.sendMessage(ip, { device_identifier: deviceIdentifier, data }, messageId, true);
|
||||
if (response?.result === false) {
|
||||
onFailure({status: "disconnected"}, deviceIdentifier, messageId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.longpollingFailedTimestamp = Date.now();
|
||||
throw e;
|
||||
}
|
||||
this.connectionStatus = "longpolling";
|
||||
}
|
||||
|
||||
async _websocket({ identifier, deviceIdentifier, data, messageId, onSuccess, onFailure, messageType }) {
|
||||
const onFailureWithTimeout = (...args) => {
|
||||
onFailure(...args);
|
||||
this.connectionStatus = "offline";
|
||||
};
|
||||
this.websocket.onMessage(identifier, deviceIdentifier, onSuccess, onFailureWithTimeout, "operation_confirmation", messageId);
|
||||
if (data) {
|
||||
this.websocket.sendMessage(
|
||||
identifier,
|
||||
{
|
||||
device_identifiers: [deviceIdentifier],
|
||||
device_identifier: deviceIdentifier, // compatibility with v19.1+ IoT Boxes
|
||||
...data
|
||||
},
|
||||
messageId,
|
||||
messageType,
|
||||
);
|
||||
}
|
||||
this.connectionStatus = "websocket";
|
||||
}
|
||||
|
||||
async _attemptFallbacks({ iotBoxId, deviceIdentifier, data, onFailure }) {
|
||||
if (!["number", "string"].includes(typeof iotBoxId)) {
|
||||
iotBoxId = iotBoxId[0]; // iotBoxId is the ``Many2one`` field, we need the actual ID
|
||||
}
|
||||
|
||||
if (!this.cachedIotBoxes[iotBoxId]) {
|
||||
this.cacheIotBoxRecords(await this.getIotBoxData(iotBoxId))
|
||||
}
|
||||
const { ip, identifier, version } = this.cachedIotBoxes[iotBoxId];
|
||||
|
||||
// if we target the box instead of a device, we want longpolling to handle action as messageType
|
||||
const messageType = deviceIdentifier === identifier ? data.action : undefined;
|
||||
const params = { ip, identifier, version, data, messageType, ...arguments[0] };
|
||||
|
||||
for (const connectionType of this.connectionTypes) {
|
||||
try {
|
||||
return await connectionType(params);
|
||||
} catch (e) {
|
||||
console.debug("IoT Box action: attempted method failed, attempting another protocol.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// If all the connection types failed, run the onFailure callback and remove the cached IoT Box data
|
||||
delete this.cachedIotBoxes[iotBoxId];
|
||||
this.connectionStatus = "offline";
|
||||
onFailure({ status: "disconnected" }, deviceIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for events on the IoT Box
|
||||
* @param iotBoxId IoT Box record ID
|
||||
* @param deviceIdentifier Identifier of the device connected to the IoT Box
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onSuccess Callback to run when a message is received
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onFailure Callback to run when the request fails
|
||||
* @param {string|null} messageId Unique identifier for the message (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async onMessage(
|
||||
iotBoxId,
|
||||
deviceIdentifier,
|
||||
onSuccess = () => {},
|
||||
onFailure = (...args) => this.onFailure(...args),
|
||||
messageId = null,
|
||||
) {
|
||||
// Attempt to listen for messages using the defined connection types
|
||||
await this._attemptFallbacks({
|
||||
iotBoxId,
|
||||
deviceIdentifier,
|
||||
messageId,
|
||||
onSuccess,
|
||||
onFailure,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call for an action method on the IoT Box
|
||||
* @param iotBoxId IoT Box record ID
|
||||
* @param deviceIdentifier Identifier of the device connected to the IoT Box
|
||||
* @param data Data to send
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onSuccess Callback to run when a message is received
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onFailure Callback to run when the request fails
|
||||
* @param {string|null} messageId Unique identifier for the message (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async action(
|
||||
iotBoxId,
|
||||
deviceIdentifier,
|
||||
data,
|
||||
onSuccess = () => {},
|
||||
onFailure = (...args) => this.onFailure(...args),
|
||||
messageId = null,
|
||||
) {
|
||||
messageId ??= uuid();
|
||||
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
data.action_unique_id = messageId;
|
||||
|
||||
await this._attemptFallbacks({
|
||||
iotBoxId,
|
||||
deviceIdentifier,
|
||||
data,
|
||||
messageId,
|
||||
onSuccess,
|
||||
onFailure,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const iotHttpService = {
|
||||
dependencies: ["notification", "orm", "bus_service", "iot_longpolling", "lazy_session"],
|
||||
|
||||
start(env, services) {
|
||||
const { iot_longpolling, bus_service } = services;
|
||||
const iotWebsocket = new IotWebsocket(services);
|
||||
const iotWebRtc = new IotWebRtc(bus_service, iotWebsocket);
|
||||
|
||||
const webRtc = {
|
||||
sendMessage: iotWebRtc.sendMessage.bind(iotWebRtc),
|
||||
onMessage: iotWebRtc.onMessage.bind(iotWebRtc),
|
||||
};
|
||||
|
||||
const longpolling = {
|
||||
sendMessage: iot_longpolling.sendMessage.bind(iot_longpolling),
|
||||
onMessage: iot_longpolling.onMessage.bind(iot_longpolling),
|
||||
};
|
||||
|
||||
const websocket = {
|
||||
sendMessage: iotWebsocket.sendMessage.bind(iotWebsocket),
|
||||
onMessage: iotWebsocket.onMessage.bind(iotWebsocket),
|
||||
};
|
||||
|
||||
const iot = new IotHttpService({ ...services, websocket: iotWebsocket, webRtc: iotWebRtc });
|
||||
const cacheIotBoxRecords = iot.cacheIotBoxRecords.bind(iot);
|
||||
const action = iot.action.bind(iot);
|
||||
const onMessage = iot.onMessage.bind(iot);
|
||||
|
||||
// Expose only those functions to the environment
|
||||
// status is a getter to have a reactive value
|
||||
return {
|
||||
post, action, webRtc, longpolling, websocket, onMessage, cacheIotBoxRecords, get status() {
|
||||
return iot.connectionStatus;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("iot_http", iotHttpService);
|
||||
202
fusion_iot/iot/static/src/network_utils/iot_webrtc.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import { range } from "@web/core/utils/numbers";
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
|
||||
const CONNECT_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* @typedef {{ id: string, connection: RTCPeerConnection, channel: RTCDataChannel }} RtcConnection
|
||||
*/
|
||||
export class IotWebRtc {
|
||||
constructor() {
|
||||
this.setup(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("@web/model/model").Services["bus_service"]} busService
|
||||
* @param {import("@iot/network_utils/iot_websocket").IotWebsocket} iotWebsocket
|
||||
*/
|
||||
async setup(busService, iotWebsocket) {
|
||||
/**
|
||||
* @type {Record<string, RtcConnection>}
|
||||
*/
|
||||
this.connections = {};
|
||||
this.busService = busService;
|
||||
this.websocket = iotWebsocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the IoT Box
|
||||
* @param {string} iotIdentifier Identifier (serial no.) of the IoT Box
|
||||
* @param {Record<string, unknown>} message Data to send to the device
|
||||
* @param {string?} actionId Unique identifier for the message (optional)
|
||||
* @param {string} messageType Type of message to send (optional)
|
||||
* @returns {Promise<string>} The action ID
|
||||
*/
|
||||
async sendMessage(iotIdentifier, message, actionId = null, messageType = "iot_action") {
|
||||
const rtcConnection = await this.waitForConnection(iotIdentifier);
|
||||
|
||||
if (rtcConnection.connection.connectionState !== "connected") {
|
||||
throw new Error(
|
||||
`WebRTC connection for ${iotIdentifier} is '${rtcConnection.connection.connectionState}'`
|
||||
);
|
||||
}
|
||||
if (rtcConnection.channel.readyState !== "open") {
|
||||
throw new Error(
|
||||
`WebRTC channel for ${iotIdentifier} is '${rtcConnection.channel.readyState}'`
|
||||
);
|
||||
}
|
||||
|
||||
actionId ??= uuid();
|
||||
const messageString = JSON.stringify({
|
||||
...message,
|
||||
session_id: actionId,
|
||||
message_type: messageType,
|
||||
});
|
||||
|
||||
if (messageString.length >= rtcConnection.connection.sctp.maxMessageSize) {
|
||||
this._sendChunkedMessage(rtcConnection, messageString);
|
||||
} else {
|
||||
rtcConnection.channel.send(messageString);
|
||||
}
|
||||
|
||||
return actionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RtcConnection} rtcConnection
|
||||
* @param {string} message
|
||||
*/
|
||||
async _sendChunkedMessage(rtcConnection, message) {
|
||||
const chunkSize = rtcConnection.connection.sctp.maxMessageSize;
|
||||
const numberOfChunks = Math.ceil(message.length / chunkSize);
|
||||
rtcConnection.channel.send("chunked_start");
|
||||
for (const chunk of range(0, numberOfChunks)) {
|
||||
rtcConnection.channel.send(message.slice(chunk * chunkSize, (chunk + 1) * chunkSize));
|
||||
}
|
||||
rtcConnection.channel.send("chunked_end");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for events/messages coming from the IoT Box.
|
||||
* This method allows defining callbacks for success and failure cases.
|
||||
* @param {string} iotIdentifier Identifier (serial no.) of the IoT Box
|
||||
* @param {string} deviceIdentifier Identifier of the device connected to the IoT Box
|
||||
* @param {string?} actionId Identifier to match the specific response we are listening for
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onSuccess Callback to run when a message is received
|
||||
* @param {(message: Record<string, unknown>, deviceId: string) => void} onFailure Callback to run when the request fails
|
||||
*/
|
||||
async onMessage(
|
||||
iotIdentifier,
|
||||
deviceIdentifier,
|
||||
actionId = null,
|
||||
onSuccess = () => {},
|
||||
onFailure = () => {}
|
||||
) {
|
||||
const connection = await this.waitForConnection(iotIdentifier);
|
||||
|
||||
const messageCallback = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (
|
||||
message.device_identifier === deviceIdentifier &&
|
||||
(!actionId ||
|
||||
actionId === message.action_args?.session_id ||
|
||||
actionId === message.owner)
|
||||
) {
|
||||
const callback = message.status === "success" || message.status?.status === "connected" ? onSuccess : onFailure;
|
||||
callback(message);
|
||||
connection.channel.removeEventListener("message", messageCallback);
|
||||
}
|
||||
};
|
||||
|
||||
connection.channel.addEventListener("message", messageCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} iotIdentifier
|
||||
*/
|
||||
async waitForConnection(iotIdentifier) {
|
||||
const { connection, channel } = await this.openConnection(iotIdentifier);
|
||||
|
||||
if (!["new", "connecting"].includes(connection.connectionState)) {
|
||||
return this.connections[iotIdentifier];
|
||||
}
|
||||
|
||||
const connectedPromise = new Promise((resolve, reject) => {
|
||||
const onConnectionChange = () => {
|
||||
if (connection.connectionState === "connected") {
|
||||
resolve();
|
||||
} else if (
|
||||
["failed", "closed", "disconnected"].includes(connection.connectionState)
|
||||
) {
|
||||
reject(`WebRTC connection is '${connection.connectionState}'`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
connection.removeEventListener("connectionstatechange", onConnectionChange);
|
||||
};
|
||||
connection.addEventListener("connectionstatechange", onConnectionChange);
|
||||
setTimeout(() => reject("WebRTC connection timed out"), CONNECT_TIMEOUT_MS);
|
||||
});
|
||||
const channelOpenPromise = new Promise((resolve) => {
|
||||
const onOpen = () => {
|
||||
resolve();
|
||||
channel.removeEventListener("open", onOpen);
|
||||
};
|
||||
channel.addEventListener("open", onOpen);
|
||||
});
|
||||
|
||||
await connectedPromise;
|
||||
await channelOpenPromise;
|
||||
|
||||
return this.connections[iotIdentifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} iotIdentifier
|
||||
*/
|
||||
async openConnection(iotIdentifier) {
|
||||
if (this.connections[iotIdentifier]) {
|
||||
return this.connections[iotIdentifier];
|
||||
}
|
||||
|
||||
const peerConnection = new RTCPeerConnection();
|
||||
const dataChannel = peerConnection.createDataChannel("iot");
|
||||
|
||||
this.connections[iotIdentifier] = {
|
||||
id: uuid(),
|
||||
connection: peerConnection,
|
||||
channel: dataChannel,
|
||||
};
|
||||
|
||||
const offer = await peerConnection.createOffer();
|
||||
peerConnection.setLocalDescription(offer);
|
||||
|
||||
const onConnectionChange = () => {
|
||||
if (["failed", "closed", "disconnected"].includes(peerConnection.connectionState)) {
|
||||
dataChannel.close();
|
||||
peerConnection.close();
|
||||
delete this.connections[iotIdentifier];
|
||||
peerConnection.removeEventListener("connectionstatechange", onConnectionChange);
|
||||
}
|
||||
};
|
||||
peerConnection.addEventListener("connectionstatechange", onConnectionChange);
|
||||
|
||||
const onIotAnswer = (payload) => {
|
||||
const { iot_box_identifier, answer } = payload;
|
||||
if (
|
||||
iot_box_identifier !== iotIdentifier ||
|
||||
peerConnection.signalingState !== "have-local-offer"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
peerConnection.setRemoteDescription(answer);
|
||||
this.busService.unsubscribe("webrtc_answer", onIotAnswer);
|
||||
};
|
||||
this.busService.subscribe("webrtc_answer", onIotAnswer);
|
||||
this.busService.addChannel(this.websocket.iotChannel);
|
||||
|
||||
await this.websocket.sendMessage(iotIdentifier, { offer }, null, "webrtc_offer");
|
||||
|
||||
return this.connections[iotIdentifier];
|
||||
}
|
||||
}
|
||||
96
fusion_iot/iot/static/src/network_utils/iot_websocket.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
|
||||
/**
|
||||
* Class to handle Websocket connections
|
||||
*/
|
||||
export class IotWebsocket {
|
||||
constructor() {
|
||||
this.setup(...arguments);
|
||||
}
|
||||
|
||||
async setup({ bus_service, orm, lazy_session }) {
|
||||
this.busService = bus_service;
|
||||
this.orm = orm;
|
||||
if (lazy_session) {
|
||||
lazy_session.getValue("iot_channel", (iotChannel) => {
|
||||
this.iotChannel = iotChannel;
|
||||
});
|
||||
} else {
|
||||
this.iotChannel = await this.orm.call("iot.channel", "get_iot_channel", [0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the IoT Box
|
||||
* @param iotBoxIdentifier Identifier of the IoT Box
|
||||
* @param message Data to send to the device
|
||||
* @param messageId Unique identifier for the message (optional)
|
||||
* @param messageType Type of message to send (optional)
|
||||
* @returns {Promise<*>} The message ID
|
||||
*/
|
||||
async sendMessage(iotBoxIdentifier, message, messageId = null, messageType = 'iot_action') {
|
||||
messageId ??= uuid();
|
||||
|
||||
await this.orm.call("iot.channel", "send_message", [
|
||||
{
|
||||
iot_identifiers: [iotBoxIdentifier],
|
||||
iot_identifier: iotBoxIdentifier, // compatibility with v19.1+ IoT Boxes
|
||||
session_id: messageId,
|
||||
...message
|
||||
},
|
||||
messageType
|
||||
]);
|
||||
|
||||
return messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for events/messages coming from the IoT Box.
|
||||
* This method allows defining callbacks for success and failure cases.
|
||||
* @param iotBoxIdentifier Identifier of the IoT Box
|
||||
* @param deviceIdentifier Identifier of the device connected to the IoT Box
|
||||
* @param onSuccess Callback to run when a message is received (can return ``message``, ``deviceIdentifier``, and ``messageId``)
|
||||
* @param onFailure Callback to run when the request fails (can return ``deviceIdentifier`` and ``messageId``)
|
||||
* @param messageType The type of message to listen for (optional)
|
||||
* @param sessionId The session ID to listen for (optional)
|
||||
*/
|
||||
onMessage(
|
||||
iotBoxIdentifier,
|
||||
deviceIdentifier,
|
||||
onSuccess = (_message, _deviceIdentifier, _messageId) => {},
|
||||
onFailure = (_message, _deviceIdentifier, _messageId) => {},
|
||||
messageType = 'operation_confirmation',
|
||||
sessionId = null,
|
||||
) {
|
||||
if (!this.iotChannel) {
|
||||
console.error("No IoT Channel found");
|
||||
return;
|
||||
}
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.debug("Websocket timeout for", iotBoxIdentifier, deviceIdentifier, sessionId);
|
||||
onFailure({
|
||||
status: "timeout",
|
||||
message: "Timeout waiting for IoT Box response, please try again.",
|
||||
}, deviceIdentifier, sessionId);
|
||||
this.busService.unsubscribe(messageType, messageCallback);
|
||||
}, 6000); // error callback if the listener is not called within 6 seconds
|
||||
|
||||
const messageCallback = (event) => {
|
||||
const { session_id, iot_box_identifier, device_identifier, message } = event;
|
||||
if (
|
||||
iot_box_identifier !== iotBoxIdentifier ||
|
||||
device_identifier !== deviceIdentifier ||
|
||||
(sessionId && session_id !== sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = message.status === "success" || message.status?.status === "connected" ? onSuccess : onFailure;
|
||||
callback(message);
|
||||
clearTimeout(timeoutId);
|
||||
this.busService.unsubscribe(messageType, messageCallback);
|
||||
}
|
||||
|
||||
this.busService.addChannel(this.iotChannel);
|
||||
this.busService.subscribe(messageType, messageCallback);
|
||||
}
|
||||
}
|
||||
BIN
fusion_iot/iot/static/src/overrides/._network_utils
Executable file
@@ -0,0 +1,47 @@
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { IoTLongpolling } from "@iot_base/network_utils/longpolling";
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
|
||||
patch(IoTLongpolling.prototype, {
|
||||
/**
|
||||
* Send a message to the IoT Box (action route)
|
||||
* @param iotBoxIp IP Address of the IoT Box
|
||||
* @param message Data to send to the device
|
||||
* @param messageId Unique identifier for the message
|
||||
* @param fallback If longpolling has a fallback option (e.g. websocket), do not display errors to the user
|
||||
* @returns {Promise<*>} response of the request (response.result tells if the device is connected or not)
|
||||
*/
|
||||
async sendMessage(iotBoxIp, message, messageId = null, fallback = false) {
|
||||
messageId ??= uuid();
|
||||
return this._rpcIoT(iotBoxIp, '/iot_drivers/action', { session_id: messageId, ...message }, undefined, fallback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen for messages from the IoT Box (polling the IoT Box)
|
||||
* @param iotBoxIp IP Address of the IoT Box
|
||||
* @param iotDeviceIdentifier Identifier of the device connected to the IoT Box
|
||||
* @param onSuccess Callback to run when a successful response is received (can return ``message``, ``deviceIdentifier``, and ``messageId``)
|
||||
* @param onFailure Callback to run when the request fails (can return ``deviceIdentifier`` and ``messageId``)
|
||||
* @param requestId The request ID to listen for (optional)
|
||||
*/
|
||||
onMessage(
|
||||
iotBoxIp,
|
||||
iotDeviceIdentifier,
|
||||
onSuccess = (_message, _deviceIdentifier, _messageId) => {},
|
||||
onFailure = (_message, _deviceIdentifier, _messageId) => {},
|
||||
requestId = null
|
||||
) {
|
||||
const listenerCallback = (message) => {
|
||||
if (requestId && message.owner !== requestId) {
|
||||
return;
|
||||
}
|
||||
this.removeListener(iotBoxIp, iotDeviceIdentifier, requestId);
|
||||
if (message.status === "success" || message.status?.status === "connected") { // 'connected' is the serial driver success status
|
||||
onSuccess(message, iotDeviceIdentifier, requestId);
|
||||
} else {
|
||||
onFailure(message, iotDeviceIdentifier, requestId);
|
||||
}
|
||||
}
|
||||
return this.addListener(iotBoxIp, [ iotDeviceIdentifier ], requestId, listenerCallback, true);
|
||||
},
|
||||
});
|
||||
44
fusion_iot/iot/static/src/select_printer_wizard.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { onWillUnmount, useSubEnv } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class SelectPrinterFormController extends FormController {
|
||||
setup () {
|
||||
super.setup();
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
this.onClickViewButton = this.env.onClickViewButton;
|
||||
|
||||
onWillUnmount(() => {
|
||||
// If the user closes the popup without selecting a printer we still send a message back
|
||||
this.env.bus.trigger("printer-selected", { reportId: this.props.context.report_id, deviceSettings: null });
|
||||
})
|
||||
useSubEnv({ onClickViewButton: this.onClickViewButtonIoT.bind(this) });
|
||||
}
|
||||
|
||||
async onClickViewButtonIoT(params) {
|
||||
const deviceSettings = {
|
||||
selectedDevices: this.model.root.evalContextWithVirtualIds.device_ids,
|
||||
skipDialog: this.model.root.evalContextWithVirtualIds.do_not_ask_again,
|
||||
};
|
||||
if (deviceSettings.selectedDevices.length > 0) {
|
||||
this.env.bus.trigger("printer-selected", { reportId: this.props.context.report_id, deviceSettings });
|
||||
this.onClickViewButton(params);
|
||||
} else {
|
||||
this.notification.add(_t("Select at least one printer"), {
|
||||
title: _t("No printer selected"),
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const selectPrinterForm = {
|
||||
...formView,
|
||||
Controller: SelectPrinterFormController,
|
||||
}
|
||||
|
||||
registry.category("views").add('select_printers_wizard', selectPrinterForm);
|
||||
BIN
fusion_iot/iot/static/src/view_widgets/._device_list.js
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._header_button.xml
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._iot_download_logs.js
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._iot_remote_debug.js
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._iot_remote_debug.xml
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._iot_reset_password.js
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._iot_restart_odoo.js
Normal file
BIN
fusion_iot/iot/static/src/view_widgets/._test_iot_box.js
Normal file
34
fusion_iot/iot/static/src/view_widgets/device_list.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
|
||||
export class DeviceListField extends X2ManyField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
/**
|
||||
* By default the `X2ManyField` opens records in a dialog,
|
||||
* however this dialog doesn't run the `js_class` Controller
|
||||
* which is responsible for saving fields to the IoT box.
|
||||
*
|
||||
* We override the behaviour to open the regular form view
|
||||
* for the device, working around the issue.
|
||||
* @override
|
||||
*/
|
||||
async openRecord(record) {
|
||||
const action = await this.orm.call(record.resModel, "get_formview_action", [[record.resId]], {
|
||||
context: this.props.context,
|
||||
});
|
||||
await this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const deviceListField = {
|
||||
...x2ManyField,
|
||||
component: DeviceListField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("device_list_field", deviceListField);
|
||||
14
fusion_iot/iot/static/src/view_widgets/header_button.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="iot.HeaderButton">
|
||||
<button type="button"
|
||||
t-att-class="'btn btn-' + props.btn_class"
|
||||
t-att-data-tooltip="props.btn_name"
|
||||
t-att-aria-label="props.btn_name"
|
||||
t-on-click.stop="onClick">
|
||||
<t t-esc='props.btn_name'/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
53
fusion_iot/iot/static/src/view_widgets/iot_download_logs.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { _t } from '@web/core/l10n/translation';
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { formatEndpoint } from "@iot_base/network_utils/http";
|
||||
|
||||
export class IoTBoxDownloadLogs extends Component {
|
||||
static template = `iot.HeaderButton`;
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
btn_name: { type: String },
|
||||
btn_class: { type: String },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService('notification');
|
||||
this.http = useService('http');
|
||||
}
|
||||
get ip_url() {
|
||||
return formatEndpoint(this.props.record.data.ip, '');
|
||||
}
|
||||
get name() {
|
||||
return this.props.record.data.name;
|
||||
}
|
||||
async onClick() {
|
||||
try {
|
||||
const response = await this.http.get(this.ip_url + '/hw_proxy/hello', 'text');
|
||||
if (response == 'ping') {
|
||||
window.location = this.ip_url + '/iot_drivers/download_logs';
|
||||
} else {
|
||||
this.doWarnFail();
|
||||
}
|
||||
} catch {
|
||||
this.doWarnFail();
|
||||
}
|
||||
}
|
||||
doWarnFail() {
|
||||
this.notification.add(_t('Failed to download logs from %s', this.name), { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
export const ioTBoxDownloadLogs = {
|
||||
component: IoTBoxDownloadLogs,
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
btn_name: attrs.btn_name,
|
||||
btn_class: attrs.btn_class
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("view_widgets").add("iot_download_logs", ioTBoxDownloadLogs);
|
||||
100
fusion_iot/iot/static/src/view_widgets/iot_remote_debug.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class IoTRemoteDebug extends Component {
|
||||
static template = `iot.HeaderButton`;
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
btn_name: { type: String },
|
||||
btn_class: { type: String },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.iotHttp = useService("iot_http");
|
||||
this.dialog = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
|
||||
this.state = useState({ enabled: false });
|
||||
|
||||
// Get ngrok status on view load
|
||||
this.iotHttp.websocket.onMessage(this.identifier, null, this.onMessageUpdateStatus.bind(this));
|
||||
this.iotHttp.websocket.sendMessage(this.identifier, { 'status': true }, null, 'remote_debug');
|
||||
}
|
||||
|
||||
get identifier() {
|
||||
return this.props.record.data.identifier;
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
this.dialog.add(TokenDialog, {
|
||||
validate: this.enableRemoteDebug.bind(this),
|
||||
enabled: this.state.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
async enableRemoteDebug(token) {
|
||||
this.iotHttp.websocket.onMessage(
|
||||
this.identifier,
|
||||
null,
|
||||
(message) => {
|
||||
this.onMessageUpdateStatus(message);
|
||||
if (token && !this.state.enabled) {
|
||||
return this.onFailure();
|
||||
}
|
||||
this.notification.add(
|
||||
_t("Remote debug is %s.", this.state.enabled ? _t("enabled") : _t("disabled")), {
|
||||
type: "info",
|
||||
}
|
||||
);
|
||||
},
|
||||
this.onFailure.bind(this),
|
||||
);
|
||||
this.iotHttp.websocket.sendMessage(this.identifier, { token }, null, "remote_debug");
|
||||
}
|
||||
|
||||
onMessageUpdateStatus(message) {
|
||||
this.state.enabled = message.result?.enabled;
|
||||
}
|
||||
|
||||
onFailure() {
|
||||
this.notification.add(_t("Failed to toggle remote debug."), {
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const ioTRemoteDebug = {
|
||||
component: IoTRemoteDebug,
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
btn_name: attrs.btn_name,
|
||||
btn_class: attrs.btn_class,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export class TokenDialog extends Component {
|
||||
static template = "iot.RemoteDebugDialog";
|
||||
static components = { Dialog };
|
||||
static props = {
|
||||
validate: Function,
|
||||
close: Function,
|
||||
enabled: Boolean,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.state = useState({ token: "" });
|
||||
}
|
||||
|
||||
validate() {
|
||||
this.props.validate(this.state.token);
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("iot_remote_debug", ioTRemoteDebug);
|
||||
29
fusion_iot/iot/static/src/view_widgets/iot_remote_debug.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="iot.RemoteDebugDialog">
|
||||
<Dialog title.translate="Remote Debug">
|
||||
<div t-if="!props.enabled" class="d-flex flex-column gap-3">
|
||||
<div>
|
||||
You can enable remote debug on your IoT Box by providing a Ngrok authtoken.<br/>
|
||||
<span><b>Be careful:</b> it enables remote access to your local network to the owner of the token.</span>
|
||||
</div>
|
||||
<div class="d-flex gap-3 w-50">
|
||||
<label class="form-label">Token</label>
|
||||
<div class="o_field_widget o_field_char w-100">
|
||||
<input t-model="state.token" class="o_input" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="">
|
||||
Remote debug is enabled, the owner of the token you provided has access to both your IoT Box
|
||||
and local network. If it's unintended, click on "Disable" below.
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary" t-on-click="validate" t-att-disabled="!state.token and !props.enabled">
|
||||
<t t-esc="props.enabled ? 'Disable' : 'Enable'"/>
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="props.close">Discard</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
58
fusion_iot/iot/static/src/view_widgets/iot_reset_password.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class IoTResetPassword extends Component {
|
||||
static template = `iot.HeaderButton`;
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
btn_name: { type: String },
|
||||
btn_class: { type: String },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.longpolling = useService("iot_longpolling");
|
||||
this.notification = useService("notification");
|
||||
}
|
||||
|
||||
get ip() {
|
||||
return this.props.record.data.ip;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.props.record.data.name;
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
try {
|
||||
const response = await this.longpolling.action(this.ip, null, null, false, "/iot_drivers/generate_password");
|
||||
if (!response?.result?.password) {
|
||||
return this.doWarnFail();
|
||||
}
|
||||
this.notification.add(response.result.password, {
|
||||
type: "info",
|
||||
title: _t("New SSH password for %s", this.name),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
doWarnFail() {
|
||||
this.notification.add(_t("Failed to reset %s password.", this.name), { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
export const ioTResetPassword = {
|
||||
component: IoTResetPassword,
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
btn_name: attrs.btn_name,
|
||||
btn_class: attrs.btn_class,
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("view_widgets").add("iot_reset_password", ioTResetPassword);
|
||||
58
fusion_iot/iot/static/src/view_widgets/iot_restart_odoo.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class IoTRestartOdoo extends Component {
|
||||
static template = `iot.HeaderButton`;
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
btn_name: { type: String },
|
||||
btn_class: { type: String },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialog = useService("dialog");
|
||||
this.iotHttpService = useService("iot_http");
|
||||
this.notification = useService("notification");
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: _t("Are you sure you want to restart Odoo on the IoT Box?"),
|
||||
confirm: this.restartOdoo.bind(this),
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
restartOdoo() {
|
||||
const { identifier, name } = this.props.record.data;
|
||||
this.iotHttpService.action(
|
||||
this.props.record._config.resId,
|
||||
identifier,
|
||||
{ action: "restart_odoo" },
|
||||
() => this.notification.add(_t("%s is currently restarting", name), {
|
||||
type: "info",
|
||||
}),
|
||||
() => {
|
||||
this.notification.add(
|
||||
_t("Failed to send the restart command to the IoT Box ('%s')", name), { type: "danger" }
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ioTRestartOdoo = {
|
||||
component: IoTRestartOdoo,
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
btn_name: attrs.btn_name,
|
||||
btn_class: attrs.btn_class,
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("view_widgets").add("iot_restart_odoo", ioTRestartOdoo);
|
||||
122
fusion_iot/iot/static/src/view_widgets/test_iot_box.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { uuid } from "@web/core/utils/strings";
|
||||
import { _t } from '@web/core/l10n/translation';
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
export class TestIotBox extends Component {
|
||||
static template = `iot.HeaderButton`;
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
btn_name: { type: String },
|
||||
btn_class: { type: String },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService('notification');
|
||||
this.iotHttpService = useService('iot_http');
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const { ip, identifier } = this.props.record.data;
|
||||
const requestId = uuid();
|
||||
this.completeSuccess = true;
|
||||
const failureCallback = (protocol) => {
|
||||
this.notification.add(_t("Communication protocol '%s' is not working properly.", protocol), {
|
||||
type: 'danger'
|
||||
});
|
||||
this.completeSuccess = false;
|
||||
}
|
||||
|
||||
this.removeTestingNotification = this.notification.add(
|
||||
_t("Testing communication with IoT Box and network quality, please wait..."),
|
||||
{
|
||||
type: 'info',
|
||||
autocloseDelay: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
// Check webRTC
|
||||
try {
|
||||
await this.iotHttpService.webRtc.onMessage(identifier, identifier, requestId, () => {}, () => {
|
||||
failureCallback("WebRTC");
|
||||
});
|
||||
await this.iotHttpService.webRtc.sendMessage(identifier, {}, requestId, "test_protocol");
|
||||
} catch {
|
||||
// Catch connection timeout (not going through onFailure)
|
||||
failureCallback("WebRTC");
|
||||
}
|
||||
|
||||
// Check longpolling (no onMessage as we only check if the endpoint is reachable)
|
||||
try {
|
||||
await this.iotHttpService.longpolling.sendMessage(ip, {
|
||||
device_identifier: identifier,
|
||||
data: {}
|
||||
}, requestId, true);
|
||||
} catch {
|
||||
failureCallback("Longpolling");
|
||||
}
|
||||
|
||||
// Check websocket
|
||||
this.iotHttpService.websocket.onMessage(
|
||||
identifier,
|
||||
identifier,
|
||||
this.onConnectionTestSuccess.bind(this),
|
||||
() => failureCallback("Websocket"),
|
||||
undefined,
|
||||
requestId,
|
||||
);
|
||||
await this.iotHttpService.websocket.sendMessage(identifier, {}, requestId, "test_connection");
|
||||
}
|
||||
|
||||
onConnectionTestSuccess(data) {
|
||||
this.removeTestingNotification?.();
|
||||
if (this.completeSuccess) {
|
||||
this.notification.add(_t("All communication protocols are working properly."), { type: 'success' });
|
||||
}
|
||||
if (!data.result) {
|
||||
this.notification.add(
|
||||
_t("Failed to check IoT Box network, check that it's connected to the Internet."), {
|
||||
type: 'danger'
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { lan_quality, wan_quality } = data.result;
|
||||
let type = 'success';
|
||||
if (lan_quality === "normal" || wan_quality === "normal") {
|
||||
type = 'info';
|
||||
}
|
||||
if (lan_quality === "slow" || wan_quality === "slow") {
|
||||
type = 'warning';
|
||||
}
|
||||
if (lan_quality === "unreachable" || wan_quality === "unreachable") {
|
||||
type = 'danger';
|
||||
}
|
||||
|
||||
this.notification.add(
|
||||
_t("IoT Box local network is %(lan_quality)s and internet is %(wan_quality)s", {
|
||||
lan_quality,
|
||||
wan_quality
|
||||
}),
|
||||
{
|
||||
type,
|
||||
autocloseDelay: 6000, // display longer to read (= websocket timeout)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const testIotBox = {
|
||||
component: TestIotBox,
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
btn_name: attrs.btn_name,
|
||||
btn_class: attrs.btn_class
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("view_widgets").add("test_iot_box", testIotBox);
|
||||