From 6646174064a739593da2f9dcc754315c0f2158af Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 5 Apr 2023 21:20:09 +0200 Subject: [PATCH 001/405] * Make pppp_open() accept config object, instead of entire env --- ankerctl.py | 4 ++-- cli/pppp.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index 7a60a4ed..6f6de1eb 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -233,7 +233,7 @@ def pppp_print_file(env, file, no_act): file, so anytime a file is uploaded, the old one is deleted. """ env.require_config() - api = cli.pppp.pppp_open(env) + api = cli.pppp.pppp_open(env.config) data = file.read() fui = FileUploadInfo.from_file(file.name, user_name="ankerctl", user_id="-", machine_id="-") @@ -266,7 +266,7 @@ def pppp_capture_video(env, file, max_size): "ffplay" from the ffmpeg program suite. """ env.require_config() - api = cli.pppp.pppp_open(env) + api = cli.pppp.pppp_open(env.config) cmd = {"commandType": P2PSubCmdType.START_LIVE, "data": {"encryptkey": "x", "accountId": "y"}} api.send_xzyh(json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD) diff --git a/cli/pppp.py b/cli/pppp.py index 94eef3ca..b539c721 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -11,8 +11,8 @@ from libflagship.ppppapi import AnkerPPPPApi, FileTransfer -def pppp_open(env): - with env.config.open() as cfg: +def pppp_open(config): + with config.open() as cfg: printer = cfg.printers[0] api = AnkerPPPPApi.open_lan(Duid.from_string(printer.p2p_duid), host=printer.ip_addr) From 717558c01ad42aec4ccdc6d489e2261b09eee809 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 5 Apr 2023 21:28:13 +0200 Subject: [PATCH 002/405] * Make mqtt_open() accept config object, instead of entire env --- ankerctl.py | 8 ++++---- cli/mqtt.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index 6f6de1eb..a8a55179 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -95,7 +95,7 @@ def mqtt_monitor(env): Connect to mqtt broker, and show low-level events in realtime. """ - client = cli.mqtt.mqtt_open(env) + client = cli.mqtt.mqtt_open(env.config, env.insecure) for msg, body in client.fetchloop(): log.info(f"TOPIC [{msg.topic}]") @@ -144,7 +144,7 @@ def mqtt_send(env, command_type, args, force): log.fatal("Sending DEVICE_NAME_SET without devName= will crash printer (override with --force)") return - client = cli.mqtt.mqtt_open(env) + client = cli.mqtt.mqtt_open(env.config, env.insecure) cli.mqtt.mqtt_command(client, cmd) @@ -156,7 +156,7 @@ def mqtt_rename_printer(env, newname): Set a new nickname for your printer """ - client = cli.mqtt.mqtt_open(env) + client = cli.mqtt.mqtt_open(env.config, env.insecure) cmd = { "commandType": MqttMsgType.ZZ_MQTT_CMD_DEVICE_NAME_SET, @@ -175,7 +175,7 @@ def mqtt_gcode(env): Press Ctrl-C to exit. (or Ctrl-D to close connection, except on Windows) """ - client = cli.mqtt.mqtt_open(env) + client = cli.mqtt.mqtt_open(env.config, env.insecure) while True: gcode = click.prompt("gcode", prompt_suffix="> ") diff --git a/cli/mqtt.py b/cli/mqtt.py index d4b6e5c4..892e99c5 100644 --- a/cli/mqtt.py +++ b/cli/mqtt.py @@ -11,19 +11,19 @@ } -def mqtt_open(env): - with env.config.open() as cfg: +def mqtt_open(config, insecure): + with config.open() as cfg: printer = cfg.printers[0] acct = cfg.account server = servertable[acct.region] - env.log.info(f"Connecting to {server}") + log.info(f"Connecting to {server}") client = AnkerMQTTBaseClient.login( printer.sn, acct.mqtt_username, acct.mqtt_password, printer.mqtt_key, ca_certs="examples/ankermake-mqtt.crt", - verify=not env.insecure, + verify=not insecure, ) client.connect(server) return client From ed3f9d0148f03c34a2d83ee77ad089ddef565670 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Thu, 6 Apr 2023 20:28:34 +0200 Subject: [PATCH 003/405] * Refactored ankerctl: moved web server code into web/ --- ankerctl.py | 60 ++------------------------------------- web/__init__.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 web/__init__.py diff --git a/ankerctl.py b/ankerctl.py index a8a55179..71085537 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -7,7 +7,6 @@ from os import path from rich import print # you need python3 from tqdm import tqdm -from flask import Flask, request, render_template import cli.config import cli.model @@ -26,6 +25,8 @@ from libflagship.pppp import PktLanSearch, P2PCmdType, P2PSubCmdType, FileTransfer from libflagship.ppppapi import AnkerPPPPApi, FileUploadInfo, PPPPError +import web + class Environment: def __init__(self): @@ -454,68 +455,13 @@ def webserver(env): env.require_config() -app = Flask(__name__, template_folder='./static') -# app.config['TEMPLATES_AUTO_RELOAD'] = True - - -@app.get("/") -def app_root(): - host = request.host.split(':') - requestPort = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 - return render_template("index.html", requestPort=requestPort, requestHost=host[0]) - - -@app.get("/api/version") -def app_api_version(): - return { - "api": "0.1", - "server": "1.9.0", - "text": "OctoPrint 1.9.0" - } - - -@app.post("/api/files/local") -def app_api_files_local(): - env = app.config["env"] - - user_name = request.headers.get("User-Agent", "ankerctl").split("/")[0] - - no_act = not cli.util.parse_http_bool(request.form["print"]) - - if no_act: - cli.util.http_abort(409, "Upload-only not supported by Ankermake M5") - - fd = request.files["file"] - - api = cli.pppp.pppp_open(env) - - data = fd.read() - fui = FileUploadInfo.from_data(data, fd.filename, user_name=user_name, user_id="-", machine_id="-") - log.info(f"Going to upload {fui.size} bytes as {fui.name!r}") - try: - cli.pppp.pppp_send_file(api, fui, data) - log.info("File upload complete. Requesting print start of job.") - api.aabb_request(b"", frametype=FileTransfer.END) - except PPPPError as E: - log.error(f"Could not send print job: {E}") - else: - log.info("Successfully sent print job") - finally: - api.stop() - - return {} - - @webserver.command("run", help="Run ankerctl webserver") @click.option("--host", default='127.0.0.1', envvar="FLASK_HOST", help="Network interface to bind to") @click.option("--port", default=4470, envvar="FLASK_PORT", help="Port to bind to") @pass_env def webserver(env, host, port): env.require_config() - app.config["env"] = env - app.config["port"] = port - app.config["host"] = host - app.run(host=host,port=port) + web.webserver(env.config, host, port) if __name__ == "__main__": diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 00000000..180e8516 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,74 @@ +import logging as log + +from flask import Flask, request, render_template + +from libflagship.ppppapi import FileUploadInfo, PPPPError, FileTransfer + +import cli.mqtt + + +app = Flask( + __name__, + static_folder="../static", + template_folder="../static" +) +# app.config['TEMPLATES_AUTO_RELOAD'] = True + + +@app.get("/") +def app_root(): + host = request.host.split(':') + requestPort = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 + return render_template( + "index.html", + requestPort=requestPort, + requestHost=host[0] + ) + + +@app.get("/api/version") +def app_api_version(): + return { + "api": "0.1", + "server": "1.9.0", + "text": "OctoPrint 1.9.0" + } + + +@app.post("/api/files/local") +def app_api_files_local(): + config = app.config["config"] + + user_name = request.headers.get("User-Agent", "ankerctl").split("/")[0] + + no_act = not cli.util.parse_http_bool(request.form["print"]) + + if no_act: + cli.util.http_abort(409, "Upload-only not supported by Ankermake M5") + + fd = request.files["file"] + + api = cli.pppp.pppp_open(config) + + data = fd.read() + fui = FileUploadInfo.from_data(data, fd.filename, user_name=user_name, user_id="-", machine_id="-") + log.info(f"Going to upload {fui.size} bytes as {fui.name!r}") + try: + cli.pppp.pppp_send_file(api, fui, data) + log.info("File upload complete. Requesting print start of job.") + api.aabb_request(b"", frametype=FileTransfer.END) + except PPPPError as E: + log.error(f"Could not send print job: {E}") + else: + log.info("Successfully sent print job") + finally: + api.stop() + + return {} + + +def webserver(config, host, port): + app.config["config"] = config + app.config["port"] = port + app.config["host"] = host + app.run(host=host, port=port) From c142ae4d32b0818b131fe01e9137742c3e5c1428 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Thu, 6 Apr 2023 20:30:53 +0200 Subject: [PATCH 004/405] + Implemented basic mqtt event streaming over websocket, allowing it to be used in web ui --- requirements.txt | 1 + static/index.html | 5 +++++ web/__init__.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/requirements.txt b/requirements.txt index 1d3cf06f..bd2e4e93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ tinyec==0.4.0 crcmod==1.7 tqdm==4.65.0 flask==2.2.0 +flask-sock==0.6.0 diff --git a/static/index.html b/static/index.html index 76749b3b..5cafcf5d 100644 --- a/static/index.html +++ b/static/index.html @@ -46,6 +46,11 @@

Connecting PrusaSlicer/SuperSlicer

- + + From 1d5c8b1f0e9736dc0c5bb2c2ab5086a152cc4e2d Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 14:26:55 +0200 Subject: [PATCH 011/405] * Move websockets into /ws scope --- static/ankersrv.js | 4 ++-- web/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/ankersrv.js b/static/ankersrv.js index 62621d45..080cf3dd 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -1,6 +1,6 @@ $(function () { - socket = new WebSocket("ws://" + location.host + "/mqtt"); + socket = new WebSocket("ws://" + location.host + "/ws/mqtt"); socket.addEventListener('message', ev => { console.log(JSON.parse(ev.data)); }); @@ -21,7 +21,7 @@ $(function () { } }); - var ws = new WebSocket("ws://" + location.host + "/video"); + var ws = new WebSocket("ws://" + location.host + "/ws/video"); ws.binaryType = 'arraybuffer'; ws.addEventListener('message',function(event) { jmuxer.feed({ diff --git a/web/__init__.py b/web/__init__.py index 72085261..ac9a14f9 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -94,7 +94,7 @@ def startup(): app.videoq = VideoQueue() -@sock.route("/mqtt") +@sock.route("/ws/mqtt") def mqtt(sock): queue = Queue() @@ -108,7 +108,7 @@ def mqtt(sock): app.mqttq.del_target(queue) -@sock.route("/video") +@sock.route("/ws/video") def video(sock): queue = Queue() app.videoq.add_target(queue) From eb04197eb155cf97e0433f9442ba35ee1fa1efb0 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 14:27:45 +0200 Subject: [PATCH 012/405] + Added proof-of-concept http endpoint for video streaming. Works with VLC: "vlc --h264-fps=15 --network-caching=100 http/h264://localhost:4470/video" --- web/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web/__init__.py b/web/__init__.py index ac9a14f9..6c42d419 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,7 +1,7 @@ import json import logging as log -from flask import Flask, request, render_template +from flask import Flask, request, render_template, Response from flask_sock import Sock from threading import Thread @@ -120,6 +120,22 @@ def video(sock): app.videoq.del_target(queue) +@app.get("/video") +def video2(): + + def generate(): + queue = Queue() + app.videoq.add_target(queue) + try: + while True: + data = queue.get() + yield data + finally: + app.videoq.del_target(queue) + + return Response(generate(), mimetype='video/mp4') + + @app.get("/") def app_root(): host = request.host.split(':') From 55e135cd9651e27b71ebf8593e7f9c06d11d5dd9 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:53:06 +0200 Subject: [PATCH 013/405] + Implemented timeout support for Wire.read() --- libflagship/ppppapi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index ea702688..e2dc23fc 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -76,9 +76,15 @@ def __init__(self): self.buf = [] self.rx, self.tx = Pipe(False) - def read(self, size): + def read(self, size, timeout=None): + if timeout: + deadline = datetime.now() + timedelta(seconds=timeout) + while len(self.buf) < size: + if timeout and not self.rx.poll(timeout=(deadline - datetime.now()).total_seconds()): + return None self.buf.extend(self.rx.recv()) + res, self.buf = self.buf[:size], self.buf[size:] return bytes(res) From a7eda7314a77dbccb9c13aa700f251efdaabad82 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:54:05 +0200 Subject: [PATCH 014/405] + Implemented timeout support for Channel.read() --- libflagship/ppppapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index e2dc23fc..ae93e984 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -167,8 +167,8 @@ def wait(self): self.event.wait() self.event.clear() - def read(self, nbytes): - return self.rx.read(nbytes) + def read(self, nbytes, timeout=None): + return self.rx.read(nbytes, timeout) def write(self, payload, block=True): pdata = payload[:] From 59067bc1fa18bc0fd4ca6c233e15819ccd83c070 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:54:55 +0200 Subject: [PATCH 015/405] + Implemented timeout support for AnkerPPPPApi.recv_xzyh() --- libflagship/ppppapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index ae93e984..81333c47 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -347,11 +347,11 @@ def send_aabb(self, data, sn=0, pos=0, frametype=0, chan=1, block=True): return self.chans[chan].write(aabb.pack_with_crc(data), block=block) - def recv_xzyh(self, chan=1): + def recv_xzyh(self, chan=1, timeout=None): fd = self.chans[chan] xzyh = Xzyh.parse(fd.read(16))[0] - xzyh.data = fd.read(xzyh.len) + xzyh.data = fd.read(xzyh.len, timeout=timeout) return xzyh def recv_aabb(self, chan=1): From 2aaa085b0f058e1391aec0a01ebbc8d3eff451f7 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:56:07 +0200 Subject: [PATCH 016/405] + Implemented timeout support for cli.pppp.pppp_open() --- cli/pppp.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cli/pppp.py b/cli/pppp.py index b539c721..d527bf9f 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -3,6 +3,7 @@ import click import logging as log +from datetime import datetime, timedelta from tqdm import tqdm import cli.util @@ -11,18 +12,22 @@ from libflagship.ppppapi import AnkerPPPPApi, FileTransfer -def pppp_open(config): +def pppp_open(config, timeout=None): + if timeout: + deadline = datetime.now() + timedelta(seconds=timeout) + with config.open() as cfg: printer = cfg.printers[0] api = AnkerPPPPApi.open_lan(Duid.from_string(printer.p2p_duid), host=printer.ip_addr) log.info("Trying connect over pppp") - api.daemon = True api.start() api.send(PktLanSearch()) while not api.rdy: + if api.stopped.is_set() or (timeout and (datetime.now() > deadline)): + raise ConnectionRefusedError("Connection rejected by device") time.sleep(0.1) log.info("Established pppp connection") From d6c254ccd7427ffcc1496ec31cda5c22929e0fcc Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:57:05 +0200 Subject: [PATCH 017/405] * Make streaming endpoints gracefully handle EOFError --- web/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 6c42d419..95cbc7f8 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -101,7 +101,10 @@ def mqtt(sock): app.mqttq.add_target(queue) try: while True: - data = queue.get() + try: + data = queue.get() + except EOFError: + break log.debug(f"MQTT message: {data}") sock.send(json.dumps(data)) finally: @@ -114,7 +117,10 @@ def video(sock): app.videoq.add_target(queue) try: while True: - data = queue.get() + try: + data = queue.get() + except EOFError: + break sock.send(data) finally: app.videoq.del_target(queue) @@ -128,7 +134,10 @@ def generate(): app.videoq.add_target(queue) try: while True: - data = queue.get() + try: + data = queue.get() + except EOFError: + break yield data finally: app.videoq.del_target(queue) From 6b1beb6d2726c72f13670f88baf0170c51b5e4ea Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 21:59:51 +0200 Subject: [PATCH 018/405] * Rework MultiQueue into an on-demand thread worker. Refactor MqttQueue and VideoQueue to fit. Now drops connections when not needed, and handles connection retries. --- web/__init__.py | 174 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 32 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 95cbc7f8..e54d8851 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,10 +1,13 @@ import json +import atexit import logging as log +from datetime import datetime, timedelta +from enum import Enum from flask import Flask, request, render_template, Response from flask_sock import Sock -from threading import Thread +from threading import Thread, Event from multiprocessing import Queue from libflagship.util import enhex @@ -24,68 +27,175 @@ sock = Sock(app) +class RunState(Enum): + Starting = 2 + Running = 3 + # Idle = 4 + Stopping = 5 + Stopped = 6 + + class MultiQueue(Thread): - def __init__(self): + def __init__(self, idle_timeout=10): super().__init__() - self.running = False - self.targets = [] - - def start(self): + self.timeout = timedelta(seconds=idle_timeout) self.running = True + self.deadline = None + self.state = RunState.Stopped + self.wanted = False + self.targets = [] + self._event = Event() + atexit.register(self.atexit) super().start() - def stop(self): + def atexit(self): + log.info(f"{self.name}: Requesting thread exit..") self.running = False self.join() + log.info(f"{self.name}: Thread cleanup done") + + @property + def name(self): + return type(self).__name__ + + def start(self): + log.info(f"{self.name}: Requesting start") + self.wanted = True + self._event.set() + + def stop(self): + log.info(f"{self.name}: Requesting stop") + self.wanted = False + self._event.set() + + def run(self): + holdoff = None + + while self.running: + if self.state == RunState.Starting: + log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") + if datetime.now() > holdoff: + try: + log.info(f"{self.name} worker start") + self.worker_start() + except Exception as E: + log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worked started") + self.state = RunState.Running + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Running: + if self.wanted: + self.worker_run(timeout=0.3) + else: + log.info(f"{self.name}: Stopping worker") + holdoff = datetime.now() + self.state = RunState.Stopping + + elif self.state == RunState.Stopping: + if datetime.now() > holdoff: + try: + self.worker_stop() + except Exception as E: + log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worked stopped") + self.state = RunState.Stopped + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Stopped: + if self.wanted: + log.info(f"{self.name}: Starting worker") + holdoff = datetime.now() + self.state = RunState.Starting + else: + self._event.wait() + self._event.clear() + else: + raise ValueError("Unknown state value") + + log.info(f"{self.name}: Shutting down thread") + if self.state == RunState.Running: + self.worker_stop() + log.info(f"{self.name}: Thread exit") def put(self, obj): for target in self.targets: target.put(obj) def add_target(self, target): - if not self.running: + if not self.targets: self.start() self.targets.append(target) def del_target(self, target): if target in self.targets: self.targets.remove(target) - if not self.targets and self.running: - self.stop() + if not self.targets: + self.stop() + + def worker_start(self): + pass + + def worker_run(self, timeout): + pass + + def worker_stop(self): + pass class MqttQueue(MultiQueue): - def run(self): - client = cli.mqtt.mqtt_open(app.config["config"], True) - while self.running: - for msg, body in client.fetch(timeout=0.5): - log.info(f"TOPIC [{msg.topic}]") - log.debug(enhex(msg.payload[:])) + def worker_start(self): + self.client = cli.mqtt.mqtt_open(app.config["config"], True) + + def worker_run(self, timeout): + for msg, body in self.client.fetch(timeout=timeout): + log.info(f"TOPIC [{msg.topic}]") + log.debug(enhex(msg.payload[:])) + + for obj in body: + self.put(obj) - for obj in body: - self.put(obj) + def worker_stop(self): + del self.client class VideoQueue(MultiQueue): - def __init__(self): - super().__init__() + def send_command(self, commandType, **kwargs): + cmd = { + "commandType": commandType, + **kwargs + } + return self.api.send_xzyh( + json.dumps(cmd).encode(), + cmd=P2PCmdType.P2P_JSON_CMD + ) - def run(self): - api = cli.pppp.pppp_open(app.config["config"]) - cmd = {"commandType": P2PSubCmdType.START_LIVE, "data": {"encryptkey": "x", "accountId": "y"}} - api.send_xzyh(json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD) + def worker_start(self): + self.api = cli.pppp.pppp_open(app.config["config"], timeout=1) - try: - while self.running: - d = api.recv_xzyh(chan=1) - log.debug(f"Video data packet: {enhex(d.data):32}...") - self.put(d.data) - finally: - cmd = {"commandType": P2PSubCmdType.CLOSE_LIVE} - api.send_xzyh(json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD) + self.send_command(P2PSubCmdType.START_LIVE, data={"encryptkey": "x", "accountId": "y"}) + + def worker_run(self, timeout): + d = self.api.recv_xzyh(chan=1, timeout=timeout) + if not d: + return + + log.debug(f"Video data packet: {enhex(d.data):32}...") + self.put(d.data) + + def worked_stop(self): + self.api.send_command(P2PSubCmdType.CLOSE_LIVE) @app.before_first_request From bb39e9bac45072a5fd0dd5ab4579d90fc88cce29 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 22:18:01 +0200 Subject: [PATCH 019/405] + Implemented Wire.peek() --- libflagship/ppppapi.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 81333c47..4c44ed57 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -76,7 +76,7 @@ def __init__(self): self.buf = [] self.rx, self.tx = Pipe(False) - def read(self, size, timeout=None): + def peek(self, size, timeout=None): if timeout: deadline = datetime.now() + timedelta(seconds=timeout) @@ -85,8 +85,13 @@ def read(self, size, timeout=None): return None self.buf.extend(self.rx.recv()) - res, self.buf = self.buf[:size], self.buf[size:] - return bytes(res) + return bytes(self.buf[:size]) + + def read(self, size, timeout=None): + res = self.peek(size, timeout) + if res: + self.buf = self.buf[size:] + return res def write(self, data): self.tx.send(data) From e2562f05599a18dc87de56c1a4be66e46d5d2cc0 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 22:19:15 +0200 Subject: [PATCH 020/405] + Implemented Channel.peek() --- libflagship/ppppapi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 4c44ed57..ec48496b 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -172,6 +172,9 @@ def wait(self): self.event.wait() self.event.clear() + def peek(self, nbytes, timeout=None): + return self.rx.peek(nbytes, timeout) + def read(self, nbytes, timeout=None): return self.rx.read(nbytes, timeout) From fc5eba1c4a03094e03b40a6eee81460b0b8349b5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 22:19:19 +0200 Subject: [PATCH 021/405] * Rework recv_xzyh() to handle timeouts correctly: use .peek() for header, to avoid desynchronization on body timeout --- libflagship/ppppapi.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index ec48496b..70965bba 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -358,8 +358,17 @@ def send_aabb(self, data, sn=0, pos=0, frametype=0, chan=1, block=True): def recv_xzyh(self, chan=1, timeout=None): fd = self.chans[chan] - xzyh = Xzyh.parse(fd.read(16))[0] - xzyh.data = fd.read(xzyh.len, timeout=timeout) + hdr = fd.peek(16, timeout=timeout) + if not hdr: + return None + + xzyh = Xzyh.parse(hdr)[0] + + data = fd.read(xzyh.len + 16, timeout=timeout) + if not data: + return None + + xzyh.data = data[16:] return xzyh def recv_aabb(self, chan=1): From a15468f15e74ba7cb1c83df74f76ae1c9ec753c1 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 7 Apr 2023 22:19:46 +0200 Subject: [PATCH 022/405] * Move time.sleep() before state check in pppp_open(), to avoid race condition on establishing connection --- cli/pppp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/pppp.py b/cli/pppp.py index d527bf9f..96b09109 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -26,9 +26,9 @@ def pppp_open(config, timeout=None): api.send(PktLanSearch()) while not api.rdy: + time.sleep(0.1) if api.stopped.is_set() or (timeout and (datetime.now() > deadline)): raise ConnectionRefusedError("Connection rejected by device") - time.sleep(0.1) log.info("Established pppp connection") return api From 7e110dafe3c59b23b9dfdc4da74de0193ab90ae6 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 00:31:37 +0200 Subject: [PATCH 023/405] * Simplified websocket implementations using contextmanager --- web/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index e54d8851..2a70d5ce 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,6 +1,7 @@ import json import atexit import logging as log +import contextlib from datetime import datetime, timedelta from enum import Enum @@ -142,6 +143,13 @@ def del_target(self, target): if not self.targets: self.stop() + @contextlib.contextmanager + def tap(self): + queue = Queue() + self.add_target(queue) + yield queue + self.del_target(queue) + def worker_start(self): pass @@ -207,9 +215,7 @@ def startup(): @sock.route("/ws/mqtt") def mqtt(sock): - queue = Queue() - app.mqttq.add_target(queue) - try: + with app.mqttq.tap() as queue: while True: try: data = queue.get() @@ -217,23 +223,18 @@ def mqtt(sock): break log.debug(f"MQTT message: {data}") sock.send(json.dumps(data)) - finally: - app.mqttq.del_target(queue) @sock.route("/ws/video") def video(sock): - queue = Queue() - app.videoq.add_target(queue) - try: + + with app.videoq.tap() as queue: while True: try: data = queue.get() except EOFError: break sock.send(data) - finally: - app.videoq.del_target(queue) @app.get("/video") From e991210fe201376937425759e6a58ff7c7ca17fd Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 01:08:24 +0200 Subject: [PATCH 024/405] * Fixed typo in P2PSubCmdType.LIGHT_STATE_SWITCH --- libflagship/pppp.py | 2 +- specification/pppp.stf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index c175118c..677906af 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -109,7 +109,7 @@ class P2PSubCmdType(enum.IntEnum): START_LIVE = 0x03e8 # unknown CLOSE_LIVE = 0x03e9 # unknown VIDEO_RECORD_SWITCH = 0x03ea # unknown - LIGHT_STATE_SWITCH = 0x03ab # unknown + LIGHT_STATE_SWITCH = 0x03eb # unknown LIGHT_STATE_GET = 0x03ec # unknown LIVE_MODE_SET = 0x03ed # unknown LIVE_MODE_GET = 0x03ee # unknown diff --git a/specification/pppp.stf b/specification/pppp.stf index 70f698d8..45fff7ec 100644 --- a/specification/pppp.stf +++ b/specification/pppp.stf @@ -81,7 +81,7 @@ enum P2PSubCmdType START_LIVE = 0x03e8 CLOSE_LIVE = 0x03e9 VIDEO_RECORD_SWITCH = 0x03ea - LIGHT_STATE_SWITCH = 0x03ab + LIGHT_STATE_SWITCH = 0x03eb LIGHT_STATE_GET = 0x03ec LIVE_MODE_SET = 0x03ed LIVE_MODE_GET = 0x03ee From 77e5f321632315209cd84663934b9706859a33ec Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 01:09:05 +0200 Subject: [PATCH 025/405] + Implement websocket endpoint for device control --- web/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/__init__.py b/web/__init__.py index 2a70d5ce..23a5636e 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -237,6 +237,19 @@ def video(sock): sock.send(data) +@sock.route("/ws/ctrl") +def ctrl(sock): + + while True: + msg = json.loads(sock.receive()) + + if "light" in msg: + app.videoq.send_command( + P2PSubCmdType.LIGHT_STATE_SWITCH, + data={"open": int(msg["light"])} + ) + + @app.get("/video") def video2(): From eb28b3bd4fdeeb4ec4b3599054085dc25951e651 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 01:09:19 +0200 Subject: [PATCH 026/405] + Add buttons for light on/off in web ui --- static/ankersrv.js | 13 +++++++++++++ static/index.html | 14 +++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/static/ankersrv.js b/static/ankersrv.js index 080cf3dd..2e31497f 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -34,6 +34,19 @@ $(function () { }); + var wsctrl = new WebSocket("ws://" + location.host + "/ws/ctrl"); + + $('#light-on').on('click', function() { + wsctrl.send(JSON.stringify({"light": true})); + return false; + }); + + $('#light-off').on('click', function() { + wsctrl.send(JSON.stringify({"light": false})); + return false; + }); + + $('#configData').on('click',function(){ navigator.clipboard.writeText("{{ configHost }}:{{ configPort }}"); return false; diff --git a/static/index.html b/static/index.html index 31fba223..32ce9776 100644 --- a/static/index.html +++ b/static/index.html @@ -7,15 +7,23 @@ -
- -

ankerctl

Congratulations on running ankerctl

+
+
+
+ +
+
+ + +
+
+
From 856b8aea63b023fe4a89f647fe17a133d850983d Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 22:51:47 +0200 Subject: [PATCH 027/405] - Remove unused import --- cli/pppp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/pppp.py b/cli/pppp.py index 96b09109..69fd1cd9 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -1,6 +1,5 @@ import time import uuid -import click import logging as log from datetime import datetime, timedelta From f9668a79633c670690231cd38ac378c856f2d8a0 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 22:53:35 +0200 Subject: [PATCH 028/405] * Fixed inconsistency in using "logging" module. Now always "import logging as log". --- ankerctl.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index 71085537..4f0d59a6 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -2,8 +2,8 @@ import json import click -import logging import platform +import logging as log from os import path from rich import print # you need python3 from tqdm import tqdm @@ -62,19 +62,17 @@ def main(ctx, verbose, quiet, insecure): env = ctx.obj levels = { - -3: logging.CRITICAL, - -2: logging.ERROR, - -1: logging.WARNING, - 0: logging.INFO, - 1: logging.DEBUG, + -3: log.CRITICAL, + -2: log.ERROR, + -1: log.WARNING, + 0: log.INFO, + 1: log.DEBUG, } env.config = cli.config.configmgr() env.insecure = insecure env.level = max(-3, min(verbose - quiet, 1)) - env.log = cli.logfmt.setup_logging(levels[env.level]) - global log - log = env.log + cli.logfmt.setup_logging(levels[env.level]) if insecure: import urllib3 From bbc15d0faec0daf1dcc117b7238dfda662c49c2d Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 22:54:12 +0200 Subject: [PATCH 029/405] + Implemented PacketWriter class for generating logfiles of packet data --- libflagship/pktdump.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 libflagship/pktdump.py diff --git a/libflagship/pktdump.py b/libflagship/pktdump.py new file mode 100644 index 00000000..64d8d865 --- /dev/null +++ b/libflagship/pktdump.py @@ -0,0 +1,27 @@ +from datetime import datetime +from .util import enhex + + +class PacketWriter: + + def __init__(self, fd): + self.fd = fd + + @staticmethod + def timestamp(): + return datetime.now().isoformat() + + @classmethod + def open(cls, filename, append=True): + fd = open(filename, "a" if append else "w") + fd.write(f"# ========== {cls.timestamp()} Logging starts ==========\n") + return cls(fd) + + def write(self, data, addr, type="-"): + self.fd.write(f"{self.timestamp()} {type} {addr[0]}:{addr[1]} {enhex(data)}\n") + + def rx(self, data, addr): + self.write(data, addr, type="RX") + + def tx(self, data, addr): + self.write(data, addr, type="TX") From 76d2c0cca74cd99a39561dceee3edb850fb7fb1c Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 22:55:35 +0200 Subject: [PATCH 030/405] + Added a packet dumping interface to AnkerPPPPApi --- libflagship/ppppapi.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 70965bba..c5526d2a 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -218,6 +218,7 @@ def __init__(self, sock, duid, addr=None): self.running = True self.stopped = Event() + self.dumper = None @classmethod def open(cls, duid, host, port): @@ -239,6 +240,9 @@ def open_broadcast(cls): addr = ("255.255.255.255", PPPP_LAN_PORT) return cls(sock, duid=None, addr=addr) + def set_dumper(self, dumper): + self.dumper = dumper + def stop(self): self.running = False self.stopped.wait() @@ -320,12 +324,16 @@ def process(self, msg): def recv(self, timeout=None): self.sock.settimeout(timeout) data, self.addr = self.sock.recvfrom(4096) + if self.dumper: + self.dumper.rx(data, self.addr) msg = Message.parse(data)[0] log.debug(f"RX <-- {msg}") return msg def send(self, pkt, addr=None): resp = pkt.pack() + if self.dumper: + self.dumper.tx(resp, self.addr) msg = Message.parse(resp)[0] log.debug(f"TX --> {msg}") self.sock.sendto(resp, addr or self.addr) From 16084048ed9d3738413dcb101d6d7eb3b7237672 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 22:56:43 +0200 Subject: [PATCH 031/405] + Added top-level --pppp-dump option, for capturing pppp packet data to log file (for debugging) --- ankerctl.py | 12 +++++++----- cli/pppp.py | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index 4f0d59a6..9cea07da 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -23,7 +23,7 @@ from libflagship.util import enhex from libflagship.mqtt import MqttMsgType from libflagship.pppp import PktLanSearch, P2PCmdType, P2PSubCmdType, FileTransfer -from libflagship.ppppapi import AnkerPPPPApi, FileUploadInfo, PPPPError +from libflagship.ppppapi import FileUploadInfo import web @@ -53,11 +53,12 @@ def upgrade_config_if_needed(self): @click.group(context_settings=dict(help_option_names=["-h", "--help"])) +@click.option("--pppp-dump", required=False, metavar="", type=click.Path(), help="Enable logging of PPPP data to ") @click.option("--insecure", "-k", is_flag=True, help="Disable TLS certificate validation") @click.option("--verbose", "-v", count=True, help="Increase verbosity") @click.option("--quiet", "-q", count=True, help="Decrease verbosity") @click.pass_context -def main(ctx, verbose, quiet, insecure): +def main(ctx, pppp_dump, verbose, quiet, insecure): ctx.ensure_object(Environment) env = ctx.obj @@ -71,6 +72,7 @@ def main(ctx, verbose, quiet, insecure): env.config = cli.config.configmgr() env.insecure = insecure env.level = max(-3, min(verbose - quiet, 1)) + env.pppp_dump = pppp_dump cli.logfmt.setup_logging(levels[env.level]) @@ -208,7 +210,7 @@ def pppp_lan_search(env): Works by broadcasting a LAN_SEARCH packet, and waiting for a reply. """ - api = AnkerPPPPApi.open_broadcast() + api = cli.pppp.pppp_open_broadcast(env.pppp_dump) try: api.send(PktLanSearch()) resp = api.recv(timeout=1.0) @@ -232,7 +234,7 @@ def pppp_print_file(env, file, no_act): file, so anytime a file is uploaded, the old one is deleted. """ env.require_config() - api = cli.pppp.pppp_open(env.config) + api = cli.pppp.pppp_open(env.config, env.pppp_dump) data = file.read() fui = FileUploadInfo.from_file(file.name, user_name="ankerctl", user_id="-", machine_id="-") @@ -265,7 +267,7 @@ def pppp_capture_video(env, file, max_size): "ffplay" from the ffmpeg program suite. """ env.require_config() - api = cli.pppp.pppp_open(env.config) + api = cli.pppp.pppp_open(env.config, env.pppp_dump) cmd = {"commandType": P2PSubCmdType.START_LIVE, "data": {"encryptkey": "x", "accountId": "y"}} api.send_xzyh(json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD) diff --git a/cli/pppp.py b/cli/pppp.py index 69fd1cd9..44679a30 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -7,11 +7,19 @@ import cli.util +from libflagship.pktdump import PacketWriter from libflagship.pppp import PktLanSearch, Duid, P2PCmdType from libflagship.ppppapi import AnkerPPPPApi, FileTransfer -def pppp_open(config, timeout=None): +def _pppp_dumpfile(api, dumpfile): + if dumpfile: + log.info(f"Logging all pppp traffic to {dumpfile!r}") + pktwr = PacketWriter.open(dumpfile) + api.set_dumper(pktwr) + + +def pppp_open(config, timeout=None, dumpfile=None): if timeout: deadline = datetime.now() + timedelta(seconds=timeout) @@ -19,6 +27,8 @@ def pppp_open(config, timeout=None): printer = cfg.printers[0] api = AnkerPPPPApi.open_lan(Duid.from_string(printer.p2p_duid), host=printer.ip_addr) + _pppp_dumpfile(api, dumpfile) + log.info("Trying connect over pppp") api.start() @@ -33,6 +43,12 @@ def pppp_open(config, timeout=None): return api +def pppp_open_broadcast(dumpfile=None): + api = AnkerPPPPApi.open_broadcast() + _pppp_dumpfile(api, dumpfile) + return api + + def pppp_send_file(api, fui, data): log.info("Requesting file transfer..") api.send_xzyh(str(uuid.uuid4())[:16].encode(), cmd=P2PCmdType.P2P_SEND_FILE) From 0d9a95172fb27f1d2723166a615a7d2ad2afe464 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 8 Apr 2023 23:09:38 +0200 Subject: [PATCH 032/405] + Enable --pppp-dump argument for webserver --- ankerctl.py | 2 +- web/__init__.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index 9cea07da..f00db9b0 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -461,7 +461,7 @@ def webserver(env): @pass_env def webserver(env, host, port): env.require_config() - web.webserver(env.config, host, port) + web.webserver(env.config, host, port, pppp_dump=env.pppp_dump) if __name__ == "__main__": diff --git a/web/__init__.py b/web/__init__.py index 23a5636e..7a089dc4 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -190,7 +190,7 @@ def send_command(self, commandType, **kwargs): ) def worker_start(self): - self.api = cli.pppp.pppp_open(app.config["config"], timeout=1) + self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) self.send_command(P2PSubCmdType.START_LIVE, data={"encryptkey": "x", "accountId": "y"}) @@ -302,7 +302,7 @@ def app_api_files_local(): fd = request.files["file"] - api = cli.pppp.pppp_open(config) + api = cli.pppp.pppp_open(config, dumpfile=app.config.get("pppp_dump")) data = fd.read() fui = FileUploadInfo.from_data(data, fd.filename, user_name=user_name, user_id="-", machine_id="-") @@ -321,8 +321,9 @@ def app_api_files_local(): return {} -def webserver(config, host, port): +def webserver(config, host, port, **kwargs): app.config["config"] = config app.config["port"] = port app.config["host"] = host + app.config.update(kwargs) app.run(host=host, port=port) From 08d4406187a0343b1bd735effffe4eac604e9743 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 13:20:42 +0200 Subject: [PATCH 033/405] + Implemented CyclicU16, a class dealing with wrapping u16 counter variables. --- libflagship/cyclic.py | 114 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 libflagship/cyclic.py diff --git a/libflagship/cyclic.py b/libflagship/cyclic.py new file mode 100644 index 00000000..503a7068 --- /dev/null +++ b/libflagship/cyclic.py @@ -0,0 +1,114 @@ +import unittest + + +class CyclicU16(int): + """Cyclic 16-bit unsigned numbers, with special handling of wraparound + + When dealing with 16-bit counters, special care must be taken when + overflowinging the number. For example, consider two increasing counter + variables, x and y. At a point in time, we have: + + x == 0xFFF2 + y == 0xFFF5 + + At this time `x < y` works, as expected. However, 12 steps later, we have: + + x == 0xFFFE + y == 0x0001 + + which will invert the result of `x < y`, even though the counters have + increased at the same rate. + + To handle this situation, we define a range (CyclicU16.wrap) in which the + numbers are assumed to have recently wrapped around, such that: + + x, y = 0xFFFE, 0x0001 + + (x < y) == False + + but: + + x, y = CyclicU16(0xFFFE), CyclicU16(0x0001) + + (x < y) == True + """ + + def __new__(cls, k): + return int.__new__(cls, k & 0xFFFF) + + def __init__(self, k, wrap=0x100): + self.wrap = wrap + + def __hash__(self): + return int(self) + + def __add__(self, k): + return type(self)(int(self) + int(k)) + + def __sub__(self, k): + return type(self)(int(self) - int(k)) + + def __eq__(self, k): + return int(self) == int(k) + + def __ne__(self, k): + return not self.__eq__(k) + + def __lt__(self, other): + return int(self - self.wrap) < int(other - self.wrap) + + def __gt__(self, other): + return int(self - self.wrap) > int(other - self.wrap) + + def __le__(self, other): + return not self.__gt__(other) + + def __ge__(self, other): + return not self.__lt__(other) + + +class TestCyclic(unittest.TestCase): + + def test_equal(self): + C = CyclicU16 + + self.assertEqual(C(0x42), 0x42) + self.assertEqual(C(0x10001), 0x1) + + def test_lt(self): + C = CyclicU16 + + self.assertFalse(C(0x1) < C(0x1)) + self.assertFalse(C(0xFFFF) < C(0xFFFF)) + self.assertTrue(C(0x1) < C(0x2)) + self.assertFalse(C(0x2) < C(0x1)) + self.assertTrue(C(0x101) < C(0x120)) + self.assertFalse(C(0x120) < C(0x101)) + self.assertTrue(C(0xFFFE) < C(0xFFFF)) + self.assertTrue(C(0xFFFE) < C(0x10)) + self.assertFalse(C(0xFFFE) < C(0x110)) + + def test_gt(self): + C = CyclicU16 + + self.assertFalse(C(0x1) > C(0x1)) + self.assertFalse(C(0xFFFF) > C(0xFFFF)) + self.assertTrue(C(0x2) > C(0x1)) + self.assertFalse(C(0x1) > C(0x2)) + self.assertTrue(C(0x120) > C(0x101)) + self.assertFalse(C(0x101) > C(0x120)) + self.assertTrue(C(0xFFFF) > C(0xFFFE)) + self.assertTrue(C(0x10) > C(0xFFFE)) + self.assertFalse(C(0x110) > C(0xFFFE)) + + def test_overflow(self): + C = CyclicU16 + + n = C(0xFFFE) + self.assertEqual(n, 0xFFFE) + n += 1 + self.assertEqual(n, 0xFFFF) + n += 1 + self.assertEqual(n, 0x0000) + n += 1 + self.assertEqual(n, 0x0001) From 1cb82e71ff5219cd35504638d126872bc6b706d8 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 13:52:20 +0200 Subject: [PATCH 034/405] * Convert .wrap to property --- libflagship/cyclic.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libflagship/cyclic.py b/libflagship/cyclic.py index 503a7068..50c83b2a 100644 --- a/libflagship/cyclic.py +++ b/libflagship/cyclic.py @@ -37,7 +37,11 @@ def __new__(cls, k): return int.__new__(cls, k & 0xFFFF) def __init__(self, k, wrap=0x100): - self.wrap = wrap + self._wrap = wrap + + @property + def wrap(self): + return self._wrap def __hash__(self): return int(self) From 2b5f1acc892ce9de936eda7ee001ecd1b4a0e6a9 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 13:53:12 +0200 Subject: [PATCH 035/405] + Introduce CyclicU16.trunc() helper, to improve compatibility with regular int objects --- libflagship/cyclic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libflagship/cyclic.py b/libflagship/cyclic.py index 50c83b2a..d6382dbd 100644 --- a/libflagship/cyclic.py +++ b/libflagship/cyclic.py @@ -34,7 +34,7 @@ class CyclicU16(int): """ def __new__(cls, k): - return int.__new__(cls, k & 0xFFFF) + return int.__new__(cls, cls.trunc(k)) def __init__(self, k, wrap=0x100): self._wrap = wrap @@ -43,6 +43,10 @@ def __init__(self, k, wrap=0x100): def wrap(self): return self._wrap + @staticmethod + def trunc(n): + return int(n) & 0xFFFF + def __hash__(self): return int(self) @@ -53,16 +57,16 @@ def __sub__(self, k): return type(self)(int(self) - int(k)) def __eq__(self, k): - return int(self) == int(k) + return int(self) == self.trunc(k) def __ne__(self, k): return not self.__eq__(k) def __lt__(self, other): - return int(self - self.wrap) < int(other - self.wrap) + return self.trunc(self - self.wrap) < self.trunc(other - self.wrap) def __gt__(self, other): - return int(self - self.wrap) > int(other - self.wrap) + return self.trunc(self - self.wrap) > self.trunc(other - self.wrap) def __le__(self, other): return not self.__gt__(other) From e3deff67c3202a2d74ac5329e75364d6c49408ca Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 13:57:35 +0200 Subject: [PATCH 036/405] * Implement cyclic counters using CyclicU16. This should solve all overflow-related issues with counters. --- libflagship/ppppapi.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index c5526d2a..e0166af4 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -10,6 +10,7 @@ from socket import AF_INET from dataclasses import dataclass +from libflagship.cyclic import CyclicU16 from libflagship.pppp import * PPPP_LAN_PORT = 32108 @@ -104,9 +105,9 @@ def __init__(self, index, max_in_flight=64): self.rxqueue = {} self.txqueue = [] self.backlog = [] - self.rx_ctr = 0 - self.tx_ctr = 0 - self.tx_ack = 0 + self.rx_ctr = CyclicU16(0) + self.tx_ctr = CyclicU16(0) + self.tx_ack = CyclicU16(0) self.rx = Wire() self.tx = Wire() self.timeout = timedelta(seconds=0.5) @@ -141,7 +142,7 @@ def rx_drw(self, index, data): # recombine data from queue while self.rx_ctr in self.rxqueue: del self.rxqueue[self.rx_ctr] - self.rx_ctr = (self.rx_ctr + 1) & 0xFFFF + self.rx_ctr += 1 self.rx.write(data) def poll(self): @@ -189,7 +190,7 @@ def write(self, payload, block=True): # schedule transmission in 1kb chunks data, pdata = pdata[:1024], pdata[1024:] self.backlog.append((deadline, self.tx_ctr, data)) - self.tx_ctr = (self.tx_ctr + 1) & 0xFFFF + self.tx_ctr += 1 tx_ctr_done = self.tx_ctr From 8cefcf4d23c0eb2df979a1eb2c49197806ad6b47 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 13:58:14 +0200 Subject: [PATCH 037/405] * Make Channel packet warning configurable (max_age_warn argument), and increase default to 128. --- libflagship/ppppapi.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index e0166af4..8ac7c348 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -100,7 +100,7 @@ def write(self, data): class Channel: - def __init__(self, index, max_in_flight=64): + def __init__(self, index, max_in_flight=64, max_age_warn=128): self.index = index self.rxqueue = {} self.txqueue = [] @@ -114,6 +114,7 @@ def __init__(self, index, max_in_flight=64): self.acks = set() self.event = Event() self.max_in_flight = max_in_flight + self.max_age_warn = max_age_warn def rx_ack(self, acks): # remove all ACKed packets from transmission queue @@ -132,7 +133,7 @@ def rx_ack(self, acks): def rx_drw(self, index, data): # drop any packets we have already recieved if self.rx_ctr > index: - if self.rx_ctr - index > 100: + if self.max_age_warn and (self.rx_ctr - index > self.max_age_warn): log.warn(f"Dropping old packet: index {index} while expecting {self.rx_ctr}.") return From cbbf360bcd9f3a2e4e81ca6dc1262e1a27b113b9 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 18:06:50 +0200 Subject: [PATCH 038/405] + Add locking for recv_xzyh() --- libflagship/ppppapi.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 8ac7c348..38176de9 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -6,7 +6,7 @@ from multiprocessing import Pipe from datetime import datetime, timedelta -from threading import Thread, Event +from threading import Thread, Event, Lock from socket import AF_INET from dataclasses import dataclass @@ -115,6 +115,7 @@ def __init__(self, index, max_in_flight=64, max_age_warn=128): self.event = Event() self.max_in_flight = max_in_flight self.max_age_warn = max_age_warn + self.lock = Lock() def rx_ack(self, acks): # remove all ACKed packets from transmission queue @@ -368,18 +369,19 @@ def send_aabb(self, data, sn=0, pos=0, frametype=0, chan=1, block=True): def recv_xzyh(self, chan=1, timeout=None): fd = self.chans[chan] - hdr = fd.peek(16, timeout=timeout) - if not hdr: - return None + with fd.lock: + hdr = fd.peek(16, timeout=timeout) + if not hdr: + return None - xzyh = Xzyh.parse(hdr)[0] + xzyh = Xzyh.parse(hdr)[0] - data = fd.read(xzyh.len + 16, timeout=timeout) - if not data: - return None + data = fd.read(xzyh.len + 16, timeout=timeout) + if not data: + return None - xzyh.data = data[16:] - return xzyh + xzyh.data = data[16:] + return xzyh def recv_aabb(self, chan=1): fd = self.chans[chan] From 1f22795590228d35bb87a77248c0f9664702904f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 10 Apr 2023 22:27:10 +0200 Subject: [PATCH 039/405] * Fixed incorrect argument order to cli.pppp.pppp_open*() --- ankerctl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index f00db9b0..f86b1d21 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -210,7 +210,7 @@ def pppp_lan_search(env): Works by broadcasting a LAN_SEARCH packet, and waiting for a reply. """ - api = cli.pppp.pppp_open_broadcast(env.pppp_dump) + api = cli.pppp.pppp_open_broadcast(dumpfile=env.pppp_dump) try: api.send(PktLanSearch()) resp = api.recv(timeout=1.0) @@ -234,7 +234,7 @@ def pppp_print_file(env, file, no_act): file, so anytime a file is uploaded, the old one is deleted. """ env.require_config() - api = cli.pppp.pppp_open(env.config, env.pppp_dump) + api = cli.pppp.pppp_open(env.config, dumpfile=env.pppp_dump) data = file.read() fui = FileUploadInfo.from_file(file.name, user_name="ankerctl", user_id="-", machine_id="-") @@ -267,7 +267,7 @@ def pppp_capture_video(env, file, max_size): "ffplay" from the ffmpeg program suite. """ env.require_config() - api = cli.pppp.pppp_open(env.config, env.pppp_dump) + api = cli.pppp.pppp_open(env.config, dumpfile=env.pppp_dump) cmd = {"commandType": P2PSubCmdType.START_LIVE, "data": {"encryptkey": "x", "accountId": "y"}} api.send_xzyh(json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD) From 8868cced98c2a9d9f5e7752d7913ae552f0ec31c Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:44:09 +0200 Subject: [PATCH 040/405] * Reworked stf enum implementation to support custom storage types (instead of only u8) --- libflagship/pppp.py | 45 ++++++++++++++++++++---------------- templates/python/pppp.py.tpl | 9 ++++---- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index 677906af..9a934a0a 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -88,22 +88,24 @@ class Type(enum.IntEnum): INVALID = 0xff # unknown @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) class P2PCmdType(enum.IntEnum): P2P_JSON_CMD = 0x06a4 # unknown P2P_SEND_FILE = 0x3a98 # unknown @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) class P2PSubCmdType(enum.IntEnum): START_LIVE = 0x03e8 # unknown @@ -115,11 +117,12 @@ class P2PSubCmdType(enum.IntEnum): LIVE_MODE_GET = 0x03ee # unknown @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) class FileTransfer(enum.IntEnum): BEGIN = 0x00 # Begin file transfer (sent with metadata) @@ -129,11 +132,12 @@ class FileTransfer(enum.IntEnum): REPLY = 0x80 # Reply from printer @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) class FileTransferReply(enum.IntEnum): OK = 0x00 # Success @@ -143,11 +147,12 @@ class FileTransferReply(enum.IntEnum): ERR_BUSY = 0xff # Printer was not ready to receive @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) @dataclass diff --git a/templates/python/pppp.py.tpl b/templates/python/pppp.py.tpl index 52f2c8e3..6f5924d7 100644 --- a/templates/python/pppp.py.tpl +++ b/templates/python/pppp.py.tpl @@ -65,11 +65,12 @@ class ${enum.name}(enum.IntEnum): %endfor @classmethod - def parse(cls, p): - return cls(struct.unpack("B", p[:1])[0]), p[1:] + def parse(cls, p, typ=u8): + d = typ.parse(p) + return cls(d[0]), d[1] - def pack(self): - return struct.pack("B", self) + def pack(self, typ=u8): + return typ.pack(self) %endif %endfor From ff463947ec48a3ad061c6a039671b2a29073e5d8 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:44:47 +0200 Subject: [PATCH 041/405] + Added missing fields for P2PCmdType --- libflagship/pppp.py | 103 ++++++++++++++++++++++++++++++++++++++++- specification/pppp.stf | 100 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 2 deletions(-) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index 9a934a0a..a788cdf7 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -96,8 +96,107 @@ def pack(self, typ=u8): return typ.pack(self) class P2PCmdType(enum.IntEnum): - P2P_JSON_CMD = 0x06a4 # unknown - P2P_SEND_FILE = 0x3a98 # unknown + APP_CMD_START_REC_BROADCASE = 0x0384 # unknown + APP_CMD_STOP_REC_BROADCASE = 0x0385 # unknown + APP_CMD_BIND_BROADCAST = 0x03e8 # unknown + APP_CMD_BIND_SYNC_ACCOUNT_INFO = 0x03e9 # unknown + APP_CMD_UNBIND_ACCOUNT = 0x03ea # unknown + APP_CMD_START_REALTIME_MEDIA = 0x03eb # unknown + APP_CMD_STOP_REALTIME_MEDIA = 0x03ec # unknown + APP_CMD_START_TALKBACK = 0x03ed # unknown + APP_CMD_STOP_TALKBACK = 0x03ee # unknown + APP_CMD_START_VOICECALL = 0x03ef # unknown + APP_CMD_STOP_VOICECALL = 0x03f0 # unknown + APP_CMD_START_RECORD = 0x03f1 # unknown + APP_CMD_STOP_RECORD = 0x03f2 # unknown + APP_CMD_PIR_SWITCH = 0x03f3 # unknown + APP_CMD_CLOSE_PIR = 0x03f4 # unknown + APP_CMD_IRCUT_SWITCH = 0x03f5 # unknown + APP_CMD_CLOSE_IRCUT = 0x03f6 # unknown + APP_CMD_EAS_SWITCH = 0x03f7 # unknown + APP_CMD_CLOSE_EAS = 0x03f8 # unknown + APP_CMD_AUDDEC_SWITCH = 0x03f9 # unknown + APP_CMD_CLOSE_AUDDEC = 0x03fa # unknown + APP_CMD_DEVS_LOCK_SWITCH = 0x03fb # unknown + APP_CMD_DEVS_UNLOCK = 0x03fc # unknown + APP_CMD_RECORD_IMG = 0x03fd # unknown + APP_CMD_RECORD_IMG_STOP = 0x03fe # unknown + APP_CMD_STOP_SHARE = 0x03ff # unknown + APP_CMD_DOWNLOAD_VIDEO = 0x0400 # unknown + APP_CMD_RECORD_VIEW = 0x0401 # unknown + APP_CMD_RECORD_PLAY_CTRL = 0x0402 # unknown + APP_CMD_DELLETE_RECORD = 0x0403 # unknown + APP_CMD_SNAPSHOT = 0x0404 # unknown + APP_CMD_FORMAT_SD = 0x0405 # unknown + APP_CMD_CHANGE_PWD = 0x0406 # unknown + APP_CMD_CHANGE_WIFI_PWD = 0x0407 # unknown + APP_CMD_WIFI_CONFIG = 0x0408 # unknown + APP_CMD_TIME_SYCN = 0x0409 # unknown + APP_CMD_HUB_REBOOT = 0x040a # unknown + APP_CMD_DEVS_SWITCH = 0x040b # unknown + APP_CMD_HUB_TO_FACTORY = 0x040c # unknown + APP_CMD_DEVS_TO_FACTORY = 0x040d # unknown + APP_CMD_DEVS_BIND_BROADCASE = 0x040e # unknown + APP_CMD_DEVS_BIND_NOTIFY = 0x040f # unknown + APP_CMD_DEVS_UNBIND = 0x0410 # unknown + APP_CMD_RECORDDATE_SEARCH = 0x0411 # unknown + APP_CMD_RECORDLIST_SEARCH = 0x0412 # unknown + APP_CMD_GET_UPGRADE_RESULT = 0x0413 # unknown + APP_CMD_P2P_DISCONNECT = 0x0414 # unknown + APP_CMD_DEV_LED_SWITCH = 0x0415 # unknown + APP_CMD_CLOSE_DEV_LED = 0x0416 # unknown + APP_CMD_COLLECT_RECORD = 0x0417 # unknown + APP_CMD_DECOLLECT_RECORD = 0x0418 # unknown + APP_CMD_BATCH_RECORD = 0x0419 # unknown + APP_CMD_STRESS_TEST_OPER = 0x041a # unknown + APP_CMD_DOWNLOAD_CANCEL = 0x041b # unknown + APP_CMD_BIND_SYNC_ACCOUNT_INFO_EX = 0x041e # unknown + APP_CMD_LIVEVIEW_LED_SWITCH = 0x0420 # unknown + APP_CMD_REPAIR_SD = 0x0421 # unknown + APP_CMD_GET_ASEKEY = 0x044c # unknown + APP_CMD_GET_BATTERY = 0x044d # unknown + APP_CMD_SDINFO = 0x044e # unknown + APP_CMD_CAMERA_INFO = 0x044f # unknown + APP_CMD_GET_RECORD_TIME = 0x0450 # unknown + APP_CMD_GET_MDETECT_PARAM = 0x0451 # unknown + APP_CMD_MDETECTINFO = 0x0452 # unknown + APP_CMD_GET_ARMING_INFO = 0x0453 # unknown + APP_CMD_GET_ARMING_STATUS = 0x0454 # unknown + APP_CMD_GET_AUDDEC_INFO = 0x0455 # unknown + APP_CMD_GET_AUDDEC_SENSITIVITY = 0x0456 # unknown + APP_CMD_GET_AUDDE_CSTATUS = 0x0457 # unknown + APP_CMD_GET_MIRRORMODE = 0x0458 # unknown + APP_CMD_GET_IRMODE = 0x0459 # unknown + APP_CMD_GET_IRCUTSENSITIVITY = 0x045a # unknown + APP_CMD_GET_PIRINFO = 0x045b # unknown + APP_CMD_GET_PIRCTRL = 0x045c # unknown + APP_CMD_GET_PIRSENSITIVITY = 0x045d # unknown + APP_CMD_GET_EAS_STATUS = 0x045e # unknown + APP_CMD_GET_CAMERA_LOCK = 0x045f # unknown + APP_CMD_GET_GATEWAY_LOCK = 0x0460 # unknown + APP_CMD_GET_UPDATE_STATUS = 0x0461 # unknown + APP_CMD_GET_ADMIN_PWD = 0x0462 # unknown + APP_CMD_GET_WIFI_PWD = 0x0463 # unknown + APP_CMD_GET_EXCEPTION_LOG = 0x0464 # unknown + APP_CMD_GET_NEWVESION = 0x0465 # unknown + APP_CMD_GET_HUB_TONE_INFO = 0x0466 # unknown + APP_CMD_GET_DEV_TONE_INFO = 0x0467 # unknown + APP_CMD_GET_HUB_NAME = 0x0468 # unknown + APP_CMD_GET_DEVS_NAME = 0x0469 # unknown + APP_CMD_GET_P2P_CONN_STATUS = 0x046a # unknown + APP_CMD_SET_DEV_STORAGE_TYPE = 0x04cc # unknown + APP_CMD_VIDEO_FRAME = 0x0514 # unknown + APP_CMD_AUDIO_FRAME = 0x0515 # unknown + APP_CMD_STREAM_MSG = 0x0516 # unknown + APP_CMD_CONVERT_MP4_OK = 0x0517 # unknown + APP_CMD_DOENLOAD_FINISH = 0x0518 # unknown + APP_CMD_SET_PAYLOAD = 0x0546 # unknown + APP_CMD_NOTIFY_PAYLOAD = 0x0547 # unknown + APP_CMD_MAKER_SET_PAYLOAD = 0x06a4 # unknown + APP_CMD_MAKER_NOTIFY_PAYLOAD = 0x06a5 # unknown + PC_CMD_FILE_RECV = 0x3a98 # unknown + P2P_JSON_CMD = 0x06a4 # unknown + P2P_SEND_FILE = 0x3a98 # unknown @classmethod def parse(cls, p, typ=u8): diff --git a/specification/pppp.stf b/specification/pppp.stf index 45fff7ec..cf80db08 100644 --- a/specification/pppp.stf +++ b/specification/pppp.stf @@ -74,6 +74,106 @@ enum Type INVALID = 0xFF enum P2PCmdType + APP_CMD_START_REC_BROADCASE = 0x384 + APP_CMD_STOP_REC_BROADCASE = 0x385 + APP_CMD_BIND_BROADCAST = 0x3e8 + APP_CMD_BIND_SYNC_ACCOUNT_INFO = 0x3e9 + APP_CMD_UNBIND_ACCOUNT = 0x3ea + APP_CMD_START_REALTIME_MEDIA = 0x3eb + APP_CMD_STOP_REALTIME_MEDIA = 0x3ec + APP_CMD_START_TALKBACK = 0x3ed + APP_CMD_STOP_TALKBACK = 0x3ee + APP_CMD_START_VOICECALL = 0x3ef + APP_CMD_STOP_VOICECALL = 0x3f0 + APP_CMD_START_RECORD = 0x3f1 + APP_CMD_STOP_RECORD = 0x3f2 + APP_CMD_PIR_SWITCH = 0x3f3 + APP_CMD_CLOSE_PIR = 0x3f4 + APP_CMD_IRCUT_SWITCH = 0x3f5 + APP_CMD_CLOSE_IRCUT = 0x3f6 + APP_CMD_EAS_SWITCH = 0x3f7 + APP_CMD_CLOSE_EAS = 0x3f8 + APP_CMD_AUDDEC_SWITCH = 0x3f9 + APP_CMD_CLOSE_AUDDEC = 0x3fa + APP_CMD_DEVS_LOCK_SWITCH = 0x3fb + APP_CMD_DEVS_UNLOCK = 0x3fc + APP_CMD_RECORD_IMG = 0x3fd + APP_CMD_RECORD_IMG_STOP = 0x3fe + APP_CMD_STOP_SHARE = 0x3ff + APP_CMD_DOWNLOAD_VIDEO = 0x400 + APP_CMD_RECORD_VIEW = 0x401 + APP_CMD_RECORD_PLAY_CTRL = 0x402 + APP_CMD_DELLETE_RECORD = 0x403 + APP_CMD_SNAPSHOT = 0x404 + APP_CMD_FORMAT_SD = 0x405 + APP_CMD_CHANGE_PWD = 0x406 + APP_CMD_CHANGE_WIFI_PWD = 0x407 + APP_CMD_WIFI_CONFIG = 0x408 + APP_CMD_TIME_SYCN = 0x409 + APP_CMD_HUB_REBOOT = 0x40a + APP_CMD_DEVS_SWITCH = 0x40b + APP_CMD_HUB_TO_FACTORY = 0x40c + APP_CMD_DEVS_TO_FACTORY = 0x40d + APP_CMD_DEVS_BIND_BROADCASE = 0x40e + APP_CMD_DEVS_BIND_NOTIFY = 0x40f + APP_CMD_DEVS_UNBIND = 0x410 + APP_CMD_RECORDDATE_SEARCH = 0x411 + APP_CMD_RECORDLIST_SEARCH = 0x412 + APP_CMD_GET_UPGRADE_RESULT = 0x413 + APP_CMD_P2P_DISCONNECT = 0x414 + APP_CMD_DEV_LED_SWITCH = 0x415 + APP_CMD_CLOSE_DEV_LED = 0x416 + APP_CMD_COLLECT_RECORD = 0x417 + APP_CMD_DECOLLECT_RECORD = 0x418 + APP_CMD_BATCH_RECORD = 0x419 + APP_CMD_STRESS_TEST_OPER = 0x41a + APP_CMD_DOWNLOAD_CANCEL = 0x41b + APP_CMD_BIND_SYNC_ACCOUNT_INFO_EX = 0x41e + APP_CMD_LIVEVIEW_LED_SWITCH = 0x420 + APP_CMD_REPAIR_SD = 0x421 + APP_CMD_GET_ASEKEY = 0x44c + APP_CMD_GET_BATTERY = 0x44d + APP_CMD_SDINFO = 0x44e + APP_CMD_CAMERA_INFO = 0x44f + APP_CMD_GET_RECORD_TIME = 0x450 + APP_CMD_GET_MDETECT_PARAM = 0x451 + APP_CMD_MDETECTINFO = 0x452 + APP_CMD_GET_ARMING_INFO = 0x453 + APP_CMD_GET_ARMING_STATUS = 0x454 + APP_CMD_GET_AUDDEC_INFO = 0x455 + APP_CMD_GET_AUDDEC_SENSITIVITY = 0x456 + APP_CMD_GET_AUDDE_CSTATUS = 0x457 + APP_CMD_GET_MIRRORMODE = 0x458 + APP_CMD_GET_IRMODE = 0x459 + APP_CMD_GET_IRCUTSENSITIVITY = 0x45a + APP_CMD_GET_PIRINFO = 0x45b + APP_CMD_GET_PIRCTRL = 0x45c + APP_CMD_GET_PIRSENSITIVITY = 0x45d + APP_CMD_GET_EAS_STATUS = 0x45e + APP_CMD_GET_CAMERA_LOCK = 0x45f + APP_CMD_GET_GATEWAY_LOCK = 0x460 + APP_CMD_GET_UPDATE_STATUS = 0x461 + APP_CMD_GET_ADMIN_PWD = 0x462 + APP_CMD_GET_WIFI_PWD = 0x463 + APP_CMD_GET_EXCEPTION_LOG = 0x464 + APP_CMD_GET_NEWVESION = 0x465 + APP_CMD_GET_HUB_TONE_INFO = 0x466 + APP_CMD_GET_DEV_TONE_INFO = 0x467 + APP_CMD_GET_HUB_NAME = 0x468 + APP_CMD_GET_DEVS_NAME = 0x469 + APP_CMD_GET_P2P_CONN_STATUS = 0x46a + APP_CMD_SET_DEV_STORAGE_TYPE = 0x4cc + APP_CMD_VIDEO_FRAME = 0x514 + APP_CMD_AUDIO_FRAME = 0x515 + APP_CMD_STREAM_MSG = 0x516 + APP_CMD_CONVERT_MP4_OK = 0x517 + APP_CMD_DOENLOAD_FINISH = 0x518 + APP_CMD_SET_PAYLOAD = 0x546 + APP_CMD_NOTIFY_PAYLOAD = 0x547 + APP_CMD_MAKER_SET_PAYLOAD = 0x6a4 + APP_CMD_MAKER_NOTIFY_PAYLOAD = 0x6a5 + PC_CMD_FILE_RECV = 0x3a98 + P2P_JSON_CMD = 0x6a4 P2P_SEND_FILE = 0x3a98 From ad8cc50d927164ed3a1d321935ad074b008434d8 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:46:22 +0200 Subject: [PATCH 042/405] * Change Xzyh.cmd from u16le to P2PCmdType, which reveals the actual structure of these packets. --- libflagship/pppp.py | 6 +++--- specification/pppp.stf | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index a788cdf7..91a2deab 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -370,7 +370,7 @@ def pack(self): @dataclass class Xzyh(_Xzyh): magic : bytes = field(repr=False, kw_only=True, default=b'XZYH') # unknown - cmd : u16le # Command field (P2PCmdType) + cmd : P2PCmdType # Command field (P2PCmdType) len : u32le # Payload length unk0 : u8 # unknown unk1 : u8 # unknown @@ -384,7 +384,7 @@ class Xzyh(_Xzyh): def parse(cls, p): # not encrypted magic, p = Magic.parse(p, 4, b'XZYH') - cmd, p = u16le.parse(p) + cmd, p = P2PCmdType.parse(p, u16le) len, p = u32le.parse(p) unk0, p = u8.parse(p) unk1, p = u8.parse(p) @@ -398,7 +398,7 @@ def parse(cls, p): def pack(self): p = Magic.pack(self.magic, 4, b'XZYH') - p += u16le.pack(self.cmd) + p += P2PCmdType.pack(self.cmd, u16le) p += u32le.pack(self.len) p += u8.pack(self.unk0) p += u8.pack(self.unk1) diff --git a/specification/pppp.stf b/specification/pppp.stf index cf80db08..ef7e89c3 100644 --- a/specification/pppp.stf +++ b/specification/pppp.stf @@ -251,7 +251,7 @@ struct Xzyh magic: magic<4, 0x585a5948> # Command field (P2PCmdType) - cmd: u16le + cmd: P2PCmdType # Payload length len: u32le From bf62882a52270479854b08c9cb4055bf24498ddb Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:47:23 +0200 Subject: [PATCH 043/405] * Fixed deadlock when shutting down unused server threads: Be sure to call self._event.set() in atexit() to trigger clean shutdown. --- web/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/web/__init__.py b/web/__init__.py index 7a089dc4..549b472e 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -53,6 +53,7 @@ def __init__(self, idle_timeout=10): def atexit(self): log.info(f"{self.name}: Requesting thread exit..") self.running = False + self._event.set() self.join() log.info(f"{self.name}: Thread cleanup done") From e5649856c3811acaae316895970e0a9a315d1808 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:48:26 +0200 Subject: [PATCH 044/405] * Simplify Flask app, by using root_path=".", and using relative paths for static_folder, template_folder --- web/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 549b472e..011f41d0 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -20,8 +20,9 @@ app = Flask( __name__, - static_folder="../static", - template_folder="../static" + root_path=".", + static_folder="static", + template_folder="static" ) app.config.from_prefixed_env() From aab40ea203f3c7742110fd52d5d3e7d106fa555e Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sat, 15 Apr 2023 16:57:42 +0200 Subject: [PATCH 045/405] * Moved MultiQueue class into web/multiqueue.py --- web/__init__.py | 142 +--------------------------------------------- web/multiqueue.py | 141 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 140 deletions(-) create mode 100644 web/multiqueue.py diff --git a/web/__init__.py b/web/__init__.py index 011f41d0..eac3389c 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,20 +1,15 @@ import json -import atexit import logging as log -import contextlib -from datetime import datetime, timedelta -from enum import Enum from flask import Flask, request, render_template, Response from flask_sock import Sock -from threading import Thread, Event -from multiprocessing import Queue - from libflagship.util import enhex from libflagship.pppp import P2PSubCmdType, P2PCmdType from libflagship.ppppapi import FileUploadInfo, PPPPError, FileTransfer +from web.multiqueue import MultiQueue + import cli.mqtt @@ -29,139 +24,6 @@ sock = Sock(app) -class RunState(Enum): - Starting = 2 - Running = 3 - # Idle = 4 - Stopping = 5 - Stopped = 6 - - -class MultiQueue(Thread): - - def __init__(self, idle_timeout=10): - super().__init__() - self.timeout = timedelta(seconds=idle_timeout) - self.running = True - self.deadline = None - self.state = RunState.Stopped - self.wanted = False - self.targets = [] - self._event = Event() - atexit.register(self.atexit) - super().start() - - def atexit(self): - log.info(f"{self.name}: Requesting thread exit..") - self.running = False - self._event.set() - self.join() - log.info(f"{self.name}: Thread cleanup done") - - @property - def name(self): - return type(self).__name__ - - def start(self): - log.info(f"{self.name}: Requesting start") - self.wanted = True - self._event.set() - - def stop(self): - log.info(f"{self.name}: Requesting stop") - self.wanted = False - self._event.set() - - def run(self): - holdoff = None - - while self.running: - if self.state == RunState.Starting: - log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") - if datetime.now() > holdoff: - try: - log.info(f"{self.name} worker start") - self.worker_start() - except Exception as E: - log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.info(f"{self.name}: Worked started") - self.state = RunState.Running - else: - self._event.wait(timeout=0.1) - self._event.clear() - - elif self.state == RunState.Running: - if self.wanted: - self.worker_run(timeout=0.3) - else: - log.info(f"{self.name}: Stopping worker") - holdoff = datetime.now() - self.state = RunState.Stopping - - elif self.state == RunState.Stopping: - if datetime.now() > holdoff: - try: - self.worker_stop() - except Exception as E: - log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.info(f"{self.name}: Worked stopped") - self.state = RunState.Stopped - else: - self._event.wait(timeout=0.1) - self._event.clear() - - elif self.state == RunState.Stopped: - if self.wanted: - log.info(f"{self.name}: Starting worker") - holdoff = datetime.now() - self.state = RunState.Starting - else: - self._event.wait() - self._event.clear() - else: - raise ValueError("Unknown state value") - - log.info(f"{self.name}: Shutting down thread") - if self.state == RunState.Running: - self.worker_stop() - log.info(f"{self.name}: Thread exit") - - def put(self, obj): - for target in self.targets: - target.put(obj) - - def add_target(self, target): - if not self.targets: - self.start() - self.targets.append(target) - - def del_target(self, target): - if target in self.targets: - self.targets.remove(target) - if not self.targets: - self.stop() - - @contextlib.contextmanager - def tap(self): - queue = Queue() - self.add_target(queue) - yield queue - self.del_target(queue) - - def worker_start(self): - pass - - def worker_run(self, timeout): - pass - - def worker_stop(self): - pass - - class MqttQueue(MultiQueue): def worker_start(self): diff --git a/web/multiqueue.py b/web/multiqueue.py new file mode 100644 index 00000000..9c4d8ce0 --- /dev/null +++ b/web/multiqueue.py @@ -0,0 +1,141 @@ +import atexit +import contextlib +import logging as log + +from datetime import datetime, timedelta +from enum import Enum +from threading import Thread, Event +from multiprocessing import Queue + + +class RunState(Enum): + Starting = 2 + Running = 3 + # Idle = 4 + Stopping = 5 + Stopped = 6 + + +class MultiQueue(Thread): + + def __init__(self, idle_timeout=10): + super().__init__() + self.timeout = timedelta(seconds=idle_timeout) + self.running = True + self.deadline = None + self.state = RunState.Stopped + self.wanted = False + self.targets = [] + self._event = Event() + atexit.register(self.atexit) + super().start() + + def atexit(self): + log.info(f"{self.name}: Requesting thread exit..") + self.running = False + self._event.set() + self.join() + log.info(f"{self.name}: Thread cleanup done") + + @property + def name(self): + return type(self).__name__ + + def start(self): + log.info(f"{self.name}: Requesting start") + self.wanted = True + self._event.set() + + def stop(self): + log.info(f"{self.name}: Requesting stop") + self.wanted = False + self._event.set() + + def run(self): + holdoff = None + + while self.running: + if self.state == RunState.Starting: + log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") + if datetime.now() > holdoff: + try: + log.info(f"{self.name} worker start") + self.worker_start() + except Exception as E: + log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worked started") + self.state = RunState.Running + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Running: + if self.wanted: + self.worker_run(timeout=0.3) + else: + log.info(f"{self.name}: Stopping worker") + holdoff = datetime.now() + self.state = RunState.Stopping + + elif self.state == RunState.Stopping: + if datetime.now() > holdoff: + try: + self.worker_stop() + except Exception as E: + log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worked stopped") + self.state = RunState.Stopped + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Stopped: + if self.wanted: + log.info(f"{self.name}: Starting worker") + holdoff = datetime.now() + self.state = RunState.Starting + else: + self._event.wait() + self._event.clear() + else: + raise ValueError("Unknown state value") + + log.info(f"{self.name}: Shutting down thread") + if self.state == RunState.Running: + self.worker_stop() + log.info(f"{self.name}: Thread exit") + + def put(self, obj): + for target in self.targets: + target.put(obj) + + def add_target(self, target): + if not self.targets: + self.start() + self.targets.append(target) + + def del_target(self, target): + if target in self.targets: + self.targets.remove(target) + if not self.targets: + self.stop() + + @contextlib.contextmanager + def tap(self): + queue = Queue() + self.add_target(queue) + yield queue + self.del_target(queue) + + def worker_start(self): + pass + + def worker_run(self, timeout): + pass + + def worker_stop(self): + pass From 56231c7146d9019088c81a1f984dab7fb8dc748b Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 19 Apr 2023 21:24:50 +0200 Subject: [PATCH 046/405] * Fixd speling --- web/multiqueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/multiqueue.py b/web/multiqueue.py index 9c4d8ce0..63f07159 100644 --- a/web/multiqueue.py +++ b/web/multiqueue.py @@ -65,7 +65,7 @@ def run(self): log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") holdoff = datetime.now() + timedelta(seconds=1) else: - log.info(f"{self.name}: Worked started") + log.info(f"{self.name}: Worker started") self.state = RunState.Running else: self._event.wait(timeout=0.1) From 9e4d01050242da7f5b739587f11bcdfad199a5d7 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 19 Apr 2023 21:25:19 +0200 Subject: [PATCH 047/405] + Make MultiQueue handle exceptions during worker_run(). Log the exception, and shut down the worker, if this happens. --- web/multiqueue.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/multiqueue.py b/web/multiqueue.py index 63f07159..d83c89d9 100644 --- a/web/multiqueue.py +++ b/web/multiqueue.py @@ -73,7 +73,13 @@ def run(self): elif self.state == RunState.Running: if self.wanted: - self.worker_run(timeout=0.3) + try: + self.worker_run(timeout=0.3) + except Exception: + log.exception("Unexpected exception while running worker") + log.warning(f"{self.name}: Stopping worker due to exception") + holdoff = datetime.now() + self.state = RunState.Stopping else: log.info(f"{self.name}: Stopping worker") holdoff = datetime.now() From 364eec74b3cec0d00e03fb1897e7bcab01bfb803 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 19 Apr 2023 21:25:52 +0200 Subject: [PATCH 048/405] * Also break on OSError from queue.get() (can happen with low-level transport errors) --- web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/__init__.py b/web/__init__.py index eac3389c..ce704e1e 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -96,7 +96,7 @@ def video(sock): while True: try: data = queue.get() - except EOFError: + except (EOFError, OSError): break sock.send(data) From d1f3738b2db68b8fa4e296eee7f0d57742c6c1f9 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Wed, 19 Apr 2023 22:08:39 +0200 Subject: [PATCH 049/405] * FINALLY found the bug behind issue #40. When recombining DRW packets, we would accidentally append the same packet data multiple times. Now we actually use the rxqueue data. Fixes #40. --- libflagship/ppppapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 38176de9..000d9a6c 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -143,6 +143,7 @@ def rx_drw(self, index, data): # recombine data from queue while self.rx_ctr in self.rxqueue: + data = self.rxqueue[self.rx_ctr] del self.rxqueue[self.rx_ctr] self.rx_ctr += 1 self.rx.write(data) From 511a59aa30ee559009aa9f914366cc7dc4ab9b57 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Fri, 21 Apr 2023 21:34:56 +0200 Subject: [PATCH 050/405] Finally found an elusive bug related to video streaming over pppp. We used the `chrivers/webserver-debuglogs` branch to generate detailed logging for pppp events. After a while, this was observed: ``` [2023-04-21 20:52:27.144658] [c1:r2:t0:b0:rc11278:tc0:ta0] PKT 6f0d6ee5fe454ad93bfdd4a474c417fa... [2023-04-21 20:52:27.144710] [c1:r1:t0:b0:rc11279:tc0:ta0] rx_drw END index=11278 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.144967] [c1:r1:t0:b0:rc11279:tc0:ta0] rx_drw BEGIN index=11279 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.145001] [c1:r2:t0:b0:rc11279:tc0:ta0] PKT ab425b59796e83e551e22d0ce79671b7... [2023-04-21 20:52:27.145050] [c1:r1:t0:b0:rc11280:tc0:ta0] rx_drw END index=11279 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.145835] [c1:r1:t0:b0:rc11280:tc0:ta0] rx_drw BEGIN index=11280 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.145871] [c1:r2:t0:b0:rc11280:tc0:ta0] PKT a231fc9d9a40383f2eeac7a8f0e4a802... [2023-04-21 20:52:27.145920] [c1:r1:t0:b0:rc11281:tc0:ta0] rx_drw END index=11280 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.146311] [c1:r1:t0:b0:rc11281:tc0:ta0] rx_drw BEGIN index=11281 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.146347] [c1:r2:t0:b0:rc11281:tc0:ta0] PKT b799e3912d428128e4cef1c91249215e... [2023-04-21 20:52:27.146471] [c1:r1:t0:b0:rc11282:tc0:ta0] rx_drw END index=11281 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.146762] [c1:r1:t0:b0:rc11282:tc0:ta0] rx_drw BEGIN index=11282 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.146797] [c1:r2:t0:b0:rc11282:tc0:ta0] PKT 4304d82688f8eafe81cfda7d2037bebe... [2023-04-21 20:52:27.146850] [c1:r1:t0:b0:rc11283:tc0:ta0] rx_drw END index=11282 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.147225] [c1:r1:t0:b0:rc11283:tc0:ta0] rx_drw BEGIN index=11283 len(data)=1024 rxq=[248] [2023-04-21 20:52:27.147261] [c1:r2:t0:b0:rc11283:tc0:ta0] PKT ebc96eeb50386d59bdaea6509ff48596... ``` We're at index 11278..11283 here, but 248 gets stuck in the rx queue! Here's where the bad magic happened: ``` [2023-04-21 20:52:02.189170] [c1:r0:t0:b0:rc295:tc0:ta0] rx_drw END index=294 len(data)=1024 rxq=[] [2023-04-21 20:52:02.189396] [c1:r0:t0:b0:rc295:tc0:ta0] rx_drw BEGIN index=295 len(data)=1024 rxq=[] [2023-04-21 20:52:02.189425] [c1:r1:t0:b0:rc295:tc0:ta0] PKT 0d22b73f0439481284237cc96da77b1e... [2023-04-21 20:52:02.189468] [c1:r0:t0:b0:rc296:tc0:ta0] rx_drw END index=295 len(data)=1024 rxq=[] [2023-04-21 20:52:02.189898] [c1:r0:t0:b0:rc296:tc0:ta0] rx_drw BEGIN index=296 len(data)=1024 rxq=[] [2023-04-21 20:52:02.189928] [c1:r1:t0:b0:rc296:tc0:ta0] PKT 23b3d6d82cdb7d8c6cca487ed4b98363... [2023-04-21 20:52:02.189971] [c1:r0:t0:b0:rc297:tc0:ta0] rx_drw END index=296 len(data)=1024 rxq=[] [2023-04-21 20:52:02.191313] [c1:r0:t0:b0:rc297:tc0:ta0] rx_drw BEGIN index=297 len(data)=544 rxq=[] [2023-04-21 20:52:02.191340] [c1:r1:t0:b0:rc297:tc0:ta0] PKT c41d42c0a66c2097080e7e60d8edcaf3... [2023-04-21 20:52:02.191385] [c1:r0:t0:b0:rc298:tc0:ta0] rx_drw END index=297 len(data)=544 rxq=[] [2023-04-21 20:52:02.191509] [c1:r0:t0:b0:rc298:tc0:ta0] rx_drw BEGIN index=248 len(data)=1024 rxq=[] [2023-04-21 20:52:02.192435] [c1:r1:t0:b0:rc298:tc0:ta0] rx_drw END index=248 len(data)=1024 rxq=[248] [2023-04-21 20:52:02.228272] [c1:r1:t0:b0:rc298:tc0:ta0] rx_drw BEGIN index=298 len(data)=1024 rxq=[248] [2023-04-21 20:52:02.228307] [c1:r2:t0:b0:rc298:tc0:ta0] PKT 585a5948140547820000010000000000... [2023-04-21 20:52:02.228354] [c1:r1:t0:b0:rc299:tc0:ta0] rx_drw END index=298 len(data)=1024 rxq=[248] [2023-04-21 20:52:02.228582] [c1:r1:t0:b0:rc299:tc0:ta0] rx_drw BEGIN index=299 len(data)=1024 rxq=[248] [2023-04-21 20:52:02.228612] [c1:r2:t0:b0:rc299:tc0:ta0] PKT 30e8afeee1fb4e3f227bdd5a9c4318a8... [2023-04-21 20:52:02.228657] [c1:r1:t0:b0:rc300:tc0:ta0] rx_drw END index=299 len(data)=1024 rxq=[248] ``` It goes 294, 295, 296, 297, ... 248 ... 298, 299, 300, 301 248 is an old retransmission, but we mistakenly keep it around! The packet counter wraps around every 64K packets, so we would expect this packet to be re-merged after a complete cycle. And indeed it does: ``` [2023-04-21 20:54:49.066916] [c1:r1:t0:b0:rc245:tc0:ta0] rx_drw END index=244 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.067307] [c1:r1:t0:b0:rc245:tc0:ta0] rx_drw BEGIN index=245 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.067338] [c1:r2:t0:b0:rc245:tc0:ta0] PKT 067f953a5d89e98025bde523bf5c6b5e... [2023-04-21 20:54:49.067382] [c1:r1:t0:b0:rc246:tc0:ta0] rx_drw END index=245 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.067857] [c1:r1:t0:b0:rc246:tc0:ta0] rx_drw BEGIN index=246 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.067886] [c1:r2:t0:b0:rc246:tc0:ta0] PKT ca2b6e4038abfb51a0b6ab67455349b8... [2023-04-21 20:54:49.067929] [c1:r1:t0:b0:rc247:tc0:ta0] rx_drw END index=246 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.068133] [c1:r1:t0:b0:rc247:tc0:ta0] rx_drw BEGIN index=247 len(data)=1024 rxq=[248] [2023-04-21 20:54:49.068164] [c1:r2:t0:b0:rc247:tc0:ta0] PKT 202131603f823a5cdd8d7d535f2dcec8... [2023-04-21 20:54:49.068207] [c1:r1:t0:b0:rc248:tc0:ta0] PKT 585a5948140510c60000010000000000... [2023-04-21 20:54:49.068243] [c1:r0:t0:b0:rc249:tc0:ta0] rx_drw END index=247 len(data)=1024 rxq=[] [2023-04-21 20:54:49.068554] [c1:r0:t0:b0:rc249:tc0:ta0] rx_drw BEGIN index=248 len(data)=1024 rxq=[] [2023-04-21 20:54:49.069244] [c1:r0:t0:b0:rc249:tc0:ta0] rx_drw BEGIN index=249 len(data)=1024 rxq=[] [2023-04-21 20:54:49.069274] [c1:r1:t0:b0:rc249:tc0:ta0] PKT 9f1f8363327a69dae1e251ef459a91d0... [2023-04-21 20:54:49.069319] [c1:r0:t0:b0:rc250:tc0:ta0] rx_drw END index=249 len(data)=1024 rxq=[] [2023-04-21 20:54:49.069740] [c1:r0:t0:b0:rc250:tc0:ta0] rx_drw BEGIN index=250 len(data)=1024 rxq=[] ``` Notice how, at BEGIN index=247, there's 2 PKT lines? Yep, packet 248 was stuck in the rx queue for an entire cycle! So: ``` (if a packet gets stuck like this) AND ( (that packet is the first in a frame, containing an XZYH header) OR ((that packet is the last in a frame) AND (has a different size than the real packet)) ) ``` Then this wrong recombination will lead to a broken stream, instead of a minor visual glitch. So what causes this? These particular 16-bit wrapping counters are annoying to deal with, so a class was implemented to handle it; `libflagship.cyclic.CyclicU16`. CyclicU16 has a wraparound window, where it assumes that small numbers are actually big numbers. This it to handle this situation: `0xFFFD, 0xFFFE, 0xFFFF, 0x0000, 0x0001, 0x0002 ...` When comparing packet numbers in this way, we should realize that 0x0001 is bigger than 0xFFFF, even though that's not how numbers normally work. Let's try asking `CyclicU16` about this situation: ``` >>> c = libflagship.cyclic.CyclicU16(298) >>> c > 248 False ``` Well, yikes. That's maximally wrong. The default CyclicU16 window is `0x100` (256). Notice that: 248 < 256 but 298 > 256 So CyclicU16 wraps in both directions, which is wrong! None of the 24 existing unit tests for CyclicU16 caught this bug :-( So, add unit tests, and rework `__gt__` and `__lt__` for CyclicU16. Let's recap. To see this error, you need the following spectacle of conditions: 1) Shaky wifi, to trigger packet loss 2) The packet loss needs to occur before packet 256 in each cycle 3) The retransmission needs to occur after packet 256 in each cycle 4) The retransmitted packet has to avoid the packet loss, to be saved in the rxqueue 5) After wrapping the packet counter, the old packet will be merged into fresh data 6) This false merge needs to happen on either the first-of-frame or last-or-frame packet, with about 20 packets per frame. 7) If ALL this happens, we will see bug 40 trigger --- libflagship/cyclic.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libflagship/cyclic.py b/libflagship/cyclic.py index d6382dbd..64631888 100644 --- a/libflagship/cyclic.py +++ b/libflagship/cyclic.py @@ -63,10 +63,18 @@ def __ne__(self, k): return not self.__eq__(k) def __lt__(self, other): - return self.trunc(self - self.wrap) < self.trunc(other - self.wrap) + # if sign bit differs, take wrap into account + if (self ^ other) & 0x8000: + return self.trunc(self - self.wrap) < self.trunc(other - self.wrap) + else: + return int(self) < other def __gt__(self, other): - return self.trunc(self - self.wrap) > self.trunc(other - self.wrap) + # if sign bit differs, take wrap into account + if (self ^ other) & 0x8000: + return self.trunc(self - self.wrap) > self.trunc(other - self.wrap) + else: + return int(self) > other def __le__(self, other): return not self.__gt__(other) @@ -90,6 +98,8 @@ def test_lt(self): self.assertFalse(C(0xFFFF) < C(0xFFFF)) self.assertTrue(C(0x1) < C(0x2)) self.assertFalse(C(0x2) < C(0x1)) + self.assertTrue(C(0x90) < C(0x120)) + self.assertFalse(C(0x120) < C(0x90)) self.assertTrue(C(0x101) < C(0x120)) self.assertFalse(C(0x120) < C(0x101)) self.assertTrue(C(0xFFFE) < C(0xFFFF)) @@ -103,6 +113,8 @@ def test_gt(self): self.assertFalse(C(0xFFFF) > C(0xFFFF)) self.assertTrue(C(0x2) > C(0x1)) self.assertFalse(C(0x1) > C(0x2)) + self.assertTrue(C(0x120) > C(0x90)) + self.assertFalse(C(0x90) > C(0x120)) self.assertTrue(C(0x120) > C(0x101)) self.assertFalse(C(0x101) > C(0x120)) self.assertTrue(C(0xFFFF) > C(0xFFFE)) From 66ce20bf3a5dd8473eae27a56dda90c7771d50a6 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 10:22:55 +0200 Subject: [PATCH 051/405] + Added "clean" target to Makefile, for convenience --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 4b855721..7d9483b8 100644 --- a/Makefile +++ b/Makefile @@ -7,3 +7,6 @@ diff: install-tools: git submodule update --init pip install ./transwarp + +clean: + @find -name '*~' -o -name '__pycache__' -print0 | xargs -0 rm -rfv From d5bdad831ed05c76b30493fef92a59b081538025 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 17 Apr 2023 22:38:51 +0200 Subject: [PATCH 052/405] * Fixed file handle leak in cli.pppp.pppp_open(): if connection startup fails, stop api thread. --- cli/pppp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/pppp.py b/cli/pppp.py index 44679a30..68b7375f 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -37,6 +37,7 @@ def pppp_open(config, timeout=None, dumpfile=None): while not api.rdy: time.sleep(0.1) if api.stopped.is_set() or (timeout and (datetime.now() > deadline)): + api.stop() raise ConnectionRefusedError("Connection rejected by device") log.info("Established pppp connection") From 7d00e84e7b775ad0054981b2de4c8f99660af5f4 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 13:37:37 +0200 Subject: [PATCH 053/405] * Split MultiQueue into Service (base class) and MultiQueue (service with multiple consumers) --- web/multiqueue.py | 62 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/web/multiqueue.py b/web/multiqueue.py index d83c89d9..2d3d9863 100644 --- a/web/multiqueue.py +++ b/web/multiqueue.py @@ -16,7 +16,7 @@ class RunState(Enum): Stopped = 6 -class MultiQueue(Thread): +class Service(Thread): def __init__(self, idle_timeout=10): super().__init__() @@ -25,7 +25,6 @@ def __init__(self, idle_timeout=10): self.deadline = None self.state = RunState.Stopped self.wanted = False - self.targets = [] self._event = Event() atexit.register(self.atexit) super().start() @@ -115,6 +114,49 @@ def run(self): self.worker_stop() log.info(f"{self.name}: Thread exit") + def worker_start(self): + pass + + def worker_run(self, timeout): + pass + + def worker_stop(self): + pass + + +class QueueTap: + + def __init__(self, queue): + self.queue = queue + + def __iter__(self): + return self + + def __next__(self): + try: + return self.get() + except (EOFError, OSError): + raise StopIteration() + + def get(self, timeout=None): + return self.queue.get(timeout=timeout) + + +class MultiQueue(Service): + + def __init__(self, idle_timeout=10): + super().__init__(idle_timeout) + self.targets = [] + + @contextlib.contextmanager + def tap(self): + queue = Queue() + self.add_target(queue) + try: + yield QueueTap(queue) + finally: + self.del_target(queue) + def put(self, obj): for target in self.targets: target.put(obj) @@ -129,19 +171,3 @@ def del_target(self, target): self.targets.remove(target) if not self.targets: self.stop() - - @contextlib.contextmanager - def tap(self): - queue = Queue() - self.add_target(queue) - yield queue - self.del_target(queue) - - def worker_start(self): - pass - - def worker_run(self, timeout): - pass - - def worker_stop(self): - pass From 5bcb9c5aab9d29ae4cc4101680a63f0998b26d95 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 14:11:18 +0200 Subject: [PATCH 054/405] * Moved Service implementation to web/lib/service.py --- web/lib/service.py | 125 +++++++++++++++++++++++++++++++++++++++++++++ web/multiqueue.py | 121 +------------------------------------------ 2 files changed, 126 insertions(+), 120 deletions(-) create mode 100644 web/lib/service.py diff --git a/web/lib/service.py b/web/lib/service.py new file mode 100644 index 00000000..76eafb91 --- /dev/null +++ b/web/lib/service.py @@ -0,0 +1,125 @@ +import atexit +import logging as log +import contextlib + +from enum import Enum +from threading import Thread, Event +from datetime import datetime, timedelta +from multiprocessing import Queue + + +class RunState(Enum): + Starting = 2 + Running = 3 + # Idle = 4 + Stopping = 5 + Stopped = 6 + + +class Service(Thread): + + def __init__(self, idle_timeout=10): + super().__init__() + self.timeout = timedelta(seconds=idle_timeout) + self.running = True + self.deadline = None + self.state = RunState.Stopped + self.wanted = False + self._event = Event() + self.handlers = [] + atexit.register(self.atexit) + super().start() + + def atexit(self): + log.info(f"{self.name}: Requesting thread exit..") + self.running = False + self._event.set() + self.join() + log.info(f"{self.name}: Thread cleanup done") + + @property + def name(self): + return type(self).__name__ + + def start(self): + log.info(f"{self.name}: Requesting start") + self.wanted = True + self._event.set() + + def stop(self): + log.info(f"{self.name}: Requesting stop") + self.wanted = False + self._event.set() + + def run(self): + holdoff = None + + while self.running: + if self.state == RunState.Starting: + log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") + if datetime.now() > holdoff: + try: + log.info(f"{self.name} worker start") + self.worker_start() + except Exception as E: + log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worker started") + self.state = RunState.Running + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Running: + if self.wanted: + try: + self.worker_run(timeout=0.3) + except Exception: + log.exception("Unexpected exception while running worker") + log.warning(f"{self.name}: Stopping worker due to exception") + holdoff = datetime.now() + self.state = RunState.Stopping + else: + log.info(f"{self.name}: Stopping worker") + holdoff = datetime.now() + self.state = RunState.Stopping + + elif self.state == RunState.Stopping: + if datetime.now() > holdoff: + try: + self.worker_stop() + except Exception as E: + log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") + holdoff = datetime.now() + timedelta(seconds=1) + else: + log.info(f"{self.name}: Worked stopped") + self.state = RunState.Stopped + else: + self._event.wait(timeout=0.1) + self._event.clear() + + elif self.state == RunState.Stopped: + if self.wanted: + log.info(f"{self.name}: Starting worker") + holdoff = datetime.now() + self.state = RunState.Starting + else: + self._event.wait() + self._event.clear() + else: + raise ValueError("Unknown state value") + + log.info(f"{self.name}: Shutting down thread") + if self.state == RunState.Running: + self.worker_stop() + log.info(f"{self.name}: Thread exit") + + def worker_start(self): + pass + + def worker_run(self, timeout): + pass + + def worker_stop(self): + pass diff --git a/web/multiqueue.py b/web/multiqueue.py index 2d3d9863..af7e0e69 100644 --- a/web/multiqueue.py +++ b/web/multiqueue.py @@ -1,127 +1,8 @@ -import atexit import contextlib -import logging as log -from datetime import datetime, timedelta -from enum import Enum -from threading import Thread, Event from multiprocessing import Queue - -class RunState(Enum): - Starting = 2 - Running = 3 - # Idle = 4 - Stopping = 5 - Stopped = 6 - - -class Service(Thread): - - def __init__(self, idle_timeout=10): - super().__init__() - self.timeout = timedelta(seconds=idle_timeout) - self.running = True - self.deadline = None - self.state = RunState.Stopped - self.wanted = False - self._event = Event() - atexit.register(self.atexit) - super().start() - - def atexit(self): - log.info(f"{self.name}: Requesting thread exit..") - self.running = False - self._event.set() - self.join() - log.info(f"{self.name}: Thread cleanup done") - - @property - def name(self): - return type(self).__name__ - - def start(self): - log.info(f"{self.name}: Requesting start") - self.wanted = True - self._event.set() - - def stop(self): - log.info(f"{self.name}: Requesting stop") - self.wanted = False - self._event.set() - - def run(self): - holdoff = None - - while self.running: - if self.state == RunState.Starting: - log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") - if datetime.now() > holdoff: - try: - log.info(f"{self.name} worker start") - self.worker_start() - except Exception as E: - log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.info(f"{self.name}: Worker started") - self.state = RunState.Running - else: - self._event.wait(timeout=0.1) - self._event.clear() - - elif self.state == RunState.Running: - if self.wanted: - try: - self.worker_run(timeout=0.3) - except Exception: - log.exception("Unexpected exception while running worker") - log.warning(f"{self.name}: Stopping worker due to exception") - holdoff = datetime.now() - self.state = RunState.Stopping - else: - log.info(f"{self.name}: Stopping worker") - holdoff = datetime.now() - self.state = RunState.Stopping - - elif self.state == RunState.Stopping: - if datetime.now() > holdoff: - try: - self.worker_stop() - except Exception as E: - log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.info(f"{self.name}: Worked stopped") - self.state = RunState.Stopped - else: - self._event.wait(timeout=0.1) - self._event.clear() - - elif self.state == RunState.Stopped: - if self.wanted: - log.info(f"{self.name}: Starting worker") - holdoff = datetime.now() - self.state = RunState.Starting - else: - self._event.wait() - self._event.clear() - else: - raise ValueError("Unknown state value") - - log.info(f"{self.name}: Shutting down thread") - if self.state == RunState.Running: - self.worker_stop() - log.info(f"{self.name}: Thread exit") - - def worker_start(self): - pass - - def worker_run(self, timeout): - pass - - def worker_stop(self): - pass +from .lib.service import Service class QueueTap: From 08163315323d373de170aa51e847a87e1ce469d5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:10:23 +0200 Subject: [PATCH 055/405] + Implement .notify() and .tap() in Service class, for distributing service updates --- web/lib/service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 76eafb91..716f6266 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -123,3 +123,15 @@ def worker_run(self, timeout): def worker_stop(self): pass + + def notify(self, data): + for handler in self.handlers: + handler(data) + + @contextlib.contextmanager + def tap(self, handler): + self.handlers.append(handler) + try: + yield self + finally: + self.handlers.remove(handler) From 50db2508f0ab659d3a50954eaa4c039c6d69f4bd Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:10:47 +0200 Subject: [PATCH 056/405] + Implement ServiceManager The ServiceManager class keeps track of registered micro-services, starts/stops them on demand, and allows services to request access to each other. A reference count is kept for each service, and when the usage goes to 0, the service is stopped. This prevents background services from keeping connections open, that may not be shareable (such as for video streaming) --- web/lib/service.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 716f6266..396f81ec 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -135,3 +135,72 @@ def tap(self, handler): yield self finally: self.handlers.remove(handler) + + +class ServiceManager: + + def __init__(self): + self.svcs = {} + self.refs = {} + + def register(self, name: str, svc: Service): + if name in self.svcs: + raise KeyError(f"Trying to register {name!r} as {svc} while already taken by {self.svcs[name]}") + + self.svcs[name] = svc + self.refs[name] = 0 + + def unregister(self, name: str): + if name not in self.svcs: + raise KeyError(f"Trying to unregister unknown service {name!r}") + + del self.svcs[name] + del self.refs[name] + + def get(self, name: str) -> Service: + if name not in self.svcs: + raise KeyError(f"Requested unknown service {name!r}") + + svc = self.svcs[name] + + if not self.refs[name]: + svc.start() + + self.refs[name] += 1 + + return svc + + def put(self, name: str): + if name not in self.svcs: + raise KeyError(f"Requested unknown service {name!r}") + + svc = self.svcs[name] + + assert self.refs[name] + + self.refs[name] -= 1 + + if not self.refs[name]: + svc.stop() + + @contextlib.contextmanager + def borrow(self, name: str): + svc = self.get(name) + try: + yield svc + finally: + self.put(name) + + def stream(self, name: str): + with self.borrow(name) as svc: + queue = Queue() + + def handler(data): + queue.put(data) + + with svc.tap(handler): + while True: + try: + yield queue.get() + except (EOFError, OSError): + break From 94380bd96901126ace293c6c10739d5d17d22e81 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:12:30 +0200 Subject: [PATCH 057/405] + Implemented MqttQueue as Service subclass --- web/service/mqtt.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 web/service/mqtt.py diff --git a/web/service/mqtt.py b/web/service/mqtt.py new file mode 100644 index 00000000..00c547da --- /dev/null +++ b/web/service/mqtt.py @@ -0,0 +1,25 @@ +import logging as log + +from ..lib.service import Service +from .. import app + +from libflagship.util import enhex + +import cli.mqtt + + +class MqttQueue(Service): + + def worker_start(self): + self.client = cli.mqtt.mqtt_open(app.config["config"], True) + + def worker_run(self, timeout): + for msg, body in self.client.fetch(timeout=timeout): + log.info(f"TOPIC [{msg.topic}]") + log.debug(enhex(msg.payload[:])) + + for obj in body: + self.notify(obj) + + def worker_stop(self): + del self.client From 6de3883119b25f33a47ed9e4895a28c1a06b7106 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:12:40 +0200 Subject: [PATCH 058/405] + Implemented VideoQueue as Service subclass --- web/service/video.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 web/service/video.py diff --git a/web/service/video.py b/web/service/video.py new file mode 100644 index 00000000..9898015c --- /dev/null +++ b/web/service/video.py @@ -0,0 +1,39 @@ +import json +import logging as log + +from ..lib.service import Service +from .. import app + +from libflagship.pppp import P2PSubCmdType, P2PCmdType +from libflagship.util import enhex + +import cli.mqtt + + +class VideoQueue(Service): + + def send_command(self, commandType, **kwargs): + cmd = { + "commandType": commandType, + **kwargs + } + return self.api.send_xzyh( + json.dumps(cmd).encode(), + cmd=P2PCmdType.P2P_JSON_CMD + ) + + def worker_start(self): + self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) + + self.send_command(P2PSubCmdType.START_LIVE, data={"encryptkey": "x", "accountId": "y"}) + + def worker_run(self, timeout): + d = self.api.recv_xzyh(chan=1, timeout=timeout) + if not d: + return + + log.debug(f"Video data packet: {enhex(d.data):32}...") + self.notify(d.data) + + def worked_stop(self): + self.api.send_command(P2PSubCmdType.CLOSE_LIVE) From 0c0b9ea152b1d30e61a65c5f7a1936b2b835c3d3 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:18:18 +0200 Subject: [PATCH 059/405] * Refactor mqtt and pppp to use new ServiceManager-enabled services. --- web/__init__.py | 79 +++++++---------------------------------------- web/multiqueue.py | 54 -------------------------------- 2 files changed, 12 insertions(+), 121 deletions(-) delete mode 100644 web/multiqueue.py diff --git a/web/__init__.py b/web/__init__.py index ce704e1e..1ab7360e 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -4,14 +4,10 @@ from flask import Flask, request, render_template, Response from flask_sock import Sock -from libflagship.util import enhex -from libflagship.pppp import P2PSubCmdType, P2PCmdType +from libflagship.pppp import P2PSubCmdType from libflagship.ppppapi import FileUploadInfo, PPPPError, FileTransfer -from web.multiqueue import MultiQueue - -import cli.mqtt - +from web.lib.service import ServiceManager app = Flask( __name__, @@ -20,85 +16,34 @@ template_folder="static" ) app.config.from_prefixed_env() +app.svc = ServiceManager() sock = Sock(app) -class MqttQueue(MultiQueue): - - def worker_start(self): - self.client = cli.mqtt.mqtt_open(app.config["config"], True) - - def worker_run(self, timeout): - for msg, body in self.client.fetch(timeout=timeout): - log.info(f"TOPIC [{msg.topic}]") - log.debug(enhex(msg.payload[:])) - - for obj in body: - self.put(obj) - - def worker_stop(self): - del self.client - - -class VideoQueue(MultiQueue): - - def send_command(self, commandType, **kwargs): - cmd = { - "commandType": commandType, - **kwargs - } - return self.api.send_xzyh( - json.dumps(cmd).encode(), - cmd=P2PCmdType.P2P_JSON_CMD - ) - - def worker_start(self): - self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) - - self.send_command(P2PSubCmdType.START_LIVE, data={"encryptkey": "x", "accountId": "y"}) - - def worker_run(self, timeout): - d = self.api.recv_xzyh(chan=1, timeout=timeout) - if not d: - return - - log.debug(f"Video data packet: {enhex(d.data):32}...") - self.put(d.data) - - def worked_stop(self): - self.api.send_command(P2PSubCmdType.CLOSE_LIVE) +import web.service.mqtt +import web.service.video @app.before_first_request def startup(): - app.mqttq = MqttQueue() - app.videoq = VideoQueue() + app.svc.register("videoqueue", web.service.video.VideoQueue()) + app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) @sock.route("/ws/mqtt") def mqtt(sock): - with app.mqttq.tap() as queue: - while True: - try: - data = queue.get() - except EOFError: - break - log.debug(f"MQTT message: {data}") - sock.send(json.dumps(data)) + for data in app.svc.stream("mqttqueue"): + log.debug(f"MQTT message: {data}") + sock.send(json.dumps(data)) @sock.route("/ws/video") def video(sock): - with app.videoq.tap() as queue: - while True: - try: - data = queue.get() - except (EOFError, OSError): - break - sock.send(data) + for data in app.svc.stream("videoqueue"): + sock.send(data) @sock.route("/ws/ctrl") diff --git a/web/multiqueue.py b/web/multiqueue.py deleted file mode 100644 index af7e0e69..00000000 --- a/web/multiqueue.py +++ /dev/null @@ -1,54 +0,0 @@ -import contextlib - -from multiprocessing import Queue - -from .lib.service import Service - - -class QueueTap: - - def __init__(self, queue): - self.queue = queue - - def __iter__(self): - return self - - def __next__(self): - try: - return self.get() - except (EOFError, OSError): - raise StopIteration() - - def get(self, timeout=None): - return self.queue.get(timeout=timeout) - - -class MultiQueue(Service): - - def __init__(self, idle_timeout=10): - super().__init__(idle_timeout) - self.targets = [] - - @contextlib.contextmanager - def tap(self): - queue = Queue() - self.add_target(queue) - try: - yield QueueTap(queue) - finally: - self.del_target(queue) - - def put(self, obj): - for target in self.targets: - target.put(obj) - - def add_target(self, target): - if not self.targets: - self.start() - self.targets.append(target) - - def del_target(self, target): - if target in self.targets: - self.targets.remove(target) - if not self.targets: - self.stop() From f272b16eef09ae3b0b0fd66f5385bf554bf7284c Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:18:36 +0200 Subject: [PATCH 060/405] + Added missing import --- web/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/__init__.py b/web/__init__.py index 1ab7360e..c5fd92b7 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -9,6 +9,8 @@ from web.lib.service import ServiceManager +import cli.util + app = Flask( __name__, root_path=".", From 354862fb39da2ef304bc77be9284cac21b6d2363 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:52:35 +0200 Subject: [PATCH 061/405] + Added option to change video stream quality from web interface --- static/ankersrv.js | 10 ++++++++++ static/index.html | 10 ++++++++-- web/__init__.py | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/static/ankersrv.js b/static/ankersrv.js index 2e31497f..d4b4065f 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -46,6 +46,16 @@ $(function () { return false; }); + $('#quality-low').on('click', function() { + wsctrl.send(JSON.stringify({"quality": 0})); + return false; + }); + + $('#quality-high').on('click', function() { + wsctrl.send(JSON.stringify({"quality": 1})); + return false; + }); + $('#configData').on('click',function(){ navigator.clipboard.writeText("{{ configHost }}:{{ configPort }}"); diff --git a/static/index.html b/static/index.html index 32ce9776..9467fdc8 100644 --- a/static/index.html +++ b/static/index.html @@ -19,8 +19,14 @@

ankerctl

- - +
+
+
+
+
+
+
+
diff --git a/web/__init__.py b/web/__init__.py index c5fd92b7..24e2aa68 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -55,10 +55,18 @@ def ctrl(sock): msg = json.loads(sock.receive()) if "light" in msg: - app.videoq.send_command( - P2PSubCmdType.LIGHT_STATE_SWITCH, - data={"open": int(msg["light"])} - ) + with app.svc.borrow("videoqueue") as vq: + vq.send_command( + P2PSubCmdType.LIGHT_STATE_SWITCH, + data={"open": int(msg["light"])} + ) + + if "quality" in msg: + with app.svc.borrow("videoqueue") as vq: + vq.send_command( + P2PSubCmdType.LIVE_MODE_SET, + data={"mode": int(msg["quality"])} + ) @app.get("/video") From ce2ad8d590094828fe41c34bbec75cff75ea1a3e Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 15:53:03 +0200 Subject: [PATCH 062/405] * Make video feed take 2/3 of the width --- static/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/index.html b/static/index.html index 9467fdc8..38ae248e 100644 --- a/static/index.html +++ b/static/index.html @@ -15,10 +15,10 @@

ankerctl

-
+
-
+
From 5012b1c4303b6351ae38a91b49a51b5ded4fe561 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 21:46:05 +0200 Subject: [PATCH 063/405] * Downgrade some Service log messages to DEBUG severity, to not be overwhelmed at INFO level. --- web/lib/service.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 396f81ec..14b26e4e 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -31,11 +31,11 @@ def __init__(self, idle_timeout=10): super().start() def atexit(self): - log.info(f"{self.name}: Requesting thread exit..") + log.debug(f"{self.name}: Requesting thread exit..") self.running = False self._event.set() self.join() - log.info(f"{self.name}: Thread cleanup done") + log.debug(f"{self.name}: Thread cleanup done") @property def name(self): @@ -59,13 +59,13 @@ def run(self): log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") if datetime.now() > holdoff: try: - log.info(f"{self.name} worker start") + log.debug(f"{self.name} worker start") self.worker_start() except Exception as E: log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") holdoff = datetime.now() + timedelta(seconds=1) else: - log.info(f"{self.name}: Worker started") + log.debug(f"{self.name}: Worker started") self.state = RunState.Running else: self._event.wait(timeout=0.1) @@ -81,7 +81,7 @@ def run(self): holdoff = datetime.now() self.state = RunState.Stopping else: - log.info(f"{self.name}: Stopping worker") + log.debug(f"{self.name}: Stopping worker") holdoff = datetime.now() self.state = RunState.Stopping @@ -93,7 +93,7 @@ def run(self): log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") holdoff = datetime.now() + timedelta(seconds=1) else: - log.info(f"{self.name}: Worked stopped") + log.debug(f"{self.name}: Worked stopped") self.state = RunState.Stopped else: self._event.wait(timeout=0.1) @@ -101,7 +101,7 @@ def run(self): elif self.state == RunState.Stopped: if self.wanted: - log.info(f"{self.name}: Starting worker") + log.debug(f"{self.name}: Starting worker") holdoff = datetime.now() self.state = RunState.Starting else: @@ -110,7 +110,7 @@ def run(self): else: raise ValueError("Unknown state value") - log.info(f"{self.name}: Shutting down thread") + log.debug(f"{self.name}: Shutting down thread") if self.state == RunState.Running: self.worker_stop() log.info(f"{self.name}: Thread exit") From 25bb05ac44cfe81178fa67811edc2daa5bbf03f9 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 21:46:52 +0200 Subject: [PATCH 064/405] + Implement Service.idle(), which waits (with optional) timeout for service change event. --- web/lib/service.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 14b26e4e..232b984f 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -51,6 +51,10 @@ def stop(self): self.wanted = False self._event.set() + def idle(self, timeout=None): + if self._event.wait(timeout=timeout): + self._event.clear() + def run(self): holdoff = None @@ -68,8 +72,7 @@ def run(self): log.debug(f"{self.name}: Worker started") self.state = RunState.Running else: - self._event.wait(timeout=0.1) - self._event.clear() + self.idle(timeout=0.1) elif self.state == RunState.Running: if self.wanted: @@ -96,8 +99,7 @@ def run(self): log.debug(f"{self.name}: Worked stopped") self.state = RunState.Stopped else: - self._event.wait(timeout=0.1) - self._event.clear() + self.idle(timeout=0.1) elif self.state == RunState.Stopped: if self.wanted: @@ -105,8 +107,7 @@ def run(self): holdoff = datetime.now() self.state = RunState.Starting else: - self._event.wait() - self._event.clear() + self.idle() else: raise ValueError("Unknown state value") From 49c2f6e2ab2279b2f45ca4fd34c013715281f604 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 21:47:55 +0200 Subject: [PATCH 065/405] + Implement Service.await_ready(), to wait for Service to become ready. Handles Service shutdown request without deadlocking. --- web/lib/service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 232b984f..111956da 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -137,6 +137,18 @@ def tap(self, handler): finally: self.handlers.remove(handler) + def await_ready(self): + while True: + log.debug(f"{self.name}: Awaiting ready ({self.state})") + if not self.wanted: + raise RuntimeError(f"{self.name}: Waiting for stopped thread") + + if self.state == RunState.Running: + log.debug(f"{self.name}: Ready") + return True + + self.idle(timeout=0.1) + class ServiceManager: From 0f9adaa9bcbdeb70a05bcf346bca1d6c31844093 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 21:48:23 +0200 Subject: [PATCH 066/405] + Make ServiceManager.get() take ready=True argument. If ready is set, wait for service ready before returning --- web/lib/service.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 111956da..6062ae81 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -170,16 +170,17 @@ def unregister(self, name: str): del self.svcs[name] del self.refs[name] - def get(self, name: str) -> Service: + def get(self, name: str, ready=True) -> Service: if name not in self.svcs: raise KeyError(f"Requested unknown service {name!r}") svc = self.svcs[name] + self.refs[name] += 1 - if not self.refs[name]: + if self.refs[name] == 1: svc.start() - - self.refs[name] += 1 + if ready: + svc.await_ready() return svc From 86bf7a78fad4a25b458dbe6af977e35eb0d4f0ab Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 21:49:38 +0200 Subject: [PATCH 067/405] * Split VideoQueue into VideoQueue and PPPPService. This allows other services to depend on pppp api also, while sharing api connection. --- web/__init__.py | 14 +++++--------- web/service/pppp.py | 33 +++++++++++++++++++++++++++++++++ web/service/video.py | 43 +++++++++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 web/service/pppp.py diff --git a/web/__init__.py b/web/__init__.py index 24e2aa68..ed5cf291 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -23,12 +23,14 @@ sock = Sock(app) -import web.service.mqtt +import web.service.pppp import web.service.video +import web.service.mqtt @app.before_first_request def startup(): + app.svc.register("pppp", web.service.pppp.PPPPService()) app.svc.register("videoqueue", web.service.video.VideoQueue()) app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) @@ -56,17 +58,11 @@ def ctrl(sock): if "light" in msg: with app.svc.borrow("videoqueue") as vq: - vq.send_command( - P2PSubCmdType.LIGHT_STATE_SWITCH, - data={"open": int(msg["light"])} - ) + vq.api_light_state(msg["light"]) if "quality" in msg: with app.svc.borrow("videoqueue") as vq: - vq.send_command( - P2PSubCmdType.LIVE_MODE_SET, - data={"mode": int(msg["quality"])} - ) + vq.api_video_mode(msg["quality"]) @app.get("/video") diff --git a/web/service/pppp.py b/web/service/pppp.py new file mode 100644 index 00000000..b950efdc --- /dev/null +++ b/web/service/pppp.py @@ -0,0 +1,33 @@ +import json + +import logging as log + +from ..lib.service import Service +from .. import app + +from libflagship.pppp import P2PCmdType + +import cli.pppp + + +class PPPPService(Service): + + def api_command(self, commandType, **kwargs): + cmd = { + "commandType": commandType, + **kwargs + } + return self.api.send_xzyh( + json.dumps(cmd).encode(), + cmd=P2PCmdType.P2P_JSON_CMD + ) + + def worker_start(self): + self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) + + def worker_run(self, timeout): + self.idle(timeout) + + def worker_stop(self): + self.api.stop() + del self.api diff --git a/web/service/video.py b/web/service/video.py index 9898015c..2e7450db 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -4,36 +4,43 @@ from ..lib.service import Service from .. import app -from libflagship.pppp import P2PSubCmdType, P2PCmdType +from libflagship.pppp import P2PSubCmdType from libflagship.util import enhex -import cli.mqtt - class VideoQueue(Service): - def send_command(self, commandType, **kwargs): - cmd = { - "commandType": commandType, - **kwargs - } - return self.api.send_xzyh( - json.dumps(cmd).encode(), - cmd=P2PCmdType.P2P_JSON_CMD - ) + def api_start_live(self): + self.pppp.api_command(P2PSubCmdType.START_LIVE, data={ + "encryptkey": "x", + "accountId": "y", + }) - def worker_start(self): - self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) + def api_stop_live(self): + self.pppp.api_command(P2PSubCmdType.CLOSE_LIVE) + + def api_light_state(self, light): + self.pppp.api_command(P2PSubCmdType.LIGHT_STATE_SWITCH, data={ + "open": light, + }) - self.send_command(P2PSubCmdType.START_LIVE, data={"encryptkey": "x", "accountId": "y"}) + def api_video_mode(self, mode): + self.pppp.api_command(P2PSubCmdType.LIVE_MODE_SET, data={ + "mode": mode + }) + + def worker_start(self): + self.pppp = app.svc.get("pppp") + self.api_start_live() def worker_run(self, timeout): - d = self.api.recv_xzyh(chan=1, timeout=timeout) + d = self.pppp.api.recv_xzyh(chan=1, timeout=timeout) if not d: return log.debug(f"Video data packet: {enhex(d.data):32}...") self.notify(d.data) - def worked_stop(self): - self.api.send_command(P2PSubCmdType.CLOSE_LIVE) + def worker_stop(self): + self.api_stop_live() + app.svc.put("pppp") From 024e5becd0de1ce1b36b45789d12eba6730c63b0 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:21:33 +0200 Subject: [PATCH 068/405] + Support timeout=0 in Wire.peek() (thereby, making Channel and AnkerPPPPApi support it as well) --- libflagship/ppppapi.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 000d9a6c..fd5ce4ab 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -78,7 +78,12 @@ def __init__(self): self.rx, self.tx = Pipe(False) def peek(self, size, timeout=None): - if timeout: + # Zero timeout on self.rx.poll() means "wait forever", but we want it to + # mean "no wait", so we emulate that by setting it to 1us. + if timeout == 0.0: + timeout = 0.000001 + + if timeout is not None: deadline = datetime.now() + timedelta(seconds=timeout) while len(self.buf) < size: From cc98cb125f0dbb15d9d858f4f152860a09411874 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:22:03 +0200 Subject: [PATCH 069/405] * Split bottom half of AnkerPPPPApi into AnkerPPPPBaseApi --- libflagship/ppppapi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index fd5ce4ab..d97a64fe 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -213,7 +213,7 @@ def write(self, payload, block=True): return (tx_ctr_start, tx_ctr_done) -class AnkerPPPPApi(Thread): +class AnkerPPPPBaseApi(Thread): def __init__(self, sock, duid, addr=None): super().__init__() @@ -372,6 +372,9 @@ def send_aabb(self, data, sn=0, pos=0, frametype=0, chan=1, block=True): return self.chans[chan].write(aabb.pack_with_crc(data), block=block) + +class AnkerPPPPApi(AnkerPPPPBaseApi): + def recv_xzyh(self, chan=1, timeout=None): fd = self.chans[chan] From 904d69ed24ed88a56202fc52cb47d8a47f61afb5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:26:33 +0200 Subject: [PATCH 070/405] + Implement AnkerPPPPAsyncApi, for poll-based PPPP access --- libflagship/ppppapi.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index d97a64fe..e92ba4d6 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -415,3 +415,22 @@ def recv_aabb_reply(self, chan=1, check=True): def aabb_request(self, data, frametype, pos=0, chan=1, check=True): self.send_aabb(data=data, frametype=frametype, chan=chan, pos=pos) return self.recv_aabb_reply(chan, check) + + +class AnkerPPPPAsyncApi(AnkerPPPPBaseApi): + + def poll(self, timeout=None): + msg = None + try: + msg = self.recv(timeout=timeout) + self.process(msg) + except TimeoutError: + pass + except StopIteration: + raise ConnectionRefusedError("Connection rejected by device") + + for idx, ch in enumerate(self.chans): + for pkt in ch.poll(): + self.send(pkt) + + return msg From bec15d8c9151c57e8c431aa2ffa20335c03fb484 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:26:55 +0200 Subject: [PATCH 071/405] * Fixed typo Service.await_ready() --- web/lib/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/service.py b/web/lib/service.py index 6062ae81..f64f214c 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -140,7 +140,7 @@ def tap(self, handler): def await_ready(self): while True: log.debug(f"{self.name}: Awaiting ready ({self.state})") - if not self.wanted: + if not self.running: raise RuntimeError(f"{self.name}: Waiting for stopped thread") if self.state == RunState.Running: From 5f9201710b0a7af091d5a0ff3b6fa9c22e44b7c4 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:29:26 +0200 Subject: [PATCH 072/405] * Re-implemented PPPPService, using AnkerPPPPAsyncApi. This version sends complete Xzyh frames as notifications. --- web/service/pppp.py | 62 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/web/service/pppp.py b/web/service/pppp.py index b950efdc..81128009 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -1,13 +1,13 @@ import json - import logging as log +from datetime import datetime, timedelta + from ..lib.service import Service from .. import app -from libflagship.pppp import P2PCmdType - -import cli.pppp +from libflagship.pppp import P2PCmdType, PktLanSearch, PktClose, Duid, Type, Xzyh +from libflagship.ppppapi import AnkerPPPPAsyncApi class PPPPService(Service): @@ -23,11 +23,59 @@ def api_command(self, commandType, **kwargs): ) def worker_start(self): - self.api = cli.pppp.pppp_open(app.config["config"], timeout=1, dumpfile=app.config.get("pppp_dump")) + config = app.config["config"] + + deadline = datetime.now() + timedelta(seconds=2) + + with config.open() as cfg: + printer = cfg.printers[0] + + api = AnkerPPPPAsyncApi.open_lan(Duid.from_string(printer.p2p_duid), host=printer.ip_addr) + # _pppp_dumpfile(api, dumpfile) + + log.info("Trying connect over pppp") + + api.send(PktLanSearch()) + + while not api.rdy: + try: + msg = api.recv(timeout=(deadline - datetime.now()).total_seconds()) + api.process(msg) + except StopIteration: + raise ConnectionRefusedError("Connection rejected by device") + + log.info("Established pppp connection") + self.api = api def worker_run(self, timeout): - self.idle(timeout) + msg = self.api.poll(timeout=timeout) + if not msg or msg.type != Type.DRW: + return + + ch = self.api.chans[msg.chan] + + with ch.lock: + data = ch.peek(16, timeout=0) + if not data: + return + + if data[:4] == b'XZYH': + hdr = ch.peek(16, timeout=0) + if not hdr: + return + + xzyh = Xzyh.parse(hdr)[0] + data = ch.read(xzyh.len + 16, timeout=0) + if not data: + return None + + xzyh.data = data[16:] + self.notify((msg.chan, xzyh)) + elif data[:2] == b'\xAA\xBB': + ... + else: + raise ValueError(f"Unexpected data in stream: {data!r}") def worker_stop(self): - self.api.stop() + self.api.send(PktClose()) del self.api From 82f2499c440b97be72d7c033fe22b58bedac291f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 23 Apr 2023 23:30:58 +0200 Subject: [PATCH 073/405] * Reimplement VideoQueue service in terms of PPPPService. This makes it possible to share an async pppp connection for multiple purposes (such as file transfer, video streaming) --- web/service/video.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/web/service/video.py b/web/service/video.py index 2e7450db..1b4c3b89 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -1,10 +1,13 @@ import json import logging as log +from queue import Empty +from multiprocessing import Queue + from ..lib.service import Service from .. import app -from libflagship.pppp import P2PSubCmdType +from libflagship.pppp import P2PSubCmdType, Xzyh from libflagship.util import enhex @@ -31,16 +34,40 @@ def api_video_mode(self, mode): def worker_start(self): self.pppp = app.svc.get("pppp") + self._tap = Queue() + + def handler(data): + self._tap.put(data) + + self._handler = handler + self.pppp.handlers.append(handler) + self.api_start_live() def worker_run(self, timeout): - d = self.pppp.api.recv_xzyh(chan=1, timeout=timeout) - if not d: + try: + data = self._tap.get(timeout=timeout) + except (Empty, OSError): + return + + if not data: return - log.debug(f"Video data packet: {enhex(d.data):32}...") - self.notify(d.data) + chan, msg = data + + if chan != 1: + return + + if not isinstance(msg, Xzyh): + return + + log.debug(f"Video data packet: {enhex(msg.data):32}...") + self.notify(msg.data) def worker_stop(self): self.api_stop_live() + self.pppp.handlers.remove(self._handler) + del self._handler + del self._tap + app.svc.put("pppp") From 8f24e7c65927704c6cb4d9be208a9cbe9c5d0bc2 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 13:50:00 +0200 Subject: [PATCH 074/405] * Make pppp.stf enums specify encoding size, and generate correct parse/pack code using this type hint. --- libflagship/pppp.py | 8 ++++---- specification/pppp.stf | 10 ++++++++++ templates/python/pppp.py.tpl | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index 91a2deab..c6a09912 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -199,11 +199,11 @@ class P2PCmdType(enum.IntEnum): P2P_SEND_FILE = 0x3a98 # unknown @classmethod - def parse(cls, p, typ=u8): + def parse(cls, p, typ=u16le): d = typ.parse(p) return cls(d[0]), d[1] - def pack(self, typ=u8): + def pack(self, typ=u16le): return typ.pack(self) class P2PSubCmdType(enum.IntEnum): @@ -216,11 +216,11 @@ class P2PSubCmdType(enum.IntEnum): LIVE_MODE_GET = 0x03ee # unknown @classmethod - def parse(cls, p, typ=u8): + def parse(cls, p, typ=u16le): d = typ.parse(p) return cls(d[0]), d[1] - def pack(self, typ=u8): + def pack(self, typ=u16le): return typ.pack(self) class FileTransfer(enum.IntEnum): diff --git a/specification/pppp.stf b/specification/pppp.stf index ef7e89c3..8ec8a5e5 100644 --- a/specification/pppp.stf +++ b/specification/pppp.stf @@ -1,4 +1,6 @@ enum Type + @type: u8 + HELLO = 0x00 HELLO_ACK = 0x01 HELLO_TO = 0x02 @@ -74,6 +76,8 @@ enum Type INVALID = 0xFF enum P2PCmdType + @type: u16le + APP_CMD_START_REC_BROADCASE = 0x384 APP_CMD_STOP_REC_BROADCASE = 0x385 APP_CMD_BIND_BROADCAST = 0x3e8 @@ -178,6 +182,8 @@ enum P2PCmdType P2P_SEND_FILE = 0x3a98 enum P2PSubCmdType + @type: u16le + START_LIVE = 0x03e8 CLOSE_LIVE = 0x03e9 VIDEO_RECORD_SWITCH = 0x03ea @@ -187,6 +193,8 @@ enum P2PSubCmdType LIVE_MODE_GET = 0x03ee enum FileTransfer + @type: u8 + # Begin file transfer (sent with metadata) BEGIN = 0x00 @@ -203,6 +211,8 @@ enum FileTransfer REPLY = 0x80 enum FileTransferReply + @type: u8 + # Success OK = 0x00 diff --git a/templates/python/pppp.py.tpl b/templates/python/pppp.py.tpl index 6f5924d7..2bfa4dd6 100644 --- a/templates/python/pppp.py.tpl +++ b/templates/python/pppp.py.tpl @@ -65,11 +65,11 @@ class ${enum.name}(enum.IntEnum): %endfor @classmethod - def parse(cls, p, typ=u8): + def parse(cls, p, typ=${enum.field("@type").type}): d = typ.parse(p) return cls(d[0]), d[1] - def pack(self, typ=u8): + def pack(self, typ=${enum.field("@type").type}): return typ.pack(self) %endif From 0043042f796608533334662446194d4c6c66f380 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 13:50:46 +0200 Subject: [PATCH 075/405] + Add libflagship.pppp.Result type. This enum specifies all the PPPP-related error codes. --- libflagship/pppp.py | 51 ++++++++++++++++++++++++++++++++++++++++++ specification/pppp.stf | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/libflagship/pppp.py b/libflagship/pppp.py index c6a09912..0fc0e63a 100644 --- a/libflagship/pppp.py +++ b/libflagship/pppp.py @@ -253,6 +253,57 @@ def parse(cls, p, typ=u8): def pack(self, typ=u8): return typ.pack(self) +class Result(enum.IntEnum): + ERROR_P2P_SUCCESSFUL = 0x00000000 # unknown + TFCARD_VOLUME_OVERFLOW = 0xffffff7c # unknown + PARAM_NO_CHANGE = 0xffffff8c # unknown + NOT_FACE = 0xffffff8d # unknown + DEV_BUSY = 0xffffff8e # unknown + DEV_UPDATEING = 0xffffff8f # unknown + HUB_UPDATEING = 0xffffff90 # unknown + OPEN_FILE_FAIL = 0xffffff91 # unknown + INVALID_PARAM = 0xffffff92 # unknown + DEV_OFFLINE = 0xffffff93 # unknown + WAIT_TIMEOUT = 0xffffff94 # unknown + NVALID_PARAM_LEN = 0xffffff95 # unknown + NOT_FIND_DEV = 0xffffff96 # unknown + WRITE_FLASH = 0xffffff97 # unknown + INVALID_ACCOUNT = 0xffffff98 # unknown + INVALID_COMMAND = 0xffffff99 # unknown + MAX_HUB_CONNECT_NUM = 0xffffff9a # unknown + HAVE_CONNECT = 0xffffff9b # unknown + NULL_POINT = 0xffffff9c # unknown + ERROR_P2P_FAIL_TO_CREATE_THREAD = 0xffffffea # unknown + ERROR_P2P_INVALID_APILICENSE = 0xffffffeb # unknown + ERROR_P2P_SESSION_CLOSED_INSUFFICIENT_MEMORY = 0xffffffec # unknown + ERROR_P2P_USER_CONNECT_BREAK = 0xffffffed # unknown + ERROR_P2P_UDP_PORT_BIND_FAILED = 0xffffffee # unknown + ERROR_P2P_MAX_SESSION = 0xffffffef # unknown + ERROR_P2P_USER_LISTEN_BREAK = 0xfffffff0 # unknown + ERROR_P2P_REMOTE_SITE_BUFFER_FULL = 0xfffffff1 # unknown + ERROR_P2P_SESSION_CLOSED_CALLED = 0xfffffff2 # unknown + ERROR_P2P_SESSION_CLOSED_TIMEOUT = 0xfffffff3 # unknown + ERROR_P2P_SESSION_CLOSED_REMOTE = 0xfffffff4 # unknown + ERROR_P2P_INVALID_SESSION_HANDLE = 0xfffffff5 # unknown + ERROR_P2P_NO_RELAY_SERVER_AVAILABLE = 0xfffffff6 # unknown + ERROR_P2P_ID_OUT_OF_DATE = 0xfffffff7 # unknown + ERROR_P2P_INVALID_PREFIX = 0xfffffff8 # unknown + ERROR_P2P_FAIL_TO_RESOLVE_NAME = 0xfffffff9 # unknown + ERROR_P2P_DEVICE_NOT_ONLINE = 0xfffffffa # unknown + ERROR_PPCS_INVALID_PARAMETER = 0xfffffffb # unknown + ERROR_P2P_INVALID_ID = 0xfffffffc # unknown + ERROR_P2P_TIME_OUT = 0xfffffffd # unknown + ERROR_P2P_ALREADY_INITIALIZED = 0xfffffffe # unknown + ERROR_P2P_NOT_INITIALIZED = 0xffffffff # unknown + + @classmethod + def parse(cls, p, typ=u32): + d = typ.parse(p) + return cls(d[0]), d[1] + + def pack(self, typ=u32): + return typ.pack(self) + @dataclass class Message: diff --git a/specification/pppp.stf b/specification/pppp.stf index 8ec8a5e5..bfb1b337 100644 --- a/specification/pppp.stf +++ b/specification/pppp.stf @@ -228,6 +228,51 @@ enum FileTransferReply # Printer was not ready to receive ERR_BUSY = 0xff +enum Result + @type: u32 + + ERROR_P2P_SUCCESSFUL = 0 + TFCARD_VOLUME_OVERFLOW = 0xffffff7c + PARAM_NO_CHANGE = 0xffffff8c + NOT_FACE = 0xffffff8d + DEV_BUSY = 0xffffff8e + DEV_UPDATEING = 0xffffff8f + HUB_UPDATEING = 0xffffff90 + OPEN_FILE_FAIL = 0xffffff91 + INVALID_PARAM = 0xffffff92 + DEV_OFFLINE = 0xffffff93 + WAIT_TIMEOUT = 0xffffff94 + NVALID_PARAM_LEN = 0xffffff95 + NOT_FIND_DEV = 0xffffff96 + WRITE_FLASH = 0xffffff97 + INVALID_ACCOUNT = 0xffffff98 + INVALID_COMMAND = 0xffffff99 + MAX_HUB_CONNECT_NUM = 0xffffff9a + HAVE_CONNECT = 0xffffff9b + NULL_POINT = 0xffffff9c + ERROR_P2P_FAIL_TO_CREATE_THREAD = 0xffffffea + ERROR_P2P_INVALID_APILICENSE = 0xffffffeb + ERROR_P2P_SESSION_CLOSED_INSUFFICIENT_MEMORY = 0xffffffec + ERROR_P2P_USER_CONNECT_BREAK = 0xffffffed + ERROR_P2P_UDP_PORT_BIND_FAILED = 0xffffffee + ERROR_P2P_MAX_SESSION = 0xffffffef + ERROR_P2P_USER_LISTEN_BREAK = 0xfffffff0 + ERROR_P2P_REMOTE_SITE_BUFFER_FULL = 0xfffffff1 + ERROR_P2P_SESSION_CLOSED_CALLED = 0xfffffff2 + ERROR_P2P_SESSION_CLOSED_TIMEOUT = 0xfffffff3 + ERROR_P2P_SESSION_CLOSED_REMOTE = 0xfffffff4 + ERROR_P2P_INVALID_SESSION_HANDLE = 0xfffffff5 + ERROR_P2P_NO_RELAY_SERVER_AVAILABLE = 0xfffffff6 + ERROR_P2P_ID_OUT_OF_DATE = 0xfffffff7 + ERROR_P2P_INVALID_PREFIX = 0xfffffff8 + ERROR_P2P_FAIL_TO_RESOLVE_NAME = 0xfffffff9 + ERROR_P2P_DEVICE_NOT_ONLINE = 0xfffffffa + ERROR_PPCS_INVALID_PARAMETER = 0xfffffffb + ERROR_P2P_INVALID_ID = 0xfffffffc + ERROR_P2P_TIME_OUT = 0xfffffffd + ERROR_P2P_ALREADY_INITIALIZED = 0xfffffffe + ERROR_P2P_NOT_INITIALIZED = 0xffffffff + struct Host pad0: zeroes<1> From f99e2edc48d2951c4fe53df870bf34cca73f5c49 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:13:43 +0200 Subject: [PATCH 076/405] * Changed AnkerPPPPApi.{rdy,new} booleans to .state enum, to better describe connection state. --- cli/pppp.py | 8 ++++---- libflagship/ppppapi.py | 21 +++++++++++++++------ web/service/pppp.py | 12 +++++++++--- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cli/pppp.py b/cli/pppp.py index 68b7375f..fa352774 100644 --- a/cli/pppp.py +++ b/cli/pppp.py @@ -8,8 +8,8 @@ import cli.util from libflagship.pktdump import PacketWriter -from libflagship.pppp import PktLanSearch, Duid, P2PCmdType -from libflagship.ppppapi import AnkerPPPPApi, FileTransfer +from libflagship.pppp import Duid, P2PCmdType, FileTransfer +from libflagship.ppppapi import AnkerPPPPApi, PPPPState def _pppp_dumpfile(api, dumpfile): @@ -32,9 +32,9 @@ def pppp_open(config, timeout=None, dumpfile=None): log.info("Trying connect over pppp") api.start() - api.send(PktLanSearch()) + api.connect_lan_search() - while not api.rdy: + while api.state != PPPPState.Connected: time.sleep(0.1) if api.stopped.is_set() or (timeout and (datetime.now() > deadline)): api.stop() diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index e92ba4d6..aa57d5bf 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -4,6 +4,7 @@ import hashlib import logging as log +from enum import Enum from multiprocessing import Pipe from datetime import datetime, timedelta from threading import Thread, Event, Lock @@ -213,6 +214,13 @@ def write(self, payload, block=True): return (tx_ctr_start, tx_ctr_done) +class PPPPState(Enum): + Idle = 1 + Connecting = 2 + Connected = 3 + Disconnected = 4 + + class AnkerPPPPBaseApi(Thread): def __init__(self, sock, duid, addr=None): @@ -221,8 +229,7 @@ def __init__(self, sock, duid, addr=None): self.duid = duid self.addr = addr - self.new = True - self.rdy = False + self.state = PPPPState.Idle self.chans = [Channel(n) for n in range(8)] self.running = True @@ -249,6 +256,10 @@ def open_broadcast(cls): addr = ("255.255.255.255", PPPP_LAN_PORT) return cls(sock, duid=None, addr=addr) + def connect_lan_search(self): + self.state = PPPPState.Connecting + self.send(PktLanSearch()) + def set_dumper(self, dumper): self.dumper = dumper @@ -321,12 +332,10 @@ def process(self, msg): elif msg.type == Type.P2P_RDY: self.send(PktP2pRdyAck(duid=self.duid, host=self.host)) - - self.new = False - self.rdy = True + self.state = PPPPState.Connected elif msg.type == Type.PUNCH_PKT: - if self.new: + if self.state == PPPPState.Connecting: self.send(PktClose()) self.send(PktP2pRdy(self.duid)) diff --git a/web/service/pppp.py b/web/service/pppp.py index 81128009..20753398 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -7,7 +7,7 @@ from .. import app from libflagship.pppp import P2PCmdType, PktLanSearch, PktClose, Duid, Type, Xzyh -from libflagship.ppppapi import AnkerPPPPAsyncApi +from libflagship.ppppapi import AnkerPPPPAsyncApi, PPPPState class PPPPService(Service): @@ -35,9 +35,9 @@ def worker_start(self): log.info("Trying connect over pppp") - api.send(PktLanSearch()) + api.connect_lan_search() - while not api.rdy: + while api.state != PPPPState.Connected: try: msg = api.recv(timeout=(deadline - datetime.now()).total_seconds()) api.process(msg) @@ -79,3 +79,9 @@ def worker_run(self, timeout): def worker_stop(self): self.api.send(PktClose()) del self.api + + @property + def connected(self): + if not hasattr(self, "_api"): + return False + return self._api.state == PPPPState.Connected From e9628fe8b08d54f661afad927f537333ed66822a Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:14:01 +0200 Subject: [PATCH 077/405] + Added missing PPPPError import --- ankerctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankerctl.py b/ankerctl.py index f86b1d21..c1eb1dd8 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -23,7 +23,7 @@ from libflagship.util import enhex from libflagship.mqtt import MqttMsgType from libflagship.pppp import PktLanSearch, P2PCmdType, P2PSubCmdType, FileTransfer -from libflagship.ppppapi import FileUploadInfo +from libflagship.ppppapi import FileUploadInfo, PPPPError import web From 1f79e478213a5b0bc9adb43ce9858d29551d3e65 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:38:59 +0200 Subject: [PATCH 078/405] * Rework ppppapi.py imports to avoid "import *" --- libflagship/ppppapi.py | 5 ++++- web/__init__.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index aa57d5bf..bddf88ec 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -12,7 +12,10 @@ from dataclasses import dataclass from libflagship.cyclic import CyclicU16 -from libflagship.pppp import * +from libflagship.pppp import Type, \ + PktDrw, PktDrwAck, PktClose, PktSessionReady, PktAliveAck, PktDevLgnAckCrc, \ + PktHelloAck, PktP2pRdyAck, PktP2pRdy, PktLanSearch, \ + Host, Message, Xzyh, Aabb, FileTransferReply PPPP_LAN_PORT = 32108 PPPP_WAN_PORT = 32100 diff --git a/web/__init__.py b/web/__init__.py index ed5cf291..4cae4318 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -4,8 +4,9 @@ from flask import Flask, request, render_template, Response from flask_sock import Sock -from libflagship.pppp import P2PSubCmdType -from libflagship.ppppapi import FileUploadInfo, PPPPError, FileTransfer +from libflagship.pppp import P2PSubCmdType, FileTransfer +from libflagship.ppppapi import FileUploadInfo, PPPPError + from web.lib.service import ServiceManager From 42a8c3226fbd5957a65464d3f9b1f21cb25fa1b5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:44:33 +0200 Subject: [PATCH 079/405] * Simplify VideoQueue service by letting handler function filter data --- web/__init__.py | 4 ++-- web/service/video.py | 39 +++++++++++++++------------------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 4cae4318..be390529 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -47,8 +47,8 @@ def mqtt(sock): @sock.route("/ws/video") def video(sock): - for data in app.svc.stream("videoqueue"): - sock.send(data) + for msg in app.svc.stream("videoqueue"): + sock.send(msg.data) @sock.route("/ws/ctrl") diff --git a/web/service/video.py b/web/service/video.py index 1b4c3b89..6f753b69 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -32,27 +32,7 @@ def api_video_mode(self, mode): "mode": mode }) - def worker_start(self): - self.pppp = app.svc.get("pppp") - self._tap = Queue() - - def handler(data): - self._tap.put(data) - - self._handler = handler - self.pppp.handlers.append(handler) - - self.api_start_live() - - def worker_run(self, timeout): - try: - data = self._tap.get(timeout=timeout) - except (Empty, OSError): - return - - if not data: - return - + def _handler(self, data): chan, msg = data if chan != 1: @@ -62,12 +42,23 @@ def worker_run(self, timeout): return log.debug(f"Video data packet: {enhex(msg.data):32}...") - self.notify(msg.data) + self.notify(msg) + + def worker_start(self): + self.pppp = app.svc.get("pppp") + + self.pppp.handlers.append(self._handler) + + self.api_start_live() + + def worker_run(self, timeout): + if not self.pppp.connected: + raise ConnectionError("No pppp connection") + + self.idle(timeout=timeout) def worker_stop(self): self.api_stop_live() self.pppp.handlers.remove(self._handler) - del self._handler - del self._tap app.svc.put("pppp") From 9d44e305d26cfa2804a824634516a75773850437 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:44:58 +0200 Subject: [PATCH 080/405] - Remove debug log for raw video data --- web/service/video.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/service/video.py b/web/service/video.py index 6f753b69..e2b1d2e0 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -8,7 +8,6 @@ from .. import app from libflagship.pppp import P2PSubCmdType, Xzyh -from libflagship.util import enhex class VideoQueue(Service): @@ -41,7 +40,6 @@ def _handler(self, data): if not isinstance(msg, Xzyh): return - log.debug(f"Video data packet: {enhex(msg.data):32}...") self.notify(msg) def worker_start(self): From 554b97c61c8bfeba5cf4365160caa5e511fbdd97 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:46:45 +0200 Subject: [PATCH 081/405] * Make PPPPService.api field private (_api) --- web/service/pppp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/service/pppp.py b/web/service/pppp.py index 20753398..34933f61 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -17,7 +17,7 @@ def api_command(self, commandType, **kwargs): "commandType": commandType, **kwargs } - return self.api.send_xzyh( + return self._api.send_xzyh( json.dumps(cmd).encode(), cmd=P2PCmdType.P2P_JSON_CMD ) @@ -45,14 +45,14 @@ def worker_start(self): raise ConnectionRefusedError("Connection rejected by device") log.info("Established pppp connection") - self.api = api + self._api = api def worker_run(self, timeout): - msg = self.api.poll(timeout=timeout) + msg = self._api.poll(timeout=timeout) if not msg or msg.type != Type.DRW: return - ch = self.api.chans[msg.chan] + ch = self._api.chans[msg.chan] with ch.lock: data = ch.peek(16, timeout=0) @@ -77,8 +77,8 @@ def worker_run(self, timeout): raise ValueError(f"Unexpected data in stream: {data!r}") def worker_stop(self): - self.api.send(PktClose()) - del self.api + self._api.send(PktClose()) + del self._api @property def connected(self): From 5e38d611482b014804db41f0fef7a8369c56ae15 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:48:17 +0200 Subject: [PATCH 082/405] + Implement Aabb framing support in PPPPService --- web/service/pppp.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/service/pppp.py b/web/service/pppp.py index 34933f61..998f552a 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -6,7 +6,7 @@ from ..lib.service import Service from .. import app -from libflagship.pppp import P2PCmdType, PktLanSearch, PktClose, Duid, Type, Xzyh +from libflagship.pppp import P2PCmdType, PktClose, Duid, Type, Xzyh, Aabb from libflagship.ppppapi import AnkerPPPPAsyncApi, PPPPState @@ -47,6 +47,13 @@ def worker_start(self): log.info("Established pppp connection") self._api = api + def _recv_aabb(self, fd): + data = fd.read(12) + aabb = Aabb.parse(data)[0] + p = data + fd.read(aabb.len + 2) + aabb, data = Aabb.parse_with_crc(p)[:2] + return aabb, data + def worker_run(self, timeout): msg = self._api.poll(timeout=timeout) if not msg or msg.type != Type.DRW: @@ -72,7 +79,12 @@ def worker_run(self, timeout): xzyh.data = data[16:] self.notify((msg.chan, xzyh)) elif data[:2] == b'\xAA\xBB': - ... + aabb, data = self._recv_aabb(ch) + if len(data) != 1: + raise ValueError(f"Unexpected reply from aabb request: {data}") + + aabb.data = data + self.notify((msg.chan, aabb)) else: raise ValueError(f"Unexpected data in stream: {data!r}") From 7cc4a680ea0322046d18f5f70982efbb92e5f735 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:49:49 +0200 Subject: [PATCH 083/405] * Use ConnectionResetError instead of StopIteration in AnkerBasePPPPApi --- libflagship/ppppapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index bddf88ec..3c60ba61 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -278,7 +278,7 @@ def run(self): self.process(msg) except TimeoutError: pass - except StopIteration: + except ConnectionResetError: break for idx, ch in enumerate(self.chans): @@ -297,7 +297,7 @@ def process(self, msg): if msg.type == Type.CLOSE: log.error("CLOSE") - raise StopIteration + raise ConnectionResetError elif msg.type == Type.REPORT_SESSION_READY: pkt = PktSessionReady( @@ -438,7 +438,7 @@ def poll(self, timeout=None): self.process(msg) except TimeoutError: pass - except StopIteration: + except ConnectionResetError: raise ConnectionRefusedError("Connection rejected by device") for idx, ch in enumerate(self.chans): From 94da8150e2349705c645f232c746c221c27d3c7a Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 15:52:21 +0200 Subject: [PATCH 084/405] * Refactor Service class, moving parts of .run() into _attempt_{start,run,stop}() --- web/lib/service.py | 69 +++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index f64f214c..9d9b69ec 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -27,6 +27,7 @@ def __init__(self, idle_timeout=10): self.wanted = False self._event = Event() self.handlers = [] + self._holdoff = None atexit.register(self.atexit) super().start() @@ -55,56 +56,62 @@ def idle(self, timeout=None): if self._event.wait(timeout=timeout): self._event.clear() - def run(self): - holdoff = None + def _attempt_start(self): + try: + log.debug(f"{self.name} worker start") + self.worker_start() + except Exception as E: + log.exception(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") + self._holdoff = datetime.now() + timedelta(seconds=1) + else: + log.debug(f"{self.name}: Worker started") + self.state = RunState.Running + + def _attempt_run(self): + try: + self.worker_run(timeout=0.3) + except Exception: + log.exception(f"{self.name}: Unexpected exception while running worker") + log.warning(f"{self.name}: Stopping worker due to exception") + self._holdoff = datetime.now() + self.state = RunState.Stopping + + def _attempt_stop(self): + try: + self.worker_stop() + except Exception as E: + log.exception(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") + self._holdoff = datetime.now() + timedelta(seconds=1) + else: + log.debug(f"{self.name}: Worked stopped") + self.state = RunState.Stopped + def run(self): while self.running: if self.state == RunState.Starting: - log.debug(f"{self.name}: {datetime.now()} vs holdoff {holdoff}") - if datetime.now() > holdoff: - try: - log.debug(f"{self.name} worker start") - self.worker_start() - except Exception as E: - log.error(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.debug(f"{self.name}: Worker started") - self.state = RunState.Running + if datetime.now() > self._holdoff: + self._attempt_start() else: self.idle(timeout=0.1) elif self.state == RunState.Running: if self.wanted: - try: - self.worker_run(timeout=0.3) - except Exception: - log.exception("Unexpected exception while running worker") - log.warning(f"{self.name}: Stopping worker due to exception") - holdoff = datetime.now() - self.state = RunState.Stopping + self._attempt_run() else: log.debug(f"{self.name}: Stopping worker") - holdoff = datetime.now() + self._holdoff = datetime.now() self.state = RunState.Stopping elif self.state == RunState.Stopping: - if datetime.now() > holdoff: - try: - self.worker_stop() - except Exception as E: - log.error(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") - holdoff = datetime.now() + timedelta(seconds=1) - else: - log.debug(f"{self.name}: Worked stopped") - self.state = RunState.Stopped + if datetime.now() > self._holdoff: + self._attempt_stop() else: self.idle(timeout=0.1) elif self.state == RunState.Stopped: if self.wanted: log.debug(f"{self.name}: Starting worker") - holdoff = datetime.now() + self._holdoff = datetime.now() self.state = RunState.Starting else: self.idle() From de33622e01f11e5ce98e0861c5be1364e6296905 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 17:51:36 +0200 Subject: [PATCH 085/405] * Refactor Service class to use new Holdoff class for managing service deadlines --- web/lib/service.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 9d9b69ec..2a4a30f2 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -8,6 +8,21 @@ from multiprocessing import Queue +class Holdoff: + + def __init__(self): + self.deadline = None + + def reset(self, delay=None): + if delay: + delay = timedelta(seconds=delay) + self.deadline = datetime.now() + + @property + def passed(self): + return datetime.now() > self.deadline + + class RunState(Enum): Starting = 2 Running = 3 @@ -27,7 +42,7 @@ def __init__(self, idle_timeout=10): self.wanted = False self._event = Event() self.handlers = [] - self._holdoff = None + self._holdoff = Holdoff() atexit.register(self.atexit) super().start() @@ -62,7 +77,7 @@ def _attempt_start(self): self.worker_start() except Exception as E: log.exception(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") - self._holdoff = datetime.now() + timedelta(seconds=1) + self._holdoff.reset(delay=1) else: log.debug(f"{self.name}: Worker started") self.state = RunState.Running @@ -73,7 +88,7 @@ def _attempt_run(self): except Exception: log.exception(f"{self.name}: Unexpected exception while running worker") log.warning(f"{self.name}: Stopping worker due to exception") - self._holdoff = datetime.now() + self._holdoff.reset() self.state = RunState.Stopping def _attempt_stop(self): @@ -81,7 +96,7 @@ def _attempt_stop(self): self.worker_stop() except Exception as E: log.exception(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") - self._holdoff = datetime.now() + timedelta(seconds=1) + self._holdoff.reset(delay=1) else: log.debug(f"{self.name}: Worked stopped") self.state = RunState.Stopped @@ -89,7 +104,7 @@ def _attempt_stop(self): def run(self): while self.running: if self.state == RunState.Starting: - if datetime.now() > self._holdoff: + if self._holdoff.passed: self._attempt_start() else: self.idle(timeout=0.1) @@ -99,11 +114,11 @@ def run(self): self._attempt_run() else: log.debug(f"{self.name}: Stopping worker") - self._holdoff = datetime.now() + self._holdoff.reset() self.state = RunState.Stopping elif self.state == RunState.Stopping: - if datetime.now() > self._holdoff: + if self._holdoff.passed: self._attempt_stop() else: self.idle(timeout=0.1) @@ -111,7 +126,7 @@ def run(self): elif self.state == RunState.Stopped: if self.wanted: log.debug(f"{self.name}: Starting worker") - self._holdoff = datetime.now() + self._holdoff.reset() self.state = RunState.Starting else: self.idle() From 7c4acab5bd02890b048982b39dfb93eb486c972f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:34:03 +0200 Subject: [PATCH 086/405] + Added safeguards against trying to send/recv pppp data when not connected --- libflagship/ppppapi.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index 3c60ba61..f85fcf9f 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -343,6 +343,9 @@ def process(self, msg): self.send(PktP2pRdy(self.duid)) def recv(self, timeout=None): + if self.state in {PPPPState.Idle, PPPPState.Disconnected}: + raise ConnectionError(f"Tried to recv packet in state {self.state}") + self.sock.settimeout(timeout) data, self.addr = self.sock.recvfrom(4096) if self.dumper: @@ -352,6 +355,9 @@ def recv(self, timeout=None): return msg def send(self, pkt, addr=None): + if self.state in {PPPPState.Idle, PPPPState.Disconnected}: + raise ConnectionError(f"Tried to send packet in state {self.state}") + resp = pkt.pack() if self.dumper: self.dumper.tx(resp, self.addr) From 0df2c922f90ca8f99e1b58d969fc8f5261676877 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:41:58 +0200 Subject: [PATCH 087/405] - Remove obsolete (unimplemented) idle_timeout feature for Service --- web/lib/service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 2a4a30f2..1683aef9 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -33,9 +33,8 @@ class RunState(Enum): class Service(Thread): - def __init__(self, idle_timeout=10): + def __init__(self): super().__init__() - self.timeout = timedelta(seconds=idle_timeout) self.running = True self.deadline = None self.state = RunState.Stopped From 8282230402a12adf306b32f09dd48baab043a4b6 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:43:31 +0200 Subject: [PATCH 088/405] + Implement a new set of Service-related signals and errors --- web/lib/service.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 1683aef9..5aabaa2d 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -23,6 +23,22 @@ def passed(self): return datetime.now() > self.deadline +class ServiceError(Exception): + pass + + +class ServiceStoppedError(ServiceError): + pass + + +class ServiceSignal(Exception): + pass + + +class ServiceRestartSignal(ServiceSignal): + pass + + class RunState(Enum): Starting = 2 Running = 3 From 0e305b2c9d1ff1bcddd531a0a9847c2ef6b680c8 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:44:04 +0200 Subject: [PATCH 089/405] + Implement Service.shutdown(), for stopping and joining the Service thread --- web/lib/service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 5aabaa2d..237ad921 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -82,6 +82,15 @@ def stop(self): self.wanted = False self._event.set() + def shutdown(self): + if self.state != RunState.Stopped: + self.stop() + self.await_stopped() + + self.running = False + self._event.set() + return self.join() + def idle(self, timeout=None): if self._event.wait(timeout=timeout): self._event.clear() From 86fa51f409f62f54e348984add345593393f1af6 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:44:55 +0200 Subject: [PATCH 090/405] + Implement Service.await_stopped(), mirroring Service.await_ready() --- web/lib/service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 237ad921..f33274a8 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -195,6 +195,14 @@ def await_ready(self): self.idle(timeout=0.1) + def await_stopped(self): + while True: + if self.state == RunState.Stopped: + log.debug(f"{self.name}: Stopped") + return True + + self.idle(timeout=0.1) + class ServiceManager: From 292e9f73ae057ba2679155a62f40c74b2db28c4f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:46:01 +0200 Subject: [PATCH 091/405] * Move atexit-related shutdown hook from Service to ServiceManager, to request shutdown of all services in parallel. Improved debug output for manager shutdown. --- web/lib/service.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index f33274a8..c1921f42 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -58,16 +58,8 @@ def __init__(self): self._event = Event() self.handlers = [] self._holdoff = Holdoff() - atexit.register(self.atexit) super().start() - def atexit(self): - log.debug(f"{self.name}: Requesting thread exit..") - self.running = False - self._event.set() - self.join() - log.debug(f"{self.name}: Thread cleanup done") - @property def name(self): return type(self).__name__ @@ -209,6 +201,33 @@ class ServiceManager: def __init__(self): self.svcs = {} self.refs = {} + atexit.register(self.atexit) + + def atexit(self): + log.debug("ServiceManager: Shutting down threads..") + self.dump() + for svc in self.svcs.values(): + if svc.state != RunState.Stopped: + svc.stop() + + log.debug("ServiceManager: Waiting for threads to stop..") + self.dump() + for svc in self.svcs.values(): + svc.await_stopped() + + log.debug("ServiceManager: Cleaning up threads..") + self.dump() + for svc in self.svcs.values(): + svc.shutdown() + + log.info("ServiceManager: Shutdown complete") + + def dump(self): + log.debug("Service state") + for name in self.svcs: + svc = self.svcs[name] + ref = self.refs[name] + log.debug(f" [{ref:>4}] {name:20} running={svc.running} state={svc.state} wanted={svc.wanted}") def register(self, name: str, svc: Service): if name in self.svcs: From 41bf5bc60e91541554c5d5b496577b003afdbc27 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:46:46 +0200 Subject: [PATCH 092/405] + Handle ServiceRestartSignal in Service._attempt_run(). Services can now request restart by raising this signal in their worker_run() function. --- web/lib/service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index c1921f42..0def96dc 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -101,6 +101,10 @@ def _attempt_start(self): def _attempt_run(self): try: self.worker_run(timeout=0.3) + except ServiceRestartSignal: + log.info(f"{self.name}: Service requested restart.") + self._holdoff.reset() + self.state = RunState.Stopping except Exception: log.exception(f"{self.name}: Unexpected exception while running worker") log.warning(f"{self.name}: Stopping worker due to exception") From 5f0d9f50c63c3286f24861825111af2b91628e55 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:47:49 +0200 Subject: [PATCH 093/405] * Adjust log level of a few Service-related messages --- web/lib/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 0def96dc..2e4a6abc 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -95,7 +95,7 @@ def _attempt_start(self): log.exception(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") self._holdoff.reset(delay=1) else: - log.debug(f"{self.name}: Worker started") + log.info(f"{self.name}: Worker started") self.state = RunState.Running def _attempt_run(self): @@ -118,7 +118,7 @@ def _attempt_stop(self): log.exception(f"{self.name}: Failed to stop worker: {E}. Retrying in 1 second.") self._holdoff.reset(delay=1) else: - log.debug(f"{self.name}: Worked stopped") + log.info(f"{self.name}: Worker stopped") self.state = RunState.Stopped def run(self): @@ -156,7 +156,7 @@ def run(self): log.debug(f"{self.name}: Shutting down thread") if self.state == RunState.Running: self.worker_stop() - log.info(f"{self.name}: Thread exit") + log.debug(f"{self.name}: Thread exit") def worker_start(self): pass From e78bee1033025d57b42c67c1643ffce13f0644a5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:49:04 +0200 Subject: [PATCH 094/405] + Added Service.worker_init() as a convenience method. This is called once, when the service thread first starts. --- web/lib/service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 2e4a6abc..8377b4d9 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -122,6 +122,8 @@ def _attempt_stop(self): self.state = RunState.Stopped def run(self): + self.worker_init() + while self.running: if self.state == RunState.Starting: if self._holdoff.passed: @@ -158,6 +160,9 @@ def run(self): self.worker_stop() log.debug(f"{self.name}: Thread exit") + def worker_init(self): + pass + def worker_start(self): pass From cf319853207ff5f8ec13a50503359f06e8ca953c Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:49:45 +0200 Subject: [PATCH 095/405] * Simplified Service.stream() using lambda function --- web/lib/service.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 8377b4d9..55f24c24 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -291,10 +291,7 @@ def stream(self, name: str): with self.borrow(name) as svc: queue = Queue() - def handler(data): - queue.put(data) - - with svc.tap(handler): + with svc.tap(lambda data: queue.put(data)): while True: try: yield queue.get() From 1b9bb06f1404d4c2eb66e8ae720ce9617b8cedc2 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:50:20 +0200 Subject: [PATCH 096/405] * Make Service.stream() more robust, by checking for service state and ServiceStoppedError exceptions --- web/lib/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 55f24c24..6ff61f07 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -292,8 +292,8 @@ def stream(self, name: str): queue = Queue() with svc.tap(lambda data: queue.put(data)): - while True: + while svc.state == RunState.Running: try: yield queue.get() - except (EOFError, OSError): + except (EOFError, OSError, ServiceStoppedError): break From a36c0ddf4df386d9ca9a3185574e8eda41e52d73 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:51:27 +0200 Subject: [PATCH 097/405] * Clear .handlers on Service thread exit --- web/lib/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/web/lib/service.py b/web/lib/service.py index 6ff61f07..4efdfc18 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -157,6 +157,7 @@ def run(self): log.debug(f"{self.name}: Shutting down thread") if self.state == RunState.Running: + self.handlers.clear() self.worker_stop() log.debug(f"{self.name}: Thread exit") From 748d784965497df4aaa211e8e71c28e99c7db2e0 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:52:11 +0200 Subject: [PATCH 098/405] * Make Service more robust by handling TimeoutError and state changes in _attempt_start() --- web/lib/service.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 4efdfc18..bc9eee0c 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -92,8 +92,13 @@ def _attempt_start(self): log.debug(f"{self.name} worker start") self.worker_start() except Exception as E: - log.exception(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") - self._holdoff.reset(delay=1) + if self.wanted: + if not isinstance(E, TimeoutError): + log.exception(f"{self.name}: Failed to start worker: {E}. Retrying in 1 second.") + self._holdoff.reset(delay=1) + else: + log.error(f"{self.name}: Failed to start worker: {E}. Shutting down service.") + self.state = RunState.Stopped else: log.info(f"{self.name}: Worker started") self.state = RunState.Running From 3028118cddd6bcf8eeb94bbffe72b3e797868633 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:53:08 +0200 Subject: [PATCH 099/405] * Avoid deadlocks by setting timeout=0.1, in case self.wanted is changed without signalling self.event --- web/lib/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/service.py b/web/lib/service.py index bc9eee0c..fcb0bdda 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -156,7 +156,7 @@ def run(self): self._holdoff.reset() self.state = RunState.Starting else: - self.idle() + self.idle(timeout=0.1) else: raise ValueError("Unknown state value") From 313222413cbb482a21c2eb98ee67cfd01f0af89c Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:53:58 +0200 Subject: [PATCH 100/405] * Make Service.await_ready() use new ServiceStoppedError to signal service failure. This allows ServiceManager to better handle these cases. --- web/lib/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index fcb0bdda..2e8c2683 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -193,8 +193,8 @@ def tap(self, handler): def await_ready(self): while True: log.debug(f"{self.name}: Awaiting ready ({self.state})") - if not self.running: - raise RuntimeError(f"{self.name}: Waiting for stopped thread") + if not (self.running and self.wanted): + raise ServiceStoppedError(f"{self.name}: Waiting for stopped thread") if self.state == RunState.Running: log.debug(f"{self.name}: Ready") From 7db17c4d2ee2615bac61b3cb3c40afde795a5bfa Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:54:30 +0200 Subject: [PATCH 101/405] * Make ServiceManager.get() release service, in case a ServiceException happens during .await_ready() --- web/lib/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/lib/service.py b/web/lib/service.py index 2e8c2683..ac128e7b 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -268,7 +268,10 @@ def get(self, name: str, ready=True) -> Service: if self.refs[name] == 1: svc.start() if ready: - svc.await_ready() + try: + svc.await_ready() + except ServiceError: + self.put(name) return svc From c4f394ac5bb4ec6fe810a301d0ec43d1df252452 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:55:19 +0200 Subject: [PATCH 102/405] * Make PPPPService convert ConnectionResetError to ServiceRestartSignal in worker_run(), to allow re-connecting lost pppp connections --- web/service/pppp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/service/pppp.py b/web/service/pppp.py index 998f552a..4c13f05a 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta -from ..lib.service import Service +from ..lib.service import Service, ServiceRestartSignal from .. import app from libflagship.pppp import P2PCmdType, PktClose, Duid, Type, Xzyh, Aabb @@ -55,7 +55,11 @@ def _recv_aabb(self, fd): return aabb, data def worker_run(self, timeout): - msg = self._api.poll(timeout=timeout) + try: + msg = self._api.poll(timeout=timeout) + except ConnectionResetError: + raise ServiceRestartSignal() + if not msg or msg.type != Type.DRW: return From dd3276fd94f2d806efe3ba381886809e4d8d4ce5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:55:51 +0200 Subject: [PATCH 103/405] * Improve error message if PPPPService.api_command() is called without active connection --- web/service/pppp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/service/pppp.py b/web/service/pppp.py index 4c13f05a..6925a1f4 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -13,6 +13,8 @@ class PPPPService(Service): def api_command(self, commandType, **kwargs): + if not hasattr(self, "_api"): + raise ConnectionError("No pppp connection") cmd = { "commandType": commandType, **kwargs From 4c820be4dcf5da9e5c18aaeee2bb93c458294860 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:56:39 +0200 Subject: [PATCH 104/405] * Make PPPPService.api_command() use block=False, to avoid blocking writes. If we are reconnecting/shutting down the pppp connection, blocking writes can cause deadlocks. --- web/service/pppp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/service/pppp.py b/web/service/pppp.py index 6925a1f4..8ac251d8 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -21,7 +21,8 @@ def api_command(self, commandType, **kwargs): } return self._api.send_xzyh( json.dumps(cmd).encode(), - cmd=P2PCmdType.P2P_JSON_CMD + cmd=P2PCmdType.P2P_JSON_CMD, + block=False ) def worker_start(self): From 7677bbe7f1dd51d5287ae81cf79a004f95e03ebf Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:57:44 +0200 Subject: [PATCH 105/405] * Make VideoQueue service detect pppp connection problems, and raise ServiceRestartSignal in response --- web/service/video.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/service/video.py b/web/service/video.py index e2b1d2e0..8a9f5f60 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -4,7 +4,7 @@ from queue import Empty from multiprocessing import Queue -from ..lib.service import Service +from ..lib.service import Service, ServiceRestartSignal from .. import app from libflagship.pppp import P2PSubCmdType, Xzyh @@ -45,15 +45,18 @@ def _handler(self, data): def worker_start(self): self.pppp = app.svc.get("pppp") + self.api_id = id(self.pppp._api) + self.pppp.handlers.append(self._handler) self.api_start_live() def worker_run(self, timeout): if not self.pppp.connected: - raise ConnectionError("No pppp connection") + raise ServiceRestartSignal("No pppp connection") - self.idle(timeout=timeout) + if id(self.pppp._api) != self.api_id: + raise ServiceRestartSignal("New pppp connection detected, restarting video feed") def worker_stop(self): self.api_stop_live() From 4e7a7ed9d07d1719e704f180048f06a5879258de Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:58:47 +0200 Subject: [PATCH 106/405] * Suppress ConnectionError in VideoQueue.worker_stop(). Otherwise, we risk a deadlock, or very slow shutdown. --- web/service/video.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/service/video.py b/web/service/video.py index 8a9f5f60..0cae7a8c 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -59,7 +59,11 @@ def worker_run(self, timeout): raise ServiceRestartSignal("New pppp connection detected, restarting video feed") def worker_stop(self): - self.api_stop_live() + try: + self.api_stop_live() + except ConnectionError: + pass + self.pppp.handlers.remove(self._handler) app.svc.put("pppp") From 46013b05a2957c7fd8825a842ae0e0853f413d7f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Sun, 30 Apr 2023 23:59:30 +0200 Subject: [PATCH 107/405] * Be sure to call self.idle() in VideoQueue.worker_run(), to not use 100% cpu for this thread. --- web/service/video.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/service/video.py b/web/service/video.py index 0cae7a8c..2b73ff6d 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -52,6 +52,8 @@ def worker_start(self): self.api_start_live() def worker_run(self, timeout): + self.idle(timeout=timeout) + if not self.pppp.connected: raise ServiceRestartSignal("No pppp connection") From 4a0d1135a72dab6abfb7ea832bc8d627c287c776 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:00:05 +0200 Subject: [PATCH 108/405] * Make VideoQueue service save light / video resolution state when set, and recover these when restarting worker --- web/service/video.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/service/video.py b/web/service/video.py index 2b73ff6d..d353c612 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -22,11 +22,13 @@ def api_stop_live(self): self.pppp.api_command(P2PSubCmdType.CLOSE_LIVE) def api_light_state(self, light): + self.saved_light_state = light self.pppp.api_command(P2PSubCmdType.LIGHT_STATE_SWITCH, data={ "open": light, }) def api_video_mode(self, mode): + self.saved_video_mode = mode self.pppp.api_command(P2PSubCmdType.LIVE_MODE_SET, data={ "mode": mode }) @@ -42,6 +44,10 @@ def _handler(self, data): self.notify(msg) + def worker_init(self): + self.saved_light_state = None + self.saved_video_mode = None + def worker_start(self): self.pppp = app.svc.get("pppp") @@ -51,6 +57,12 @@ def worker_start(self): self.api_start_live() + if self.saved_light_state is not None: + self.api_light_state(self.saved_light_state) + + if self.saved_video_mode is not None: + self.api_video_mode(self.saved_video_mode) + def worker_run(self, timeout): self.idle(timeout=timeout) From a05a0d624a79d65ce16afc828e81eb93ff44a8d5 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:05:48 +0200 Subject: [PATCH 109/405] * Avoid wrapping ConnectionResetError in AnkerPPPPApi.poll(), since the inner function now return a reasonable exception type --- libflagship/ppppapi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index f85fcf9f..bf3ce7cd 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -444,8 +444,6 @@ def poll(self, timeout=None): self.process(msg) except TimeoutError: pass - except ConnectionResetError: - raise ConnectionRefusedError("Connection rejected by device") for idx, ch in enumerate(self.chans): for pkt in ch.poll(): From 7fd85880af2791702d8925c51d59bb947a1c4e01 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:16:34 +0200 Subject: [PATCH 110/405] + Put in a 1-second delay after ServiceRestartSignal, to allow other services to react to restarted services --- web/lib/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/service.py b/web/lib/service.py index ac128e7b..5397c8f7 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -108,7 +108,7 @@ def _attempt_run(self): self.worker_run(timeout=0.3) except ServiceRestartSignal: log.info(f"{self.name}: Service requested restart.") - self._holdoff.reset() + self._holdoff.reset(delay=1) self.state = RunState.Stopping except Exception: log.exception(f"{self.name}: Unexpected exception while running worker") From 0576e0e3d966a0acebaff5ced303222d0403966e Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:17:22 +0200 Subject: [PATCH 111/405] + Implement FileTransferService, which depends on PPPPService, and shares the pppp connection to upload files to the printer --- web/service/filetransfer.py | 73 +++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 web/service/filetransfer.py diff --git a/web/service/filetransfer.py b/web/service/filetransfer.py new file mode 100644 index 00000000..0bad8471 --- /dev/null +++ b/web/service/filetransfer.py @@ -0,0 +1,73 @@ +import uuid +import logging as log + +from multiprocessing import Queue + +from ..lib.service import Service +from .. import app + +from libflagship.pppp import P2PCmdType, Aabb, FileTransfer +from libflagship.ppppapi import FileUploadInfo, PPPPError + +import cli.mqtt +import cli.util + + +class FileTransferService(Service): + + def api_aabb(self, api, frametype, msg=b"", pos=0): + api.send_aabb(msg, frametype=frametype, pos=pos) + + def api_aabb_request(self, api, frametype, msg=b"", pos=0): + self.api_aabb(api, frametype, msg, pos) + resp = self._tap.get() + log.debug(f"{self.name}: Aabb response: {resp}") + + def send_file(self, fd, user_name): + api = self.pppp._api + data = fd.read() + fui = FileUploadInfo.from_data(data, fd.filename, user_name=user_name, user_id="-", machine_id="-") + log.info(f"Going to upload {fui.size} bytes as {fui.name!r}") + try: + log.info("Requesting file transfer..") + api.send_xzyh(str(uuid.uuid4())[:16].encode(), cmd=P2PCmdType.P2P_SEND_FILE) + + log.info("Sending file metadata..") + self.api_aabb(api, FileTransfer.BEGIN, bytes(fui) + b"\x00") + + log.info("Sending file contents..") + blocksize = 1024 * 32 + chunks = cli.util.split_chunks(data, blocksize) + pos = 0 + + for chunk in chunks: + self.api_aabb_request(api, FileTransfer.DATA, chunk, pos) + pos += len(chunk) + + log.info("File upload complete. Requesting print start of job.") + + self.api_aabb_request(api, FileTransfer.END) + except PPPPError as E: + log.error(f"Could not send print job: {E}") + else: + log.info("Successfully sent print job") + + def handler(self, data): + chan, msg = data + if isinstance(msg, Aabb): + self._tap.put(msg) + + def worker_start(self): + self.pppp = app.svc.get("pppp") + self._tap = Queue() + + self.pppp.handlers.append(self.handler) + + def worker_run(self, timeout): + self.idle(timeout=timeout) + + def worker_stop(self): + self.pppp.handlers.remove(self.handler) + del self._tap + + app.svc.put("pppp") From 4c64c5820791431e565ccf7304e3af6babd3b451 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:19:03 +0200 Subject: [PATCH 112/405] * Reimplement /api/files/local endpoint in terms of FileTransferService --- web/__init__.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index be390529..c5d5d8ba 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -27,6 +27,7 @@ import web.service.pppp import web.service.video import web.service.mqtt +import web.service.filetransfer @app.before_first_request @@ -34,6 +35,7 @@ def startup(): app.svc.register("pppp", web.service.pppp.PPPPService()) app.svc.register("videoqueue", web.service.video.VideoQueue()) app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) + app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) @sock.route("/ws/mqtt") @@ -107,8 +109,6 @@ def app_api_version(): @app.post("/api/files/local") def app_api_files_local(): - config = app.config["config"] - user_name = request.headers.get("User-Agent", "ankerctl").split("/")[0] no_act = not cli.util.parse_http_bool(request.form["print"]) @@ -118,21 +118,8 @@ def app_api_files_local(): fd = request.files["file"] - api = cli.pppp.pppp_open(config, dumpfile=app.config.get("pppp_dump")) - - data = fd.read() - fui = FileUploadInfo.from_data(data, fd.filename, user_name=user_name, user_id="-", machine_id="-") - log.info(f"Going to upload {fui.size} bytes as {fui.name!r}") - try: - cli.pppp.pppp_send_file(api, fui, data) - log.info("File upload complete. Requesting print start of job.") - api.aabb_request(b"", frametype=FileTransfer.END) - except PPPPError as E: - log.error(f"Could not send print job: {E}") - else: - log.info("Successfully sent print job") - finally: - api.stop() + with app.svc.borrow("filetransfer") as ft: + ft.send_file(fd, user_name) return {} From 324839c515b467b7a279f86f945e1744b312f990 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:19:33 +0200 Subject: [PATCH 113/405] * Limit rx/tx debug logging to 128 characters, to avoid drowning in log spam when streaming video --- libflagship/ppppapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libflagship/ppppapi.py b/libflagship/ppppapi.py index bf3ce7cd..b4332aef 100644 --- a/libflagship/ppppapi.py +++ b/libflagship/ppppapi.py @@ -351,7 +351,7 @@ def recv(self, timeout=None): if self.dumper: self.dumper.rx(data, self.addr) msg = Message.parse(data)[0] - log.debug(f"RX <-- {msg}") + log.debug(f"RX <-- {str(msg)[:128]}") return msg def send(self, pkt, addr=None): @@ -362,7 +362,7 @@ def send(self, pkt, addr=None): if self.dumper: self.dumper.tx(resp, self.addr) msg = Message.parse(resp)[0] - log.debug(f"TX --> {msg}") + log.debug(f"TX --> {str(msg)[:128]}") self.sock.sendto(resp, addr or self.addr) def send_xzyh(self, data, cmd, chan=0, unk0=0, unk1=0, sign_code=0, unk3=0, dev_type=0, block=True): From c4f41d16f154fc74d570bb7559908cedb6d13463 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Mon, 1 May 2023 00:37:46 +0200 Subject: [PATCH 114/405] * Fixed and simplified /video route (non-websocket video download endpoint) --- web/__init__.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index c5d5d8ba..cea1275d 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -69,20 +69,11 @@ def ctrl(sock): @app.get("/video") -def video2(): +def video_download(): def generate(): - queue = Queue() - app.videoq.add_target(queue) - try: - while True: - try: - data = queue.get() - except EOFError: - break - yield data - finally: - app.videoq.del_target(queue) + for msg in app.svc.stream("videoqueue"): + yield msg.data return Response(generate(), mimetype='video/mp4') From 3aabda3711cbe77afc02d7fc2801fd294f7bfe34 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Thu, 4 May 2023 23:59:09 +0200 Subject: [PATCH 115/405] * Disable autopep8 for flask circular imports --- web/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/__init__.py b/web/__init__.py index cea1275d..b109ff12 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -24,10 +24,12 @@ sock = Sock(app) +# autopep8: off import web.service.pppp import web.service.video import web.service.mqtt import web.service.filetransfer +# autopep8: on @app.before_first_request From 51eaf4c40f8e27bf0c66efd20d9994219e752d2f Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:06:50 +0200 Subject: [PATCH 116/405] * Fixed mistake in Holdoff.reset(), which did not take delay parameter into account. --- web/lib/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index 5397c8f7..39cd6308 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -14,9 +14,9 @@ def __init__(self): self.deadline = None def reset(self, delay=None): - if delay: - delay = timedelta(seconds=delay) self.deadline = datetime.now() + if delay: + self.deadline += timedelta(seconds=delay) @property def passed(self): From a27797e851c8c5fbbd6cb2ff9795b2b6dd57974e Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:16:02 +0200 Subject: [PATCH 117/405] * Update flask version to avoid risk of CVE-2023-30861 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bd2e4e93..75175e5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,5 @@ git+https://github.com/chrivers/transwarp.git tinyec==0.4.0 crcmod==1.7 tqdm==4.65.0 -flask==2.2.0 +flask==2.2.5 flask-sock==0.6.0 From e0ce831ab3eddae86c46c9a6b1aea0a82e6d45fe Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:22:08 +0200 Subject: [PATCH 118/405] + Add safeguard in ServiceManager.unregister() against removing services that are in use --- web/lib/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index 39cd6308..d9980325 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -255,6 +255,9 @@ def unregister(self, name: str): if name not in self.svcs: raise KeyError(f"Trying to unregister unknown service {name!r}") + if self.refs[name]: + raise ServiceError(f"Trying to unregister service {name!r} with {self.refs[name]} reference(s)") + del self.svcs[name] del self.refs[name] From ab0d9cefa0c2e034015163a306e2104a6933cb01 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:26:01 +0200 Subject: [PATCH 119/405] + Make ServiceManager iterable, for a convenient way to enumerate registered services --- web/lib/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index d9980325..feda7eb9 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -218,6 +218,9 @@ def __init__(self): self.refs = {} atexit.register(self.atexit) + def __iter__(self): + return iter(self.svcs) + def atexit(self): log.debug("ServiceManager: Shutting down threads..") self.dump() From 4f0756b3c99424201cd8bb3780f449693267b029 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:30:19 +0200 Subject: [PATCH 120/405] + Implement __contains__ for ServiceManager --- web/lib/service.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/lib/service.py b/web/lib/service.py index feda7eb9..d24a450a 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -221,6 +221,9 @@ def __init__(self): def __iter__(self): return iter(self.svcs) + def __contains__(self, name): + return name in self.svcs + def atexit(self): log.debug("ServiceManager: Shutting down threads..") self.dump() @@ -248,14 +251,14 @@ def dump(self): log.debug(f" [{ref:>4}] {name:20} running={svc.running} state={svc.state} wanted={svc.wanted}") def register(self, name: str, svc: Service): - if name in self.svcs: + if name in self: raise KeyError(f"Trying to register {name!r} as {svc} while already taken by {self.svcs[name]}") self.svcs[name] = svc self.refs[name] = 0 def unregister(self, name: str): - if name not in self.svcs: + if name not in self: raise KeyError(f"Trying to unregister unknown service {name!r}") if self.refs[name]: @@ -265,7 +268,7 @@ def unregister(self, name: str): del self.refs[name] def get(self, name: str, ready=True) -> Service: - if name not in self.svcs: + if name not in self: raise KeyError(f"Requested unknown service {name!r}") svc = self.svcs[name] @@ -282,7 +285,7 @@ def get(self, name: str, ready=True) -> Service: return svc def put(self, name: str): - if name not in self.svcs: + if name not in self: raise KeyError(f"Requested unknown service {name!r}") svc = self.svcs[name] From d9c631b652158f25e69b91b88becbf8724734d58 Mon Sep 17 00:00:00 2001 From: Christian Iversen Date: Tue, 9 May 2023 13:38:34 +0200 Subject: [PATCH 121/405] + Implemented .restart() function for Service class --- web/lib/service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/lib/service.py b/web/lib/service.py index d24a450a..fb863c6e 100644 --- a/web/lib/service.py +++ b/web/lib/service.py @@ -74,6 +74,15 @@ def stop(self): self.wanted = False self._event.set() + def restart(self): + log.info(f"{self.name}: Requesting restart") + wanted = self.wanted + self.stop() + self.await_stopped() + if wanted: + self.start() + self.await_ready() + def shutdown(self): if self.state != RunState.Stopped: self.stop() From e781390772f79f17c991c515be2591e8ec0ffafa Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Mon, 24 Apr 2023 19:21:10 -0700 Subject: [PATCH 122/405] Improving interface for front-end --- static/index.html | 121 +++++++++++++++++++++++++++++++--------------- web/__init__.py | 10 ++++ 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/static/index.html b/static/index.html index 38ae248e..e936e594 100644 --- a/static/index.html +++ b/static/index.html @@ -13,49 +13,92 @@

ankerctl

Congratulations on running ankerctl

-
-
-
- -
-
-
-
-
-
-
-
-
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-

Connecting PrusaSlicer/SuperSlicer

-
    -
  1. Go to "Printer settings" tab
  2. -
  3. Click "Gear"⚙️ (Add/Edit Physical Printer)
  4. -
  5. Fill in "Descriptive name for the printer" with whatever you like
  6. -
  7. Select your Ankermake printer profile from the dropdown menu
  8. -
  9. Under "Host Type" select "OctoPrint"
  10. -
  11. In "Hostname, IP or URL" fill in with: {{ requestHost }}:{{ requestPort }} 📋
  12. -
  13. Leave "API Key / Password" blank
  14. -
  15. Click "Test" to validate input
  16. -
  17. Click "Ok" to close the window
  18. -
  19. Slice your file as normal
  20. -
  21. Click "G>" button to start direct upload to the printer
  22. -
  23. Click "Upload and print" - the "Upload"-only is not supported
  24. -
  25. Enjoy printing with ankerctl 😉
  26. -
-
Hint: Ctrl+Shift+G will slice and open the upload to printer window +
+
+
+
+

Connecting PrusaSlicer/SuperSlicer

+
    +
  1. Go to "Printer settings" tab
  2. +
  3. Click "Gear"⚙️ (Add/Edit Physical Printer)
  4. +
  5. Fill in "Descriptive name for the printer" with whatever you like
  6. +
  7. Select your Ankermake printer instructions from the dropdown menu
  8. +
  9. Under "Host Type" select "OctoPrint"
  10. +
  11. In "Hostname, IP or URL" fill in with: {{ requestHost }}:{{ requestPort }} 📋
  12. +
  13. Leave "API Key / Password" blank
  14. +
  15. Click "Test" to validate input
  16. +
  17. Click "Ok" to close the window
  18. +
  19. Slice your file as normal
  20. +
  21. Click "G>" button to start direct upload to the printer
  22. +
  23. Click "Upload and print" - the "Upload"-only is not supported
  24. +
  25. Enjoy printing with ankerctl 😉
  26. +
+
Hint: Ctrl+Shift+G will slice and open the upload to printer window +
+
+ step 1 + step 2 + step 3 + step 4 +
+
-
- step 1 - step 2 - step 3 - step 4 +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ + + {{ ankerConfig }} + + +
+
diff --git a/web/__init__.py b/web/__init__.py index b109ff12..f9f694a3 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -98,6 +98,16 @@ def app_api_version(): "server": "1.9.0", "text": "OctoPrint 1.9.0" } + + +@app.post('/api/config/upload') +def app_api_config_upload(): + config = app.config["config"] + + if request.method == 'POST': + f = request.files['loginFile'] + f.save(f.filename) + return 'Login uploaded successfully' @app.post("/api/files/local") From b53e4b09e6a1fd923f886823ca712dd91853b040 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Tue, 25 Apr 2023 01:05:52 -0700 Subject: [PATCH 123/405] Handling config loading when not configured --- ankerctl.py | 10 ++++++---- requirements.txt | 3 +++ static/index.html | 11 ++++++----- web/__init__.py | 49 ++++++++++++++++++++++++++++++++++++----------- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/ankerctl.py b/ankerctl.py index c1eb1dd8..e5344174 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -32,10 +32,13 @@ class Environment: def __init__(self): pass - def require_config(self): + def require_config(self, required=True): with self.config.open() as config: if not getattr(config, 'printers', False): - log.critical("No printers found in config. Please import configuration using 'config import'") + if not required: + log.info("No printers found in config. Please upload configuration using web browser") + else: + log.critical("No printers found in config. Please import configuration using 'config import'") def upgrade_config_if_needed(self): try: @@ -452,7 +455,7 @@ def config_show(env): @main.group("webserver", help="Built-in webserver support") @pass_env def webserver(env): - env.require_config() + env.require_config(False) @webserver.command("run", help="Run ankerctl webserver") @@ -460,7 +463,6 @@ def webserver(env): @click.option("--port", default=4470, envvar="FLASK_PORT", help="Port to bind to") @pass_env def webserver(env, host, port): - env.require_config() web.webserver(env.config, host, port, pppp_dump=env.pppp_dump) diff --git a/requirements.txt b/requirements.txt index 75175e5f..5a2dc3d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,6 @@ crcmod==1.7 tqdm==4.65.0 flask==2.2.5 flask-sock==0.6.0 +ua-parser==0.10.0 +user-agents==2.2.0 +pyyaml==5.4.1 diff --git a/static/index.html b/static/index.html index e936e594..1ef2dcd4 100644 --- a/static/index.html +++ b/static/index.html @@ -16,18 +16,18 @@

ankerctl

-
+
@@ -77,17 +77,18 @@

Connecting PrusaSlicer/SuperSlicer

-
+
- +
+ Location: {{ loginFilePath }}
diff --git a/web/__init__.py b/web/__init__.py index f9f694a3..6136f64b 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,3 +1,4 @@ +import re import json import logging as log @@ -10,7 +11,17 @@ from web.lib.service import ServiceManager +from user_agents import parse + import cli.util +import cli.config + + +def dict_match(dict, match): + for key in dict: + if re.match(rf"^{key}.*", match): + return key + app = Flask( __name__, @@ -34,10 +45,11 @@ @app.before_first_request def startup(): - app.svc.register("pppp", web.service.pppp.PPPPService()) - app.svc.register("videoqueue", web.service.video.VideoQueue()) - app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) - app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) + if app.config["login"]: + app.svc.register("pppp", web.service.pppp.PPPPService()) + app.svc.register("videoqueue", web.service.video.VideoQueue()) + app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) + app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) @sock.route("/ws/mqtt") @@ -82,12 +94,24 @@ def generate(): @app.get("/") def app_root(): + config = app.config["config"] + user_agent = parse(request.headers.get('User-Agent')) + login_path = { + 'Mac OS': '~/Library/Application Support/AnkerMake/AnkerMake_64bit_fp/login.json', + 'Windows': r'%LOCALAPPDATA%\Ankermake\AnkerMake_64bit_fp\login.json', + 'None': 'Unsupported OS, supply path to login.json', + } + useros = dict_match(login_path, user_agent.os.family) + host = request.host.split(':') requestPort = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 return render_template( "index.html", requestPort=requestPort, requestHost=host[0] + configure=app.config["login"], + loginFilePath=login_path[useros] if useros in login_path else login_path["None"], + ankerConfig=str(config.open()) if app.config["login"] else 'No config found...', ) @@ -98,12 +122,13 @@ def app_api_version(): "server": "1.9.0", "text": "OctoPrint 1.9.0" } - + @app.post('/api/config/upload') def app_api_config_upload(): config = app.config["config"] - + ua = request.headers.get('User-Agent') + if request.method == 'POST': f = request.files['loginFile'] f.save(f.filename) @@ -128,8 +153,10 @@ def app_api_files_local(): def webserver(config, host, port, **kwargs): - app.config["config"] = config - app.config["port"] = port - app.config["host"] = host - app.config.update(kwargs) - app.run(host=host, port=port) + with config.open() as cfg: + app.config["config"] = config + app.config["login"] = True if cfg else False + app.config["port"] = port + app.config["host"] = host + app.config.update(kwargs) + app.run(host=host, port=port) From 3062ea61c6a43f2ce41bbc41c2b3f7230b9a835f Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Tue, 25 Apr 2023 01:13:04 -0700 Subject: [PATCH 124/405] Changing error message --- static/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/index.html b/static/index.html index 1ef2dcd4..033fc3ad 100644 --- a/static/index.html +++ b/static/index.html @@ -30,6 +30,7 @@

ankerctl

+ {% if configure %}
@@ -43,6 +44,11 @@

ankerctl

+ {% else %} +
+ No printer configuration found, upload config in Setup +
+ {% endif %}
From df4196c8fb52ec4b49a8f3ffd528b10b12bc99ba Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Tue, 25 Apr 2023 05:15:17 -0700 Subject: [PATCH 125/405] Interface improvements --- static/ankersrv.js | 10 +++++++++- static/index.html | 47 ++++++++++++++++++++++++++++++++++------------ web/__init__.py | 40 +++++++++++++++++++++++++++++++-------- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/static/ankersrv.js b/static/ankersrv.js index d4b4065f..1b62f48e 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -56,9 +56,17 @@ $(function () { return false; }); - $('#configData').on('click',function(){ navigator.clipboard.writeText("{{ configHost }}:{{ configPort }}"); + console.log('Copied {{ configHost }}:{{ configPort }} to clipboard...') return false; }); + + $('#copyFilePath').on('click',function(){ + navigator.clipboard.writeText("{{ loginFilePath }}"); + console.log('Copied {{ loginFilePath }} to clipboard...') + return false; + }) + + $(".alert").alert() }); diff --git a/static/index.html b/static/index.html index 033fc3ad..777f899b 100644 --- a/static/index.html +++ b/static/index.html @@ -5,6 +5,7 @@ ankerctl +
@@ -16,21 +17,23 @@

ankerctl

+ {% if configure %}
- {% if configure %}
@@ -44,14 +47,17 @@

ankerctl

- {% else %} -
- No printer configuration found, upload config in Setup +
+ {% else %} +
+
+ No printer configuration found, upload config in Setup
- {% endif %}
+ {% endif %}
+ {% if configure %}
@@ -83,25 +89,42 @@

Connecting PrusaSlicer/SuperSlicer

+ {% endif %}
-
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
- +
- Location: {{ loginFilePath }} + Location: {{ loginFilePath }} 
- {{ ankerConfig }} + {{ ankerConfig }}
diff --git a/web/__init__.py b/web/__init__.py index 6136f64b..f619c182 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,9 +1,14 @@ +import os import re import json +import tempfile import logging as log -from flask import Flask, request, render_template, Response +from secrets import token_urlsafe as token + +from flask import Flask, flash, request, redirect, url_for, render_template, Response from flask_sock import Sock +from werkzeug.utils import secure_filename from libflagship.pppp import P2PSubCmdType, FileTransfer from libflagship.ppppapi import FileUploadInfo, PPPPError @@ -29,6 +34,7 @@ def dict_match(dict, match): static_folder="static", template_folder="static" ) +app.secret_key = token(24) app.config.from_prefixed_env() app.svc = ServiceManager() @@ -124,15 +130,33 @@ def app_api_version(): } +def allowed_file(filename, ALLOWED_EXTENSIONS): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + @app.post('/api/config/upload') def app_api_config_upload(): - config = app.config["config"] - ua = request.headers.get('User-Agent') - - if request.method == 'POST': - f = request.files['loginFile'] - f.save(f.filename) - return 'Login uploaded successfully' + ALLOWED_EXTENSIONS = set(['json']) + with tempfile.TemporaryDirectory(prefix='ankerctl_') as tmpdir: + config = app.config["config"] + + if request.method == 'POST': + if 'loginFile' not in request.files: + flash('No file found', 'error') + return redirect('/') + file = request.files['loginFile'] + if file.filename == '': + flash('No file selected', 'error') + return redirect('/') + if file and allowed_file(file.filename, ALLOWED_EXTENSIONS): + filename = secure_filename(file.filename) + filepath = os.path.join(tmpdir, filename) + file.save(filepath) + flash(f'Login file uploaded to {filepath}', 'success') + return redirect('/') + elif file and not allowed_file(file.filename, ALLOWED_EXTENSIONS): + flash(f'File must be of type: {str(ALLOWED_EXTENSIONS)}', 'warning') + return redirect('/') @app.post("/api/files/local") From 1782753e706220bce6347840a572219e81835189 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Tue, 25 Apr 2023 08:57:25 -0700 Subject: [PATCH 126/405] Pushing alerts improvements --- static/index.html | 46 ++++++++++++++++++++++++++++++---------------- web/__init__.py | 4 ++-- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/static/index.html b/static/index.html index 777f899b..731ad253 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@ ankerctl - +
@@ -29,6 +29,35 @@

ankerctl

+
+ + + + + + + + + + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
@@ -95,21 +124,6 @@

Connecting PrusaSlicer/SuperSlicer

-
- {% with messages = get_flashed_messages(with_categories=true) %} - - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} -
diff --git a/web/__init__.py b/web/__init__.py index f619c182..769f091a 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -142,11 +142,11 @@ def app_api_config_upload(): if request.method == 'POST': if 'loginFile' not in request.files: - flash('No file found', 'error') + flash('No file found', 'danger') return redirect('/') file = request.files['loginFile'] if file.filename == '': - flash('No file selected', 'error') + flash('No file selected', 'danger') return redirect('/') if file and allowed_file(file.filename, ALLOWED_EXTENSIONS): filename = secure_filename(file.filename) From 822f2640ddce92e01509cebb85519205e05aa6b3 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Wed, 26 Apr 2023 21:53:41 -0700 Subject: [PATCH 127/405] Adding in config import and reload of webserver --- web/__init__.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 769f091a..2ca517ca 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -10,6 +10,9 @@ from flask_sock import Sock from werkzeug.utils import secure_filename +import libflagship.httpapi +import libflagship.logincache + from libflagship.pppp import P2PSubCmdType, FileTransfer from libflagship.ppppapi import FileUploadInfo, PPPPError @@ -134,6 +137,35 @@ def allowed_file(filename, ALLOWED_EXTENSIONS): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS +def config_import(loginFile): + file = open(loginFile, 'r') + config = app.config["config"] + + # extract auth token + cache = libflagship.logincache.load(file.read())["data"] + auth_token = cache["auth_token"] + + # extract account region + region = libflagship.logincache.guess_region(cache["ab_code"]) + + try: + newConfig = cli.config.load_config_from_api(auth_token, region, False) + except libflagship.httpapi.APIError as E: + flash(f"Config import failed: {E} (auth token might be expired: make sure Ankermake Slicer can connect, then try again", 'danger') + return redirect('/') + except Exception as E: + flash(f"Config import failed: {E}", 'danger') + return redirect('/') + + try: + config.save("default", newConfig) + except Exception as E: + flash(f"Config import failed: {E}", 'danger') + return redirect('/') + flash("Configuration imported! Restarting...", 'success') + return redirect('/') + + @app.post('/api/config/upload') def app_api_config_upload(): ALLOWED_EXTENSIONS = set(['json']) @@ -152,8 +184,8 @@ def app_api_config_upload(): filename = secure_filename(file.filename) filepath = os.path.join(tmpdir, filename) file.save(filepath) - flash(f'Login file uploaded to {filepath}', 'success') - return redirect('/') + config_import(filepath) + return redirect('/reload') elif file and not allowed_file(file.filename, ALLOWED_EXTENSIONS): flash(f'File must be of type: {str(ALLOWED_EXTENSIONS)}', 'warning') return redirect('/') @@ -176,6 +208,18 @@ def app_api_files_local(): return {} +@app.get("/reload") +def reload_webserver(): + config = app.config["config"] + with config.open() as cfg: + app.config["config"] = config + app.config["login"] = True if cfg else False + if cfg: + flash('Configuration loaded', 'success') + startup() + return redirect('/') + + def webserver(config, host, port, **kwargs): with config.open() as cfg: app.config["config"] = config From a8a59b7f58b8e86e19bbd5954477cce35f91a5a4 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Wed, 26 Apr 2023 22:26:19 -0700 Subject: [PATCH 128/405] Updating icon sizes and reload function --- static/index.html | 23 ++++------------------- web/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/static/index.html b/static/index.html index 731ad253..ed44863c 100644 --- a/static/index.html +++ b/static/index.html @@ -30,29 +30,14 @@

ankerctl

- - - - - - - - - - - - - - {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} -
- - - - diff --git a/web/__init__.py b/web/__init__.py index 6b4fff04..6a74f9fc 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -120,7 +120,7 @@ def app_root(): requestHost=host[0] configure=app.config["login"], loginFilePath=login_path[useros] if useros in login_path else login_path["None"], - ankerConfig=str(config.open()) if app.config["login"] else 'No config found...', + ankerConfig=str(config_show()) if app.config["login"] else '

No printers found, please load your login config...

', ) @@ -137,6 +137,28 @@ def allowed_file(filename, ALLOWED_EXTENSIONS): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS +def config_show(): + config = app.config["config"] + with config.open() as cfg: + config_output = f'''

Account:

+ user_id: {cfg.account.user_id[:10]}...[REDACTED]
+ auth_token: {cfg.account.auth_token[:10]}...[REDACTED]
+ email: {cfg.account.email}
+ region: {cfg.account.region.upper()}

+ duid: {p.p2p_duid}
+ sn: {p.sn}
+ ip: {p.ip_addr}
+ wifi_mac: {cli.util.pretty_mac(p.wifi_mac)}
+ api_hosts: {', '.join(p.api_hosts)}
+ p2p_hosts: {', '.join(p.p2p_hosts)}

+ ''' + return config_output + + def config_import(loginFile): file = open(loginFile, 'r') config = app.config["config"] From cec69aba789350c712fbf6ac1249a60eab741f06 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 27 Apr 2023 00:20:54 -0700 Subject: [PATCH 130/405] Removing unused clipboard copy function --- static/index.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/index.html b/static/index.html index 0ef83c50..21e372e4 100644 --- a/static/index.html +++ b/static/index.html @@ -12,10 +12,6 @@ - - - - - + + +
@@ -40,75 +19,79 @@

ankerctl

Congratulations on running ankerctl

- -
- {% with messages = get_flashed_messages(with_categories=true) %} - - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} -
-
-
-
- {% if configure %} -
-
- -
-
-
-
-
-
-
-
-
-
-
-
- {% else %} -
-
- No printer configuration found, upload config in Setup -
-
- {% endif %} -
-
- {% if configure %} -
-
-
-
-

Connecting PrusaSlicer/SuperSlicer

-
    -
  1. Go to "Printer settings" tab
  2. -
  3. Click "Gear"⚙️ (Add/Edit Physical Printer)
  4. -
  5. Fill in "Descriptive name for the printer" with whatever you like
  6. -
  7. Select your Ankermake printer instructions from the dropdown menu
  8. -
  9. Under "Host Type" select "OctoPrint"
  10. -
  11. In "Hostname, IP or URL" fill in with: {{ requestHost }}:{{ requestPort }} 📋
  12. + +
    + {% with messages = get_flashed_messages(with_categories=true) %} + + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
    +
    +
    +
    + {% if configure %} +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + {% else %} +
    +
    + No printer configuration found, upload config in Setup +
    +
    + {% endif %} +
    +
    + {% if configure %} +
    +
    +
    +
    +

    Connecting PrusaSlicer/SuperSlicer

    +
      +
    1. Go to "Printer settings" tab
    2. +
    3. Click "Gear"⚙️ (Add/Edit Physical Printer)
    4. +
    5. Fill in "Descriptive name for the printer" with whatever you like
    6. +
    7. Select your Ankermake printer instructions from the dropdown menu
    8. +
    9. Under "Host Type" select "OctoPrint"
    10. +
    11. In "Hostname, IP or URL" fill in with: {{ request_host }}:{{ request_port }} 
    12. Leave "API Key / Password" blank
    13. Click "Test" to validate input
    14. Click "Ok" to close the window
    15. @@ -133,14 +116,14 @@

      Connecting PrusaSlicer/SuperSlicer

      - +
      - +
      - Location: {{ loginFilePath }} 
      @@ -150,7 +133,7 @@

      Connecting PrusaSlicer/SuperSlicer

      {% autoescape false %} - {{ ankerConfig }} + {{ anker_config }} {% endautoescape %} diff --git a/web/__init__.py b/web/__init__.py index a666b7d1..a50ececc 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,42 +1,34 @@ -import os -import re import json import tempfile import logging as log from secrets import token_urlsafe as token - -from flask import Flask, flash, request, redirect, url_for, render_template, Response, session +from flask import Flask, request, render_template, Response, session, url_for from flask_sock import Sock -from werkzeug.utils import secure_filename - -import libflagship.httpapi -import libflagship.logincache - -from libflagship.pppp import P2PSubCmdType, FileTransfer -from libflagship.ppppapi import FileUploadInfo, PPPPError - +from user_agents import parse as user_agent_parse from web.lib.service import ServiceManager -from user_agents import parse +import web.config +import web.platform +import web.util + +import web.service.pppp +import web.service.video +import web.service.mqtt +import web.service.filetransfer import cli.util import cli.config -def dict_match(dict, match): - for key in dict: - if re.match(rf"^{key}.*", match): - return key - - app = Flask( __name__, root_path=".", static_folder="static", template_folder="static" ) +# secret_key is required for flash() to function app.secret_key = token(24) app.config.from_prefixed_env() app.svc = ServiceManager() @@ -54,53 +46,64 @@ def dict_match(dict, match): @app.before_first_request def startup(): - if app.config["login"]: - app.svc.register("pppp", web.service.pppp.PPPPService()) - app.svc.register("videoqueue", web.service.video.VideoQueue()) - app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) - app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) + app.svc.register("pppp", web.service.pppp.PPPPService()) + app.svc.register("videoqueue", web.service.video.VideoQueue()) + app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) + app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) + + +def shutdown(): + app.svc.unregister("pppp") + app.svc.unregister("videoqueue") + app.svc.unregister("mqttqueue") + app.svc.unregister("filetransfer") + + +def restart(): + shutdown() + startup() @sock.route("/ws/mqtt") def mqtt(sock): - - if app.config["login"]: - for data in app.svc.stream("mqttqueue"): - log.debug(f"MQTT message: {data}") - sock.send(json.dumps(data)) + if not app.config["login"]: + return + for data in app.svc.stream("mqttqueue"): + log.debug(f"MQTT message: {data}") + sock.send(json.dumps(data)) @sock.route("/ws/video") def video(sock): - - if app.config["login"]: - for msg in app.svc.stream("videoqueue"): - sock.send(msg.data) + if not app.config["login"]: + return + for msg in app.svc.stream("videoqueue"): + sock.send(msg.data) @sock.route("/ws/ctrl") def ctrl(sock): + if not app.config["login"]: + return + while True: + msg = json.loads(sock.receive()) - if app.config["login"]: - while True: - msg = json.loads(sock.receive()) - - if "light" in msg: - with app.svc.borrow("videoqueue") as vq: - vq.api_light_state(msg["light"]) + if "light" in msg: + with app.svc.borrow("videoqueue") as vq: + vq.api_light_state(msg["light"]) - if "quality" in msg: - with app.svc.borrow("videoqueue") as vq: - vq.api_video_mode(msg["quality"]) + if "quality" in msg: + with app.svc.borrow("videoqueue") as vq: + vq.api_video_mode(msg["quality"]) @app.get("/video") def video_download(): - def generate(): - if app.config["login"]: - for msg in app.svc.stream("videoqueue"): - yield msg.data + if not app.config["login"]: + return + for msg in app.svc.stream("videoqueue"): + yield msg.data return Response(generate(), mimetype='video/mp4') @@ -108,24 +111,22 @@ def generate(): @app.get("/") def app_root(): config = app.config["config"] - user_agent = parse(request.headers.get('User-Agent')) - login_path = { - 'Mac OS': '~/Library/Application Support/AnkerMake/AnkerMake_64bit_fp/login.json', - 'Windows': r'%LOCALAPPDATA%\Ankermake\AnkerMake_64bit_fp\login.json', - 'None': 'Unsupported OS, supply path to login.json', - } - useros = dict_match(login_path, user_agent.os.family) + with config.open() as cfg: + user_agent = user_agent_parse(request.headers.get('User-Agent')) - host = request.host.split(':') - requestPort = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 - return render_template( - "index.html", - requestPort=requestPort, - requestHost=host[0], - configure=app.config["login"], - loginFilePath=login_path[useros] if useros in login_path else login_path["None"], - ankerConfig=str(config_show()) if app.config["login"] else '

      No printers found, please load your login config...

      ', - ) + host = request.host.split(':') + request_port = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 + no_config = '

      No printers found, please load your login config...

      ' + anker_config = str(web.config.config_show(cfg)) if app.config["login"] else no_config + + return render_template( + "index.html", + request_port=request_port, + request_host=host[0], + configure=app.config["login"], + login_file_path=web.platform.login_path(web.platform.os_platform(user_agent.os.family)), + anker_config=anker_config + ) @app.get("/api/version") @@ -137,84 +138,19 @@ def app_api_version(): } -def allowed_file(filename, ALLOWED_EXTENSIONS): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - - -def config_show(): - config = app.config["config"] - with config.open() as cfg: - config_output = f'''

      Account:

      - user_id: {cfg.account.user_id[:10]}...[REDACTED]
      - auth_token: {cfg.account.auth_token[:10]}...[REDACTED]
      - email: {cfg.account.email}
      - region: {cfg.account.region.upper()}

      - duid: {p.p2p_duid}
      - sn: {p.sn}
      - ip: {p.ip_addr}
      - wifi_mac: {cli.util.pretty_mac(p.wifi_mac)}
      - api_hosts: {', '.join(p.api_hosts)}
      - p2p_hosts: {', '.join(p.p2p_hosts)}

      - ''' - return config_output - - -def config_import(loginFile): - file = open(loginFile, 'r') - config = app.config["config"] - - # extract auth token - cache = libflagship.logincache.load(file.read())["data"] - auth_token = cache["auth_token"] - - # extract account region - region = libflagship.logincache.guess_region(cache["ab_code"]) - - try: - newConfig = cli.config.load_config_from_api(auth_token, region, False) - except libflagship.httpapi.APIError as E: - flash(f"Config import failed: {E} (auth token might be expired: make sure Ankermake Slicer can connect, then try again", 'danger') - return redirect('/') - except Exception as E: - flash(f"Config import failed: {E}", 'danger') - return redirect('/') - - try: - config.save("default", newConfig) - except Exception as E: - flash(f"Config import failed: {E}", 'danger') - return redirect('/') - flash("Configuration imported! Restarting...", 'success') - return redirect('/') - - -@app.post('/api/config/upload') -def app_api_config_upload(): - ALLOWED_EXTENSIONS = set(['json']) +@app.post('/api/ankerctl/config/upload') +def app_api_ankerctl_config_upload(): with tempfile.TemporaryDirectory(prefix='ankerctl_') as tmpdir: - config = app.config["config"] - - if request.method == 'POST': - if 'loginFile' not in request.files: - flash('No file found', 'danger') - return redirect('/') - file = request.files['loginFile'] - if file.filename == '': - flash('No file selected', 'danger') - return redirect('/') - if file and allowed_file(file.filename, ALLOWED_EXTENSIONS): - filename = secure_filename(file.filename) - filepath = os.path.join(tmpdir, filename) - file.save(filepath) - config_import(filepath) - return redirect('/reload') - elif file and not allowed_file(file.filename, ALLOWED_EXTENSIONS): - flash(f'File must be of type: {str(ALLOWED_EXTENSIONS)}', 'warning') - return redirect('/') + if request.method != 'POST': + return web.util.flash_redirect() + if 'login_file' not in request.files: + return web.util.flash_redirect('No file found', 'danger') + file = request.files['login_file'] + if file.filename == '': + return web.util.flash_redirect('No file uploaded', 'danger') + else: + web.config.config_import(file, app.config['config']) + return web.util.flash_redirect(path = '/reload') @app.post("/api/files/local") @@ -238,13 +174,12 @@ def app_api_files_local(): def reload_webserver(): config = app.config["config"] with config.open() as cfg: - app.config["config"] = config - app.config["login"] = True if cfg else False - if cfg: - session['_flashes'].clear() - flash('Configuration loaded', 'success') - startup() - return redirect('/') + if not getattr(cfg, 'printers', False): + return web.util.flash_redirect('No printers found in config', 'warning') + app.config["login"] = True + session['_flashes'].clear() + restart() + return web.util.flash_redirect('Configuration Loaded', 'success') def webserver(config, host, port, **kwargs): diff --git a/web/service/filetransfer.py b/web/service/filetransfer.py index 0bad8471..b05f9f1d 100644 --- a/web/service/filetransfer.py +++ b/web/service/filetransfer.py @@ -4,7 +4,6 @@ from multiprocessing import Queue from ..lib.service import Service -from .. import app from libflagship.pppp import P2PCmdType, Aabb, FileTransfer from libflagship.ppppapi import FileUploadInfo, PPPPError @@ -58,6 +57,7 @@ def handler(self, data): self._tap.put(msg) def worker_start(self): + from web import app self.pppp = app.svc.get("pppp") self._tap = Queue() @@ -67,6 +67,7 @@ def worker_run(self, timeout): self.idle(timeout=timeout) def worker_stop(self): + from web import app self.pppp.handlers.remove(self.handler) del self._tap diff --git a/web/service/mqtt.py b/web/service/mqtt.py index 00c547da..3f780710 100644 --- a/web/service/mqtt.py +++ b/web/service/mqtt.py @@ -1,7 +1,6 @@ import logging as log from ..lib.service import Service -from .. import app from libflagship.util import enhex @@ -11,6 +10,7 @@ class MqttQueue(Service): def worker_start(self): + from web import app self.client = cli.mqtt.mqtt_open(app.config["config"], True) def worker_run(self, timeout): diff --git a/web/service/pppp.py b/web/service/pppp.py index 8ac251d8..99c5f21e 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta from ..lib.service import Service, ServiceRestartSignal -from .. import app from libflagship.pppp import P2PCmdType, PktClose, Duid, Type, Xzyh, Aabb from libflagship.ppppapi import AnkerPPPPAsyncApi, PPPPState @@ -26,6 +25,7 @@ def api_command(self, commandType, **kwargs): ) def worker_start(self): + from web import app config = app.config["config"] deadline = datetime.now() + timedelta(seconds=2) diff --git a/web/service/video.py b/web/service/video.py index d353c612..62c3ff9a 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -5,7 +5,6 @@ from multiprocessing import Queue from ..lib.service import Service, ServiceRestartSignal -from .. import app from libflagship.pppp import P2PSubCmdType, Xzyh @@ -49,6 +48,7 @@ def worker_init(self): self.saved_video_mode = None def worker_start(self): + from web import app self.pppp = app.svc.get("pppp") self.api_id = id(self.pppp._api) @@ -73,6 +73,7 @@ def worker_run(self, timeout): raise ServiceRestartSignal("New pppp connection detected, restarting video feed") def worker_stop(self): + from web import app try: self.api_stop_live() except ConnectionError: From de02f37cc9e48f76ec17fa406efbe3e86cb760dc Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Tue, 2 May 2023 02:00:09 -0700 Subject: [PATCH 135/405] Importing new files --- static/ankersrv.css | 21 + .../font/bootstrap-icons.json | 1955 +++++++++++++++++ .../font/bootstrap-icons.min.css | 5 + .../font/fonts/bootstrap-icons.woff | Bin 0 -> 164360 bytes .../font/fonts/bootstrap-icons.woff2 | Bin 0 -> 121340 bytes web/config.py | 53 + web/platform.py | 19 + web/util.py | 10 + 8 files changed, 2063 insertions(+) create mode 100644 static/ankersrv.css create mode 100644 static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json create mode 100644 static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.min.css create mode 100644 static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff create mode 100644 static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff2 create mode 100644 web/config.py create mode 100644 web/platform.py create mode 100644 web/util.py diff --git a/static/ankersrv.css b/static/ankersrv.css new file mode 100644 index 00000000..07189f49 --- /dev/null +++ b/static/ankersrv.css @@ -0,0 +1,21 @@ +.nav-link { + color: #88f387!important; +} + +.nav-link.active, .btn-primary { + color: #000000!important; + background-color: #88f387!important; + border:#41a03f!important; +} + +.nav-link:hover, .btn:hover { + color: #000000!important; + background-color: #adf3ac; + border-color: #41a03f; /*set the color you want here*/ +} + +pre, code { + color: #88f387; + background-color: #292929 +} + diff --git a/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json b/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json new file mode 100644 index 00000000..d85eaaf2 --- /dev/null +++ b/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json @@ -0,0 +1,1955 @@ +{ + "123": 63103, + "alarm-fill": 61697, + "alarm": 61698, + "align-bottom": 61699, + "align-center": 61700, + "align-end": 61701, + "align-middle": 61702, + "align-start": 61703, + "align-top": 61704, + "alt": 61705, + "app-indicator": 61706, + "app": 61707, + "archive-fill": 61708, + "archive": 61709, + "arrow-90deg-down": 61710, + "arrow-90deg-left": 61711, + "arrow-90deg-right": 61712, + "arrow-90deg-up": 61713, + "arrow-bar-down": 61714, + "arrow-bar-left": 61715, + "arrow-bar-right": 61716, + "arrow-bar-up": 61717, + "arrow-clockwise": 61718, + "arrow-counterclockwise": 61719, + "arrow-down-circle-fill": 61720, + "arrow-down-circle": 61721, + "arrow-down-left-circle-fill": 61722, + "arrow-down-left-circle": 61723, + "arrow-down-left-square-fill": 61724, + "arrow-down-left-square": 61725, + "arrow-down-left": 61726, + "arrow-down-right-circle-fill": 61727, + "arrow-down-right-circle": 61728, + "arrow-down-right-square-fill": 61729, + "arrow-down-right-square": 61730, + "arrow-down-right": 61731, + "arrow-down-short": 61732, + "arrow-down-square-fill": 61733, + "arrow-down-square": 61734, + "arrow-down-up": 61735, + "arrow-down": 61736, + "arrow-left-circle-fill": 61737, + "arrow-left-circle": 61738, + "arrow-left-right": 61739, + "arrow-left-short": 61740, + "arrow-left-square-fill": 61741, + "arrow-left-square": 61742, + "arrow-left": 61743, + "arrow-repeat": 61744, + "arrow-return-left": 61745, + "arrow-return-right": 61746, + "arrow-right-circle-fill": 61747, + "arrow-right-circle": 61748, + "arrow-right-short": 61749, + "arrow-right-square-fill": 61750, + "arrow-right-square": 61751, + "arrow-right": 61752, + "arrow-up-circle-fill": 61753, + "arrow-up-circle": 61754, + "arrow-up-left-circle-fill": 61755, + "arrow-up-left-circle": 61756, + "arrow-up-left-square-fill": 61757, + "arrow-up-left-square": 61758, + "arrow-up-left": 61759, + "arrow-up-right-circle-fill": 61760, + "arrow-up-right-circle": 61761, + "arrow-up-right-square-fill": 61762, + "arrow-up-right-square": 61763, + "arrow-up-right": 61764, + "arrow-up-short": 61765, + "arrow-up-square-fill": 61766, + "arrow-up-square": 61767, + "arrow-up": 61768, + "arrows-angle-contract": 61769, + "arrows-angle-expand": 61770, + "arrows-collapse": 61771, + "arrows-expand": 61772, + "arrows-fullscreen": 61773, + "arrows-move": 61774, + "aspect-ratio-fill": 61775, + "aspect-ratio": 61776, + "asterisk": 61777, + "at": 61778, + "award-fill": 61779, + "award": 61780, + "back": 61781, + "backspace-fill": 61782, + "backspace-reverse-fill": 61783, + "backspace-reverse": 61784, + "backspace": 61785, + "badge-3d-fill": 61786, + "badge-3d": 61787, + "badge-4k-fill": 61788, + "badge-4k": 61789, + "badge-8k-fill": 61790, + "badge-8k": 61791, + "badge-ad-fill": 61792, + "badge-ad": 61793, + "badge-ar-fill": 61794, + "badge-ar": 61795, + "badge-cc-fill": 61796, + "badge-cc": 61797, + "badge-hd-fill": 61798, + "badge-hd": 61799, + "badge-tm-fill": 61800, + "badge-tm": 61801, + "badge-vo-fill": 61802, + "badge-vo": 61803, + "badge-vr-fill": 61804, + "badge-vr": 61805, + "badge-wc-fill": 61806, + "badge-wc": 61807, + "bag-check-fill": 61808, + "bag-check": 61809, + "bag-dash-fill": 61810, + "bag-dash": 61811, + "bag-fill": 61812, + "bag-plus-fill": 61813, + "bag-plus": 61814, + "bag-x-fill": 61815, + "bag-x": 61816, + "bag": 61817, + "bar-chart-fill": 61818, + "bar-chart-line-fill": 61819, + "bar-chart-line": 61820, + "bar-chart-steps": 61821, + "bar-chart": 61822, + "basket-fill": 61823, + "basket": 61824, + "basket2-fill": 61825, + "basket2": 61826, + "basket3-fill": 61827, + "basket3": 61828, + "battery-charging": 61829, + "battery-full": 61830, + "battery-half": 61831, + "battery": 61832, + "bell-fill": 61833, + "bell": 61834, + "bezier": 61835, + "bezier2": 61836, + "bicycle": 61837, + "binoculars-fill": 61838, + "binoculars": 61839, + "blockquote-left": 61840, + "blockquote-right": 61841, + "book-fill": 61842, + "book-half": 61843, + "book": 61844, + "bookmark-check-fill": 61845, + "bookmark-check": 61846, + "bookmark-dash-fill": 61847, + "bookmark-dash": 61848, + "bookmark-fill": 61849, + "bookmark-heart-fill": 61850, + "bookmark-heart": 61851, + "bookmark-plus-fill": 61852, + "bookmark-plus": 61853, + "bookmark-star-fill": 61854, + "bookmark-star": 61855, + "bookmark-x-fill": 61856, + "bookmark-x": 61857, + "bookmark": 61858, + "bookmarks-fill": 61859, + "bookmarks": 61860, + "bookshelf": 61861, + "bootstrap-fill": 61862, + "bootstrap-reboot": 61863, + "bootstrap": 61864, + "border-all": 61865, + "border-bottom": 61866, + "border-center": 61867, + "border-inner": 61868, + "border-left": 61869, + "border-middle": 61870, + "border-outer": 61871, + "border-right": 61872, + "border-style": 61873, + "border-top": 61874, + "border-width": 61875, + "border": 61876, + "bounding-box-circles": 61877, + "bounding-box": 61878, + "box-arrow-down-left": 61879, + "box-arrow-down-right": 61880, + "box-arrow-down": 61881, + "box-arrow-in-down-left": 61882, + "box-arrow-in-down-right": 61883, + "box-arrow-in-down": 61884, + "box-arrow-in-left": 61885, + "box-arrow-in-right": 61886, + "box-arrow-in-up-left": 61887, + "box-arrow-in-up-right": 61888, + "box-arrow-in-up": 61889, + "box-arrow-left": 61890, + "box-arrow-right": 61891, + "box-arrow-up-left": 61892, + "box-arrow-up-right": 61893, + "box-arrow-up": 61894, + "box-seam": 61895, + "box": 61896, + "braces": 61897, + "bricks": 61898, + "briefcase-fill": 61899, + "briefcase": 61900, + "brightness-alt-high-fill": 61901, + "brightness-alt-high": 61902, + "brightness-alt-low-fill": 61903, + "brightness-alt-low": 61904, + "brightness-high-fill": 61905, + "brightness-high": 61906, + "brightness-low-fill": 61907, + "brightness-low": 61908, + "broadcast-pin": 61909, + "broadcast": 61910, + "brush-fill": 61911, + "brush": 61912, + "bucket-fill": 61913, + "bucket": 61914, + "bug-fill": 61915, + "bug": 61916, + "building": 61917, + "bullseye": 61918, + "calculator-fill": 61919, + "calculator": 61920, + "calendar-check-fill": 61921, + "calendar-check": 61922, + "calendar-date-fill": 61923, + "calendar-date": 61924, + "calendar-day-fill": 61925, + "calendar-day": 61926, + "calendar-event-fill": 61927, + "calendar-event": 61928, + "calendar-fill": 61929, + "calendar-minus-fill": 61930, + "calendar-minus": 61931, + "calendar-month-fill": 61932, + "calendar-month": 61933, + "calendar-plus-fill": 61934, + "calendar-plus": 61935, + "calendar-range-fill": 61936, + "calendar-range": 61937, + "calendar-week-fill": 61938, + "calendar-week": 61939, + "calendar-x-fill": 61940, + "calendar-x": 61941, + "calendar": 61942, + "calendar2-check-fill": 61943, + "calendar2-check": 61944, + "calendar2-date-fill": 61945, + "calendar2-date": 61946, + "calendar2-day-fill": 61947, + "calendar2-day": 61948, + "calendar2-event-fill": 61949, + "calendar2-event": 61950, + "calendar2-fill": 61951, + "calendar2-minus-fill": 61952, + "calendar2-minus": 61953, + "calendar2-month-fill": 61954, + "calendar2-month": 61955, + "calendar2-plus-fill": 61956, + "calendar2-plus": 61957, + "calendar2-range-fill": 61958, + "calendar2-range": 61959, + "calendar2-week-fill": 61960, + "calendar2-week": 61961, + "calendar2-x-fill": 61962, + "calendar2-x": 61963, + "calendar2": 61964, + "calendar3-event-fill": 61965, + "calendar3-event": 61966, + "calendar3-fill": 61967, + "calendar3-range-fill": 61968, + "calendar3-range": 61969, + "calendar3-week-fill": 61970, + "calendar3-week": 61971, + "calendar3": 61972, + "calendar4-event": 61973, + "calendar4-range": 61974, + "calendar4-week": 61975, + "calendar4": 61976, + "camera-fill": 61977, + "camera-reels-fill": 61978, + "camera-reels": 61979, + "camera-video-fill": 61980, + "camera-video-off-fill": 61981, + "camera-video-off": 61982, + "camera-video": 61983, + "camera": 61984, + "camera2": 61985, + "capslock-fill": 61986, + "capslock": 61987, + "card-checklist": 61988, + "card-heading": 61989, + "card-image": 61990, + "card-list": 61991, + "card-text": 61992, + "caret-down-fill": 61993, + "caret-down-square-fill": 61994, + "caret-down-square": 61995, + "caret-down": 61996, + "caret-left-fill": 61997, + "caret-left-square-fill": 61998, + "caret-left-square": 61999, + "caret-left": 62000, + "caret-right-fill": 62001, + "caret-right-square-fill": 62002, + "caret-right-square": 62003, + "caret-right": 62004, + "caret-up-fill": 62005, + "caret-up-square-fill": 62006, + "caret-up-square": 62007, + "caret-up": 62008, + "cart-check-fill": 62009, + "cart-check": 62010, + "cart-dash-fill": 62011, + "cart-dash": 62012, + "cart-fill": 62013, + "cart-plus-fill": 62014, + "cart-plus": 62015, + "cart-x-fill": 62016, + "cart-x": 62017, + "cart": 62018, + "cart2": 62019, + "cart3": 62020, + "cart4": 62021, + "cash-stack": 62022, + "cash": 62023, + "cast": 62024, + "chat-dots-fill": 62025, + "chat-dots": 62026, + "chat-fill": 62027, + "chat-left-dots-fill": 62028, + "chat-left-dots": 62029, + "chat-left-fill": 62030, + "chat-left-quote-fill": 62031, + "chat-left-quote": 62032, + "chat-left-text-fill": 62033, + "chat-left-text": 62034, + "chat-left": 62035, + "chat-quote-fill": 62036, + "chat-quote": 62037, + "chat-right-dots-fill": 62038, + "chat-right-dots": 62039, + "chat-right-fill": 62040, + "chat-right-quote-fill": 62041, + "chat-right-quote": 62042, + "chat-right-text-fill": 62043, + "chat-right-text": 62044, + "chat-right": 62045, + "chat-square-dots-fill": 62046, + "chat-square-dots": 62047, + "chat-square-fill": 62048, + "chat-square-quote-fill": 62049, + "chat-square-quote": 62050, + "chat-square-text-fill": 62051, + "chat-square-text": 62052, + "chat-square": 62053, + "chat-text-fill": 62054, + "chat-text": 62055, + "chat": 62056, + "check-all": 62057, + "check-circle-fill": 62058, + "check-circle": 62059, + "check-square-fill": 62060, + "check-square": 62061, + "check": 62062, + "check2-all": 62063, + "check2-circle": 62064, + "check2-square": 62065, + "check2": 62066, + "chevron-bar-contract": 62067, + "chevron-bar-down": 62068, + "chevron-bar-expand": 62069, + "chevron-bar-left": 62070, + "chevron-bar-right": 62071, + "chevron-bar-up": 62072, + "chevron-compact-down": 62073, + "chevron-compact-left": 62074, + "chevron-compact-right": 62075, + "chevron-compact-up": 62076, + "chevron-contract": 62077, + "chevron-double-down": 62078, + "chevron-double-left": 62079, + "chevron-double-right": 62080, + "chevron-double-up": 62081, + "chevron-down": 62082, + "chevron-expand": 62083, + "chevron-left": 62084, + "chevron-right": 62085, + "chevron-up": 62086, + "circle-fill": 62087, + "circle-half": 62088, + "circle-square": 62089, + "circle": 62090, + "clipboard-check": 62091, + "clipboard-data": 62092, + "clipboard-minus": 62093, + "clipboard-plus": 62094, + "clipboard-x": 62095, + "clipboard": 62096, + "clock-fill": 62097, + "clock-history": 62098, + "clock": 62099, + "cloud-arrow-down-fill": 62100, + "cloud-arrow-down": 62101, + "cloud-arrow-up-fill": 62102, + "cloud-arrow-up": 62103, + "cloud-check-fill": 62104, + "cloud-check": 62105, + "cloud-download-fill": 62106, + "cloud-download": 62107, + "cloud-drizzle-fill": 62108, + "cloud-drizzle": 62109, + "cloud-fill": 62110, + "cloud-fog-fill": 62111, + "cloud-fog": 62112, + "cloud-fog2-fill": 62113, + "cloud-fog2": 62114, + "cloud-hail-fill": 62115, + "cloud-hail": 62116, + "cloud-haze-fill": 62118, + "cloud-haze": 62119, + "cloud-haze2-fill": 62120, + "cloud-lightning-fill": 62121, + "cloud-lightning-rain-fill": 62122, + "cloud-lightning-rain": 62123, + "cloud-lightning": 62124, + "cloud-minus-fill": 62125, + "cloud-minus": 62126, + "cloud-moon-fill": 62127, + "cloud-moon": 62128, + "cloud-plus-fill": 62129, + "cloud-plus": 62130, + "cloud-rain-fill": 62131, + "cloud-rain-heavy-fill": 62132, + "cloud-rain-heavy": 62133, + "cloud-rain": 62134, + "cloud-slash-fill": 62135, + "cloud-slash": 62136, + "cloud-sleet-fill": 62137, + "cloud-sleet": 62138, + "cloud-snow-fill": 62139, + "cloud-snow": 62140, + "cloud-sun-fill": 62141, + "cloud-sun": 62142, + "cloud-upload-fill": 62143, + "cloud-upload": 62144, + "cloud": 62145, + "clouds-fill": 62146, + "clouds": 62147, + "cloudy-fill": 62148, + "cloudy": 62149, + "code-slash": 62150, + "code-square": 62151, + "code": 62152, + "collection-fill": 62153, + "collection-play-fill": 62154, + "collection-play": 62155, + "collection": 62156, + "columns-gap": 62157, + "columns": 62158, + "command": 62159, + "compass-fill": 62160, + "compass": 62161, + "cone-striped": 62162, + "cone": 62163, + "controller": 62164, + "cpu-fill": 62165, + "cpu": 62166, + "credit-card-2-back-fill": 62167, + "credit-card-2-back": 62168, + "credit-card-2-front-fill": 62169, + "credit-card-2-front": 62170, + "credit-card-fill": 62171, + "credit-card": 62172, + "crop": 62173, + "cup-fill": 62174, + "cup-straw": 62175, + "cup": 62176, + "cursor-fill": 62177, + "cursor-text": 62178, + "cursor": 62179, + "dash-circle-dotted": 62180, + "dash-circle-fill": 62181, + "dash-circle": 62182, + "dash-square-dotted": 62183, + "dash-square-fill": 62184, + "dash-square": 62185, + "dash": 62186, + "diagram-2-fill": 62187, + "diagram-2": 62188, + "diagram-3-fill": 62189, + "diagram-3": 62190, + "diamond-fill": 62191, + "diamond-half": 62192, + "diamond": 62193, + "dice-1-fill": 62194, + "dice-1": 62195, + "dice-2-fill": 62196, + "dice-2": 62197, + "dice-3-fill": 62198, + "dice-3": 62199, + "dice-4-fill": 62200, + "dice-4": 62201, + "dice-5-fill": 62202, + "dice-5": 62203, + "dice-6-fill": 62204, + "dice-6": 62205, + "disc-fill": 62206, + "disc": 62207, + "discord": 62208, + "display-fill": 62209, + "display": 62210, + "distribute-horizontal": 62211, + "distribute-vertical": 62212, + "door-closed-fill": 62213, + "door-closed": 62214, + "door-open-fill": 62215, + "door-open": 62216, + "dot": 62217, + "download": 62218, + "droplet-fill": 62219, + "droplet-half": 62220, + "droplet": 62221, + "earbuds": 62222, + "easel-fill": 62223, + "easel": 62224, + "egg-fill": 62225, + "egg-fried": 62226, + "egg": 62227, + "eject-fill": 62228, + "eject": 62229, + "emoji-angry-fill": 62230, + "emoji-angry": 62231, + "emoji-dizzy-fill": 62232, + "emoji-dizzy": 62233, + "emoji-expressionless-fill": 62234, + "emoji-expressionless": 62235, + "emoji-frown-fill": 62236, + "emoji-frown": 62237, + "emoji-heart-eyes-fill": 62238, + "emoji-heart-eyes": 62239, + "emoji-laughing-fill": 62240, + "emoji-laughing": 62241, + "emoji-neutral-fill": 62242, + "emoji-neutral": 62243, + "emoji-smile-fill": 62244, + "emoji-smile-upside-down-fill": 62245, + "emoji-smile-upside-down": 62246, + "emoji-smile": 62247, + "emoji-sunglasses-fill": 62248, + "emoji-sunglasses": 62249, + "emoji-wink-fill": 62250, + "emoji-wink": 62251, + "envelope-fill": 62252, + "envelope-open-fill": 62253, + "envelope-open": 62254, + "envelope": 62255, + "eraser-fill": 62256, + "eraser": 62257, + "exclamation-circle-fill": 62258, + "exclamation-circle": 62259, + "exclamation-diamond-fill": 62260, + "exclamation-diamond": 62261, + "exclamation-octagon-fill": 62262, + "exclamation-octagon": 62263, + "exclamation-square-fill": 62264, + "exclamation-square": 62265, + "exclamation-triangle-fill": 62266, + "exclamation-triangle": 62267, + "exclamation": 62268, + "exclude": 62269, + "eye-fill": 62270, + "eye-slash-fill": 62271, + "eye-slash": 62272, + "eye": 62273, + "eyedropper": 62274, + "eyeglasses": 62275, + "facebook": 62276, + "file-arrow-down-fill": 62277, + "file-arrow-down": 62278, + "file-arrow-up-fill": 62279, + "file-arrow-up": 62280, + "file-bar-graph-fill": 62281, + "file-bar-graph": 62282, + "file-binary-fill": 62283, + "file-binary": 62284, + "file-break-fill": 62285, + "file-break": 62286, + "file-check-fill": 62287, + "file-check": 62288, + "file-code-fill": 62289, + "file-code": 62290, + "file-diff-fill": 62291, + "file-diff": 62292, + "file-earmark-arrow-down-fill": 62293, + "file-earmark-arrow-down": 62294, + "file-earmark-arrow-up-fill": 62295, + "file-earmark-arrow-up": 62296, + "file-earmark-bar-graph-fill": 62297, + "file-earmark-bar-graph": 62298, + "file-earmark-binary-fill": 62299, + "file-earmark-binary": 62300, + "file-earmark-break-fill": 62301, + "file-earmark-break": 62302, + "file-earmark-check-fill": 62303, + "file-earmark-check": 62304, + "file-earmark-code-fill": 62305, + "file-earmark-code": 62306, + "file-earmark-diff-fill": 62307, + "file-earmark-diff": 62308, + "file-earmark-easel-fill": 62309, + "file-earmark-easel": 62310, + "file-earmark-excel-fill": 62311, + "file-earmark-excel": 62312, + "file-earmark-fill": 62313, + "file-earmark-font-fill": 62314, + "file-earmark-font": 62315, + "file-earmark-image-fill": 62316, + "file-earmark-image": 62317, + "file-earmark-lock-fill": 62318, + "file-earmark-lock": 62319, + "file-earmark-lock2-fill": 62320, + "file-earmark-lock2": 62321, + "file-earmark-medical-fill": 62322, + "file-earmark-medical": 62323, + "file-earmark-minus-fill": 62324, + "file-earmark-minus": 62325, + "file-earmark-music-fill": 62326, + "file-earmark-music": 62327, + "file-earmark-person-fill": 62328, + "file-earmark-person": 62329, + "file-earmark-play-fill": 62330, + "file-earmark-play": 62331, + "file-earmark-plus-fill": 62332, + "file-earmark-plus": 62333, + "file-earmark-post-fill": 62334, + "file-earmark-post": 62335, + "file-earmark-ppt-fill": 62336, + "file-earmark-ppt": 62337, + "file-earmark-richtext-fill": 62338, + "file-earmark-richtext": 62339, + "file-earmark-ruled-fill": 62340, + "file-earmark-ruled": 62341, + "file-earmark-slides-fill": 62342, + "file-earmark-slides": 62343, + "file-earmark-spreadsheet-fill": 62344, + "file-earmark-spreadsheet": 62345, + "file-earmark-text-fill": 62346, + "file-earmark-text": 62347, + "file-earmark-word-fill": 62348, + "file-earmark-word": 62349, + "file-earmark-x-fill": 62350, + "file-earmark-x": 62351, + "file-earmark-zip-fill": 62352, + "file-earmark-zip": 62353, + "file-earmark": 62354, + "file-easel-fill": 62355, + "file-easel": 62356, + "file-excel-fill": 62357, + "file-excel": 62358, + "file-fill": 62359, + "file-font-fill": 62360, + "file-font": 62361, + "file-image-fill": 62362, + "file-image": 62363, + "file-lock-fill": 62364, + "file-lock": 62365, + "file-lock2-fill": 62366, + "file-lock2": 62367, + "file-medical-fill": 62368, + "file-medical": 62369, + "file-minus-fill": 62370, + "file-minus": 62371, + "file-music-fill": 62372, + "file-music": 62373, + "file-person-fill": 62374, + "file-person": 62375, + "file-play-fill": 62376, + "file-play": 62377, + "file-plus-fill": 62378, + "file-plus": 62379, + "file-post-fill": 62380, + "file-post": 62381, + "file-ppt-fill": 62382, + "file-ppt": 62383, + "file-richtext-fill": 62384, + "file-richtext": 62385, + "file-ruled-fill": 62386, + "file-ruled": 62387, + "file-slides-fill": 62388, + "file-slides": 62389, + "file-spreadsheet-fill": 62390, + "file-spreadsheet": 62391, + "file-text-fill": 62392, + "file-text": 62393, + "file-word-fill": 62394, + "file-word": 62395, + "file-x-fill": 62396, + "file-x": 62397, + "file-zip-fill": 62398, + "file-zip": 62399, + "file": 62400, + "files-alt": 62401, + "files": 62402, + "film": 62403, + "filter-circle-fill": 62404, + "filter-circle": 62405, + "filter-left": 62406, + "filter-right": 62407, + "filter-square-fill": 62408, + "filter-square": 62409, + "filter": 62410, + "flag-fill": 62411, + "flag": 62412, + "flower1": 62413, + "flower2": 62414, + "flower3": 62415, + "folder-check": 62416, + "folder-fill": 62417, + "folder-minus": 62418, + "folder-plus": 62419, + "folder-symlink-fill": 62420, + "folder-symlink": 62421, + "folder-x": 62422, + "folder": 62423, + "folder2-open": 62424, + "folder2": 62425, + "fonts": 62426, + "forward-fill": 62427, + "forward": 62428, + "front": 62429, + "fullscreen-exit": 62430, + "fullscreen": 62431, + "funnel-fill": 62432, + "funnel": 62433, + "gear-fill": 62434, + "gear-wide-connected": 62435, + "gear-wide": 62436, + "gear": 62437, + "gem": 62438, + "geo-alt-fill": 62439, + "geo-alt": 62440, + "geo-fill": 62441, + "geo": 62442, + "gift-fill": 62443, + "gift": 62444, + "github": 62445, + "globe": 62446, + "globe2": 62447, + "google": 62448, + "graph-down": 62449, + "graph-up": 62450, + "grid-1x2-fill": 62451, + "grid-1x2": 62452, + "grid-3x2-gap-fill": 62453, + "grid-3x2-gap": 62454, + "grid-3x2": 62455, + "grid-3x3-gap-fill": 62456, + "grid-3x3-gap": 62457, + "grid-3x3": 62458, + "grid-fill": 62459, + "grid": 62460, + "grip-horizontal": 62461, + "grip-vertical": 62462, + "hammer": 62463, + "hand-index-fill": 62464, + "hand-index-thumb-fill": 62465, + "hand-index-thumb": 62466, + "hand-index": 62467, + "hand-thumbs-down-fill": 62468, + "hand-thumbs-down": 62469, + "hand-thumbs-up-fill": 62470, + "hand-thumbs-up": 62471, + "handbag-fill": 62472, + "handbag": 62473, + "hash": 62474, + "hdd-fill": 62475, + "hdd-network-fill": 62476, + "hdd-network": 62477, + "hdd-rack-fill": 62478, + "hdd-rack": 62479, + "hdd-stack-fill": 62480, + "hdd-stack": 62481, + "hdd": 62482, + "headphones": 62483, + "headset": 62484, + "heart-fill": 62485, + "heart-half": 62486, + "heart": 62487, + "heptagon-fill": 62488, + "heptagon-half": 62489, + "heptagon": 62490, + "hexagon-fill": 62491, + "hexagon-half": 62492, + "hexagon": 62493, + "hourglass-bottom": 62494, + "hourglass-split": 62495, + "hourglass-top": 62496, + "hourglass": 62497, + "house-door-fill": 62498, + "house-door": 62499, + "house-fill": 62500, + "house": 62501, + "hr": 62502, + "hurricane": 62503, + "image-alt": 62504, + "image-fill": 62505, + "image": 62506, + "images": 62507, + "inbox-fill": 62508, + "inbox": 62509, + "inboxes-fill": 62510, + "inboxes": 62511, + "info-circle-fill": 62512, + "info-circle": 62513, + "info-square-fill": 62514, + "info-square": 62515, + "info": 62516, + "input-cursor-text": 62517, + "input-cursor": 62518, + "instagram": 62519, + "intersect": 62520, + "journal-album": 62521, + "journal-arrow-down": 62522, + "journal-arrow-up": 62523, + "journal-bookmark-fill": 62524, + "journal-bookmark": 62525, + "journal-check": 62526, + "journal-code": 62527, + "journal-medical": 62528, + "journal-minus": 62529, + "journal-plus": 62530, + "journal-richtext": 62531, + "journal-text": 62532, + "journal-x": 62533, + "journal": 62534, + "journals": 62535, + "joystick": 62536, + "justify-left": 62537, + "justify-right": 62538, + "justify": 62539, + "kanban-fill": 62540, + "kanban": 62541, + "key-fill": 62542, + "key": 62543, + "keyboard-fill": 62544, + "keyboard": 62545, + "ladder": 62546, + "lamp-fill": 62547, + "lamp": 62548, + "laptop-fill": 62549, + "laptop": 62550, + "layer-backward": 62551, + "layer-forward": 62552, + "layers-fill": 62553, + "layers-half": 62554, + "layers": 62555, + "layout-sidebar-inset-reverse": 62556, + "layout-sidebar-inset": 62557, + "layout-sidebar-reverse": 62558, + "layout-sidebar": 62559, + "layout-split": 62560, + "layout-text-sidebar-reverse": 62561, + "layout-text-sidebar": 62562, + "layout-text-window-reverse": 62563, + "layout-text-window": 62564, + "layout-three-columns": 62565, + "layout-wtf": 62566, + "life-preserver": 62567, + "lightbulb-fill": 62568, + "lightbulb-off-fill": 62569, + "lightbulb-off": 62570, + "lightbulb": 62571, + "lightning-charge-fill": 62572, + "lightning-charge": 62573, + "lightning-fill": 62574, + "lightning": 62575, + "link-45deg": 62576, + "link": 62577, + "linkedin": 62578, + "list-check": 62579, + "list-nested": 62580, + "list-ol": 62581, + "list-stars": 62582, + "list-task": 62583, + "list-ul": 62584, + "list": 62585, + "lock-fill": 62586, + "lock": 62587, + "mailbox": 62588, + "mailbox2": 62589, + "map-fill": 62590, + "map": 62591, + "markdown-fill": 62592, + "markdown": 62593, + "mask": 62594, + "megaphone-fill": 62595, + "megaphone": 62596, + "menu-app-fill": 62597, + "menu-app": 62598, + "menu-button-fill": 62599, + "menu-button-wide-fill": 62600, + "menu-button-wide": 62601, + "menu-button": 62602, + "menu-down": 62603, + "menu-up": 62604, + "mic-fill": 62605, + "mic-mute-fill": 62606, + "mic-mute": 62607, + "mic": 62608, + "minecart-loaded": 62609, + "minecart": 62610, + "moisture": 62611, + "moon-fill": 62612, + "moon-stars-fill": 62613, + "moon-stars": 62614, + "moon": 62615, + "mouse-fill": 62616, + "mouse": 62617, + "mouse2-fill": 62618, + "mouse2": 62619, + "mouse3-fill": 62620, + "mouse3": 62621, + "music-note-beamed": 62622, + "music-note-list": 62623, + "music-note": 62624, + "music-player-fill": 62625, + "music-player": 62626, + "newspaper": 62627, + "node-minus-fill": 62628, + "node-minus": 62629, + "node-plus-fill": 62630, + "node-plus": 62631, + "nut-fill": 62632, + "nut": 62633, + "octagon-fill": 62634, + "octagon-half": 62635, + "octagon": 62636, + "option": 62637, + "outlet": 62638, + "paint-bucket": 62639, + "palette-fill": 62640, + "palette": 62641, + "palette2": 62642, + "paperclip": 62643, + "paragraph": 62644, + "patch-check-fill": 62645, + "patch-check": 62646, + "patch-exclamation-fill": 62647, + "patch-exclamation": 62648, + "patch-minus-fill": 62649, + "patch-minus": 62650, + "patch-plus-fill": 62651, + "patch-plus": 62652, + "patch-question-fill": 62653, + "patch-question": 62654, + "pause-btn-fill": 62655, + "pause-btn": 62656, + "pause-circle-fill": 62657, + "pause-circle": 62658, + "pause-fill": 62659, + "pause": 62660, + "peace-fill": 62661, + "peace": 62662, + "pen-fill": 62663, + "pen": 62664, + "pencil-fill": 62665, + "pencil-square": 62666, + "pencil": 62667, + "pentagon-fill": 62668, + "pentagon-half": 62669, + "pentagon": 62670, + "people-fill": 62671, + "people": 62672, + "percent": 62673, + "person-badge-fill": 62674, + "person-badge": 62675, + "person-bounding-box": 62676, + "person-check-fill": 62677, + "person-check": 62678, + "person-circle": 62679, + "person-dash-fill": 62680, + "person-dash": 62681, + "person-fill": 62682, + "person-lines-fill": 62683, + "person-plus-fill": 62684, + "person-plus": 62685, + "person-square": 62686, + "person-x-fill": 62687, + "person-x": 62688, + "person": 62689, + "phone-fill": 62690, + "phone-landscape-fill": 62691, + "phone-landscape": 62692, + "phone-vibrate-fill": 62693, + "phone-vibrate": 62694, + "phone": 62695, + "pie-chart-fill": 62696, + "pie-chart": 62697, + "pin-angle-fill": 62698, + "pin-angle": 62699, + "pin-fill": 62700, + "pin": 62701, + "pip-fill": 62702, + "pip": 62703, + "play-btn-fill": 62704, + "play-btn": 62705, + "play-circle-fill": 62706, + "play-circle": 62707, + "play-fill": 62708, + "play": 62709, + "plug-fill": 62710, + "plug": 62711, + "plus-circle-dotted": 62712, + "plus-circle-fill": 62713, + "plus-circle": 62714, + "plus-square-dotted": 62715, + "plus-square-fill": 62716, + "plus-square": 62717, + "plus": 62718, + "power": 62719, + "printer-fill": 62720, + "printer": 62721, + "puzzle-fill": 62722, + "puzzle": 62723, + "question-circle-fill": 62724, + "question-circle": 62725, + "question-diamond-fill": 62726, + "question-diamond": 62727, + "question-octagon-fill": 62728, + "question-octagon": 62729, + "question-square-fill": 62730, + "question-square": 62731, + "question": 62732, + "rainbow": 62733, + "receipt-cutoff": 62734, + "receipt": 62735, + "reception-0": 62736, + "reception-1": 62737, + "reception-2": 62738, + "reception-3": 62739, + "reception-4": 62740, + "record-btn-fill": 62741, + "record-btn": 62742, + "record-circle-fill": 62743, + "record-circle": 62744, + "record-fill": 62745, + "record": 62746, + "record2-fill": 62747, + "record2": 62748, + "reply-all-fill": 62749, + "reply-all": 62750, + "reply-fill": 62751, + "reply": 62752, + "rss-fill": 62753, + "rss": 62754, + "rulers": 62755, + "save-fill": 62756, + "save": 62757, + "save2-fill": 62758, + "save2": 62759, + "scissors": 62760, + "screwdriver": 62761, + "search": 62762, + "segmented-nav": 62763, + "server": 62764, + "share-fill": 62765, + "share": 62766, + "shield-check": 62767, + "shield-exclamation": 62768, + "shield-fill-check": 62769, + "shield-fill-exclamation": 62770, + "shield-fill-minus": 62771, + "shield-fill-plus": 62772, + "shield-fill-x": 62773, + "shield-fill": 62774, + "shield-lock-fill": 62775, + "shield-lock": 62776, + "shield-minus": 62777, + "shield-plus": 62778, + "shield-shaded": 62779, + "shield-slash-fill": 62780, + "shield-slash": 62781, + "shield-x": 62782, + "shield": 62783, + "shift-fill": 62784, + "shift": 62785, + "shop-window": 62786, + "shop": 62787, + "shuffle": 62788, + "signpost-2-fill": 62789, + "signpost-2": 62790, + "signpost-fill": 62791, + "signpost-split-fill": 62792, + "signpost-split": 62793, + "signpost": 62794, + "sim-fill": 62795, + "sim": 62796, + "skip-backward-btn-fill": 62797, + "skip-backward-btn": 62798, + "skip-backward-circle-fill": 62799, + "skip-backward-circle": 62800, + "skip-backward-fill": 62801, + "skip-backward": 62802, + "skip-end-btn-fill": 62803, + "skip-end-btn": 62804, + "skip-end-circle-fill": 62805, + "skip-end-circle": 62806, + "skip-end-fill": 62807, + "skip-end": 62808, + "skip-forward-btn-fill": 62809, + "skip-forward-btn": 62810, + "skip-forward-circle-fill": 62811, + "skip-forward-circle": 62812, + "skip-forward-fill": 62813, + "skip-forward": 62814, + "skip-start-btn-fill": 62815, + "skip-start-btn": 62816, + "skip-start-circle-fill": 62817, + "skip-start-circle": 62818, + "skip-start-fill": 62819, + "skip-start": 62820, + "slack": 62821, + "slash-circle-fill": 62822, + "slash-circle": 62823, + "slash-square-fill": 62824, + "slash-square": 62825, + "slash": 62826, + "sliders": 62827, + "smartwatch": 62828, + "snow": 62829, + "snow2": 62830, + "snow3": 62831, + "sort-alpha-down-alt": 62832, + "sort-alpha-down": 62833, + "sort-alpha-up-alt": 62834, + "sort-alpha-up": 62835, + "sort-down-alt": 62836, + "sort-down": 62837, + "sort-numeric-down-alt": 62838, + "sort-numeric-down": 62839, + "sort-numeric-up-alt": 62840, + "sort-numeric-up": 62841, + "sort-up-alt": 62842, + "sort-up": 62843, + "soundwave": 62844, + "speaker-fill": 62845, + "speaker": 62846, + "speedometer": 62847, + "speedometer2": 62848, + "spellcheck": 62849, + "square-fill": 62850, + "square-half": 62851, + "square": 62852, + "stack": 62853, + "star-fill": 62854, + "star-half": 62855, + "star": 62856, + "stars": 62857, + "stickies-fill": 62858, + "stickies": 62859, + "sticky-fill": 62860, + "sticky": 62861, + "stop-btn-fill": 62862, + "stop-btn": 62863, + "stop-circle-fill": 62864, + "stop-circle": 62865, + "stop-fill": 62866, + "stop": 62867, + "stoplights-fill": 62868, + "stoplights": 62869, + "stopwatch-fill": 62870, + "stopwatch": 62871, + "subtract": 62872, + "suit-club-fill": 62873, + "suit-club": 62874, + "suit-diamond-fill": 62875, + "suit-diamond": 62876, + "suit-heart-fill": 62877, + "suit-heart": 62878, + "suit-spade-fill": 62879, + "suit-spade": 62880, + "sun-fill": 62881, + "sun": 62882, + "sunglasses": 62883, + "sunrise-fill": 62884, + "sunrise": 62885, + "sunset-fill": 62886, + "sunset": 62887, + "symmetry-horizontal": 62888, + "symmetry-vertical": 62889, + "table": 62890, + "tablet-fill": 62891, + "tablet-landscape-fill": 62892, + "tablet-landscape": 62893, + "tablet": 62894, + "tag-fill": 62895, + "tag": 62896, + "tags-fill": 62897, + "tags": 62898, + "telegram": 62899, + "telephone-fill": 62900, + "telephone-forward-fill": 62901, + "telephone-forward": 62902, + "telephone-inbound-fill": 62903, + "telephone-inbound": 62904, + "telephone-minus-fill": 62905, + "telephone-minus": 62906, + "telephone-outbound-fill": 62907, + "telephone-outbound": 62908, + "telephone-plus-fill": 62909, + "telephone-plus": 62910, + "telephone-x-fill": 62911, + "telephone-x": 62912, + "telephone": 62913, + "terminal-fill": 62914, + "terminal": 62915, + "text-center": 62916, + "text-indent-left": 62917, + "text-indent-right": 62918, + "text-left": 62919, + "text-paragraph": 62920, + "text-right": 62921, + "textarea-resize": 62922, + "textarea-t": 62923, + "textarea": 62924, + "thermometer-half": 62925, + "thermometer-high": 62926, + "thermometer-low": 62927, + "thermometer-snow": 62928, + "thermometer-sun": 62929, + "thermometer": 62930, + "three-dots-vertical": 62931, + "three-dots": 62932, + "toggle-off": 62933, + "toggle-on": 62934, + "toggle2-off": 62935, + "toggle2-on": 62936, + "toggles": 62937, + "toggles2": 62938, + "tools": 62939, + "tornado": 62940, + "trash-fill": 62941, + "trash": 62942, + "trash2-fill": 62943, + "trash2": 62944, + "tree-fill": 62945, + "tree": 62946, + "triangle-fill": 62947, + "triangle-half": 62948, + "triangle": 62949, + "trophy-fill": 62950, + "trophy": 62951, + "tropical-storm": 62952, + "truck-flatbed": 62953, + "truck": 62954, + "tsunami": 62955, + "tv-fill": 62956, + "tv": 62957, + "twitch": 62958, + "twitter": 62959, + "type-bold": 62960, + "type-h1": 62961, + "type-h2": 62962, + "type-h3": 62963, + "type-italic": 62964, + "type-strikethrough": 62965, + "type-underline": 62966, + "type": 62967, + "ui-checks-grid": 62968, + "ui-checks": 62969, + "ui-radios-grid": 62970, + "ui-radios": 62971, + "umbrella-fill": 62972, + "umbrella": 62973, + "union": 62974, + "unlock-fill": 62975, + "unlock": 62976, + "upc-scan": 62977, + "upc": 62978, + "upload": 62979, + "vector-pen": 62980, + "view-list": 62981, + "view-stacked": 62982, + "vinyl-fill": 62983, + "vinyl": 62984, + "voicemail": 62985, + "volume-down-fill": 62986, + "volume-down": 62987, + "volume-mute-fill": 62988, + "volume-mute": 62989, + "volume-off-fill": 62990, + "volume-off": 62991, + "volume-up-fill": 62992, + "volume-up": 62993, + "vr": 62994, + "wallet-fill": 62995, + "wallet": 62996, + "wallet2": 62997, + "watch": 62998, + "water": 62999, + "whatsapp": 63000, + "wifi-1": 63001, + "wifi-2": 63002, + "wifi-off": 63003, + "wifi": 63004, + "wind": 63005, + "window-dock": 63006, + "window-sidebar": 63007, + "window": 63008, + "wrench": 63009, + "x-circle-fill": 63010, + "x-circle": 63011, + "x-diamond-fill": 63012, + "x-diamond": 63013, + "x-octagon-fill": 63014, + "x-octagon": 63015, + "x-square-fill": 63016, + "x-square": 63017, + "x": 63018, + "youtube": 63019, + "zoom-in": 63020, + "zoom-out": 63021, + "bank": 63022, + "bank2": 63023, + "bell-slash-fill": 63024, + "bell-slash": 63025, + "cash-coin": 63026, + "check-lg": 63027, + "coin": 63028, + "currency-bitcoin": 63029, + "currency-dollar": 63030, + "currency-euro": 63031, + "currency-exchange": 63032, + "currency-pound": 63033, + "currency-yen": 63034, + "dash-lg": 63035, + "exclamation-lg": 63036, + "file-earmark-pdf-fill": 63037, + "file-earmark-pdf": 63038, + "file-pdf-fill": 63039, + "file-pdf": 63040, + "gender-ambiguous": 63041, + "gender-female": 63042, + "gender-male": 63043, + "gender-trans": 63044, + "headset-vr": 63045, + "info-lg": 63046, + "mastodon": 63047, + "messenger": 63048, + "piggy-bank-fill": 63049, + "piggy-bank": 63050, + "pin-map-fill": 63051, + "pin-map": 63052, + "plus-lg": 63053, + "question-lg": 63054, + "recycle": 63055, + "reddit": 63056, + "safe-fill": 63057, + "safe2-fill": 63058, + "safe2": 63059, + "sd-card-fill": 63060, + "sd-card": 63061, + "skype": 63062, + "slash-lg": 63063, + "translate": 63064, + "x-lg": 63065, + "safe": 63066, + "apple": 63067, + "microsoft": 63069, + "windows": 63070, + "behance": 63068, + "dribbble": 63071, + "line": 63072, + "medium": 63073, + "paypal": 63074, + "pinterest": 63075, + "signal": 63076, + "snapchat": 63077, + "spotify": 63078, + "stack-overflow": 63079, + "strava": 63080, + "wordpress": 63081, + "vimeo": 63082, + "activity": 63083, + "easel2-fill": 63084, + "easel2": 63085, + "easel3-fill": 63086, + "easel3": 63087, + "fan": 63088, + "fingerprint": 63089, + "graph-down-arrow": 63090, + "graph-up-arrow": 63091, + "hypnotize": 63092, + "magic": 63093, + "person-rolodex": 63094, + "person-video": 63095, + "person-video2": 63096, + "person-video3": 63097, + "person-workspace": 63098, + "radioactive": 63099, + "webcam-fill": 63100, + "webcam": 63101, + "yin-yang": 63102, + "bandaid-fill": 63104, + "bandaid": 63105, + "bluetooth": 63106, + "body-text": 63107, + "boombox": 63108, + "boxes": 63109, + "dpad-fill": 63110, + "dpad": 63111, + "ear-fill": 63112, + "ear": 63113, + "envelope-check-fill": 63115, + "envelope-check": 63116, + "envelope-dash-fill": 63118, + "envelope-dash": 63119, + "envelope-exclamation-fill": 63121, + "envelope-exclamation": 63122, + "envelope-plus-fill": 63123, + "envelope-plus": 63124, + "envelope-slash-fill": 63126, + "envelope-slash": 63127, + "envelope-x-fill": 63129, + "envelope-x": 63130, + "explicit-fill": 63131, + "explicit": 63132, + "git": 63133, + "infinity": 63134, + "list-columns-reverse": 63135, + "list-columns": 63136, + "meta": 63137, + "nintendo-switch": 63140, + "pc-display-horizontal": 63141, + "pc-display": 63142, + "pc-horizontal": 63143, + "pc": 63144, + "playstation": 63145, + "plus-slash-minus": 63146, + "projector-fill": 63147, + "projector": 63148, + "qr-code-scan": 63149, + "qr-code": 63150, + "quora": 63151, + "quote": 63152, + "robot": 63153, + "send-check-fill": 63154, + "send-check": 63155, + "send-dash-fill": 63156, + "send-dash": 63157, + "send-exclamation-fill": 63159, + "send-exclamation": 63160, + "send-fill": 63161, + "send-plus-fill": 63162, + "send-plus": 63163, + "send-slash-fill": 63164, + "send-slash": 63165, + "send-x-fill": 63166, + "send-x": 63167, + "send": 63168, + "steam": 63169, + "terminal-dash": 63171, + "terminal-plus": 63172, + "terminal-split": 63173, + "ticket-detailed-fill": 63174, + "ticket-detailed": 63175, + "ticket-fill": 63176, + "ticket-perforated-fill": 63177, + "ticket-perforated": 63178, + "ticket": 63179, + "tiktok": 63180, + "window-dash": 63181, + "window-desktop": 63182, + "window-fullscreen": 63183, + "window-plus": 63184, + "window-split": 63185, + "window-stack": 63186, + "window-x": 63187, + "xbox": 63188, + "ethernet": 63189, + "hdmi-fill": 63190, + "hdmi": 63191, + "usb-c-fill": 63192, + "usb-c": 63193, + "usb-fill": 63194, + "usb-plug-fill": 63195, + "usb-plug": 63196, + "usb-symbol": 63197, + "usb": 63198, + "boombox-fill": 63199, + "displayport": 63201, + "gpu-card": 63202, + "memory": 63203, + "modem-fill": 63204, + "modem": 63205, + "motherboard-fill": 63206, + "motherboard": 63207, + "optical-audio-fill": 63208, + "optical-audio": 63209, + "pci-card": 63210, + "router-fill": 63211, + "router": 63212, + "thunderbolt-fill": 63215, + "thunderbolt": 63216, + "usb-drive-fill": 63217, + "usb-drive": 63218, + "usb-micro-fill": 63219, + "usb-micro": 63220, + "usb-mini-fill": 63221, + "usb-mini": 63222, + "cloud-haze2": 63223, + "device-hdd-fill": 63224, + "device-hdd": 63225, + "device-ssd-fill": 63226, + "device-ssd": 63227, + "displayport-fill": 63228, + "mortarboard-fill": 63229, + "mortarboard": 63230, + "terminal-x": 63231, + "arrow-through-heart-fill": 63232, + "arrow-through-heart": 63233, + "badge-sd-fill": 63234, + "badge-sd": 63235, + "bag-heart-fill": 63236, + "bag-heart": 63237, + "balloon-fill": 63238, + "balloon-heart-fill": 63239, + "balloon-heart": 63240, + "balloon": 63241, + "box2-fill": 63242, + "box2-heart-fill": 63243, + "box2-heart": 63244, + "box2": 63245, + "braces-asterisk": 63246, + "calendar-heart-fill": 63247, + "calendar-heart": 63248, + "calendar2-heart-fill": 63249, + "calendar2-heart": 63250, + "chat-heart-fill": 63251, + "chat-heart": 63252, + "chat-left-heart-fill": 63253, + "chat-left-heart": 63254, + "chat-right-heart-fill": 63255, + "chat-right-heart": 63256, + "chat-square-heart-fill": 63257, + "chat-square-heart": 63258, + "clipboard-check-fill": 63259, + "clipboard-data-fill": 63260, + "clipboard-fill": 63261, + "clipboard-heart-fill": 63262, + "clipboard-heart": 63263, + "clipboard-minus-fill": 63264, + "clipboard-plus-fill": 63265, + "clipboard-pulse": 63266, + "clipboard-x-fill": 63267, + "clipboard2-check-fill": 63268, + "clipboard2-check": 63269, + "clipboard2-data-fill": 63270, + "clipboard2-data": 63271, + "clipboard2-fill": 63272, + "clipboard2-heart-fill": 63273, + "clipboard2-heart": 63274, + "clipboard2-minus-fill": 63275, + "clipboard2-minus": 63276, + "clipboard2-plus-fill": 63277, + "clipboard2-plus": 63278, + "clipboard2-pulse-fill": 63279, + "clipboard2-pulse": 63280, + "clipboard2-x-fill": 63281, + "clipboard2-x": 63282, + "clipboard2": 63283, + "emoji-kiss-fill": 63284, + "emoji-kiss": 63285, + "envelope-heart-fill": 63286, + "envelope-heart": 63287, + "envelope-open-heart-fill": 63288, + "envelope-open-heart": 63289, + "envelope-paper-fill": 63290, + "envelope-paper-heart-fill": 63291, + "envelope-paper-heart": 63292, + "envelope-paper": 63293, + "filetype-aac": 63294, + "filetype-ai": 63295, + "filetype-bmp": 63296, + "filetype-cs": 63297, + "filetype-css": 63298, + "filetype-csv": 63299, + "filetype-doc": 63300, + "filetype-docx": 63301, + "filetype-exe": 63302, + "filetype-gif": 63303, + "filetype-heic": 63304, + "filetype-html": 63305, + "filetype-java": 63306, + "filetype-jpg": 63307, + "filetype-js": 63308, + "filetype-jsx": 63309, + "filetype-key": 63310, + "filetype-m4p": 63311, + "filetype-md": 63312, + "filetype-mdx": 63313, + "filetype-mov": 63314, + "filetype-mp3": 63315, + "filetype-mp4": 63316, + "filetype-otf": 63317, + "filetype-pdf": 63318, + "filetype-php": 63319, + "filetype-png": 63320, + "filetype-ppt": 63322, + "filetype-psd": 63323, + "filetype-py": 63324, + "filetype-raw": 63325, + "filetype-rb": 63326, + "filetype-sass": 63327, + "filetype-scss": 63328, + "filetype-sh": 63329, + "filetype-svg": 63330, + "filetype-tiff": 63331, + "filetype-tsx": 63332, + "filetype-ttf": 63333, + "filetype-txt": 63334, + "filetype-wav": 63335, + "filetype-woff": 63336, + "filetype-xls": 63338, + "filetype-xml": 63339, + "filetype-yml": 63340, + "heart-arrow": 63341, + "heart-pulse-fill": 63342, + "heart-pulse": 63343, + "heartbreak-fill": 63344, + "heartbreak": 63345, + "hearts": 63346, + "hospital-fill": 63347, + "hospital": 63348, + "house-heart-fill": 63349, + "house-heart": 63350, + "incognito": 63351, + "magnet-fill": 63352, + "magnet": 63353, + "person-heart": 63354, + "person-hearts": 63355, + "phone-flip": 63356, + "plugin": 63357, + "postage-fill": 63358, + "postage-heart-fill": 63359, + "postage-heart": 63360, + "postage": 63361, + "postcard-fill": 63362, + "postcard-heart-fill": 63363, + "postcard-heart": 63364, + "postcard": 63365, + "search-heart-fill": 63366, + "search-heart": 63367, + "sliders2-vertical": 63368, + "sliders2": 63369, + "trash3-fill": 63370, + "trash3": 63371, + "valentine": 63372, + "valentine2": 63373, + "wrench-adjustable-circle-fill": 63374, + "wrench-adjustable-circle": 63375, + "wrench-adjustable": 63376, + "filetype-json": 63377, + "filetype-pptx": 63378, + "filetype-xlsx": 63379, + "1-circle-fill": 63382, + "1-circle": 63383, + "1-square-fill": 63384, + "1-square": 63385, + "2-circle-fill": 63388, + "2-circle": 63389, + "2-square-fill": 63390, + "2-square": 63391, + "3-circle-fill": 63394, + "3-circle": 63395, + "3-square-fill": 63396, + "3-square": 63397, + "4-circle-fill": 63400, + "4-circle": 63401, + "4-square-fill": 63402, + "4-square": 63403, + "5-circle-fill": 63406, + "5-circle": 63407, + "5-square-fill": 63408, + "5-square": 63409, + "6-circle-fill": 63412, + "6-circle": 63413, + "6-square-fill": 63414, + "6-square": 63415, + "7-circle-fill": 63418, + "7-circle": 63419, + "7-square-fill": 63420, + "7-square": 63421, + "8-circle-fill": 63424, + "8-circle": 63425, + "8-square-fill": 63426, + "8-square": 63427, + "9-circle-fill": 63430, + "9-circle": 63431, + "9-square-fill": 63432, + "9-square": 63433, + "airplane-engines-fill": 63434, + "airplane-engines": 63435, + "airplane-fill": 63436, + "airplane": 63437, + "alexa": 63438, + "alipay": 63439, + "android": 63440, + "android2": 63441, + "box-fill": 63442, + "box-seam-fill": 63443, + "browser-chrome": 63444, + "browser-edge": 63445, + "browser-firefox": 63446, + "browser-safari": 63447, + "c-circle-fill": 63450, + "c-circle": 63451, + "c-square-fill": 63452, + "c-square": 63453, + "capsule-pill": 63454, + "capsule": 63455, + "car-front-fill": 63456, + "car-front": 63457, + "cassette-fill": 63458, + "cassette": 63459, + "cc-circle-fill": 63462, + "cc-circle": 63463, + "cc-square-fill": 63464, + "cc-square": 63465, + "cup-hot-fill": 63466, + "cup-hot": 63467, + "currency-rupee": 63468, + "dropbox": 63469, + "escape": 63470, + "fast-forward-btn-fill": 63471, + "fast-forward-btn": 63472, + "fast-forward-circle-fill": 63473, + "fast-forward-circle": 63474, + "fast-forward-fill": 63475, + "fast-forward": 63476, + "filetype-sql": 63477, + "fire": 63478, + "google-play": 63479, + "h-circle-fill": 63482, + "h-circle": 63483, + "h-square-fill": 63484, + "h-square": 63485, + "indent": 63486, + "lungs-fill": 63487, + "lungs": 63488, + "microsoft-teams": 63489, + "p-circle-fill": 63492, + "p-circle": 63493, + "p-square-fill": 63494, + "p-square": 63495, + "pass-fill": 63496, + "pass": 63497, + "prescription": 63498, + "prescription2": 63499, + "r-circle-fill": 63502, + "r-circle": 63503, + "r-square-fill": 63504, + "r-square": 63505, + "repeat-1": 63506, + "repeat": 63507, + "rewind-btn-fill": 63508, + "rewind-btn": 63509, + "rewind-circle-fill": 63510, + "rewind-circle": 63511, + "rewind-fill": 63512, + "rewind": 63513, + "train-freight-front-fill": 63514, + "train-freight-front": 63515, + "train-front-fill": 63516, + "train-front": 63517, + "train-lightrail-front-fill": 63518, + "train-lightrail-front": 63519, + "truck-front-fill": 63520, + "truck-front": 63521, + "ubuntu": 63522, + "unindent": 63523, + "unity": 63524, + "universal-access-circle": 63525, + "universal-access": 63526, + "virus": 63527, + "virus2": 63528, + "wechat": 63529, + "yelp": 63530, + "sign-stop-fill": 63531, + "sign-stop-lights-fill": 63532, + "sign-stop-lights": 63533, + "sign-stop": 63534, + "sign-turn-left-fill": 63535, + "sign-turn-left": 63536, + "sign-turn-right-fill": 63537, + "sign-turn-right": 63538, + "sign-turn-slight-left-fill": 63539, + "sign-turn-slight-left": 63540, + "sign-turn-slight-right-fill": 63541, + "sign-turn-slight-right": 63542, + "sign-yield-fill": 63543, + "sign-yield": 63544, + "ev-station-fill": 63545, + "ev-station": 63546, + "fuel-pump-diesel-fill": 63547, + "fuel-pump-diesel": 63548, + "fuel-pump-fill": 63549, + "fuel-pump": 63550, + "0-circle-fill": 63551, + "0-circle": 63552, + "0-square-fill": 63553, + "0-square": 63554, + "rocket-fill": 63555, + "rocket-takeoff-fill": 63556, + "rocket-takeoff": 63557, + "rocket": 63558, + "stripe": 63559, + "subscript": 63560, + "superscript": 63561, + "trello": 63562, + "envelope-at-fill": 63563, + "envelope-at": 63564, + "regex": 63565, + "text-wrap": 63566, + "sign-dead-end-fill": 63567, + "sign-dead-end": 63568, + "sign-do-not-enter-fill": 63569, + "sign-do-not-enter": 63570, + "sign-intersection-fill": 63571, + "sign-intersection-side-fill": 63572, + "sign-intersection-side": 63573, + "sign-intersection-t-fill": 63574, + "sign-intersection-t": 63575, + "sign-intersection-y-fill": 63576, + "sign-intersection-y": 63577, + "sign-intersection": 63578, + "sign-merge-left-fill": 63579, + "sign-merge-left": 63580, + "sign-merge-right-fill": 63581, + "sign-merge-right": 63582, + "sign-no-left-turn-fill": 63583, + "sign-no-left-turn": 63584, + "sign-no-parking-fill": 63585, + "sign-no-parking": 63586, + "sign-no-right-turn-fill": 63587, + "sign-no-right-turn": 63588, + "sign-railroad-fill": 63589, + "sign-railroad": 63590, + "building-add": 63591, + "building-check": 63592, + "building-dash": 63593, + "building-down": 63594, + "building-exclamation": 63595, + "building-fill-add": 63596, + "building-fill-check": 63597, + "building-fill-dash": 63598, + "building-fill-down": 63599, + "building-fill-exclamation": 63600, + "building-fill-gear": 63601, + "building-fill-lock": 63602, + "building-fill-slash": 63603, + "building-fill-up": 63604, + "building-fill-x": 63605, + "building-fill": 63606, + "building-gear": 63607, + "building-lock": 63608, + "building-slash": 63609, + "building-up": 63610, + "building-x": 63611, + "buildings-fill": 63612, + "buildings": 63613, + "bus-front-fill": 63614, + "bus-front": 63615, + "ev-front-fill": 63616, + "ev-front": 63617, + "globe-americas": 63618, + "globe-asia-australia": 63619, + "globe-central-south-asia": 63620, + "globe-europe-africa": 63621, + "house-add-fill": 63622, + "house-add": 63623, + "house-check-fill": 63624, + "house-check": 63625, + "house-dash-fill": 63626, + "house-dash": 63627, + "house-down-fill": 63628, + "house-down": 63629, + "house-exclamation-fill": 63630, + "house-exclamation": 63631, + "house-gear-fill": 63632, + "house-gear": 63633, + "house-lock-fill": 63634, + "house-lock": 63635, + "house-slash-fill": 63636, + "house-slash": 63637, + "house-up-fill": 63638, + "house-up": 63639, + "house-x-fill": 63640, + "house-x": 63641, + "person-add": 63642, + "person-down": 63643, + "person-exclamation": 63644, + "person-fill-add": 63645, + "person-fill-check": 63646, + "person-fill-dash": 63647, + "person-fill-down": 63648, + "person-fill-exclamation": 63649, + "person-fill-gear": 63650, + "person-fill-lock": 63651, + "person-fill-slash": 63652, + "person-fill-up": 63653, + "person-fill-x": 63654, + "person-gear": 63655, + "person-lock": 63656, + "person-slash": 63657, + "person-up": 63658, + "scooter": 63659, + "taxi-front-fill": 63660, + "taxi-front": 63661, + "amd": 63662, + "database-add": 63663, + "database-check": 63664, + "database-dash": 63665, + "database-down": 63666, + "database-exclamation": 63667, + "database-fill-add": 63668, + "database-fill-check": 63669, + "database-fill-dash": 63670, + "database-fill-down": 63671, + "database-fill-exclamation": 63672, + "database-fill-gear": 63673, + "database-fill-lock": 63674, + "database-fill-slash": 63675, + "database-fill-up": 63676, + "database-fill-x": 63677, + "database-fill": 63678, + "database-gear": 63679, + "database-lock": 63680, + "database-slash": 63681, + "database-up": 63682, + "database-x": 63683, + "database": 63684, + "houses-fill": 63685, + "houses": 63686, + "nvidia": 63687, + "person-vcard-fill": 63688, + "person-vcard": 63689, + "sina-weibo": 63690, + "tencent-qq": 63691, + "wikipedia": 63692 +} \ No newline at end of file diff --git a/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.min.css b/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.min.css new file mode 100644 index 00000000..088ba569 --- /dev/null +++ b/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap Icons v1.10.5 (https://icons.getbootstrap.com/) + * Copyright 2019-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?1fa40e8900654d2863d011707b9fb6f2") format("woff2"),url("fonts/bootstrap-icons.woff?1fa40e8900654d2863d011707b9fb6f2") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"} \ No newline at end of file diff --git a/static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff b/static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff new file mode 100644 index 0000000000000000000000000000000000000000..6e72a590a26bc71c7011e3c50da2ca766a853344 GIT binary patch literal 164360 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͍DL09^UPJd{U1rzmAhp)eP! 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 literal 0 HcmV?d00001 diff --git a/static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff2 b/static/vendor/bootstrap-icons-1.10.5/font/fonts/bootstrap-icons.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3b957d5a764cc096fcee42de5ab6a48dfee9af4a GIT binary patch literal 121340 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 z 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Ã-ERF?&y_pfYxO}cJT5#fJl}fxL8iJjg5hEI`5w1| z4jj3w1x_3U4yc2`ssnEup!_G!d2A0a@I{6#o9N@`^G?bm;jLk)oJ?V43+woLdqX9B z`MAFx53^t4oDLizw;xYzMc0H}G@Qh+XGaYWNV_RgjU6;gr0wR8og5R1b9FoYlATn5 zQmNtwR^OPOaX3Ap^-bB$&#yffJPrKczSPCnno8|;NeQ8FT?)Kh@lPm&wkGvM@7`ci znqvD~YX&8D{C^>o%L);}=?PDzjILP`^MdqQ%UXirxTwY&)zJWf-8poOCB6y5j(P7$ z3OL*>+GB~gE9~>8n5RalUQmJTHka!PB}lLZ?aF*Yu|3S z0JGW>wWo3Vvb2K66FMP12`Byz*C!nWqGCCJ{pb7?Ni)-za5B&N>{#EOy z*KgsVTvu5Z=m4!LPaZLAVXTe@_*X`gLfL3l;%O>$>7X2Clt(^}ro8llVP_Q9oY@5Q ztiXrTW~mh{NR%{RPaN%@T9EZvxFJ&l=aKr9#D}+3#R0O@qbZH}frCcR!kT~7>pu-F zj{SYE{*1Zo+qb@`m_>6{!Y;CF$&j@R4A+m#MoC z{J|nLJs&LQY2+54TN~9imGufz%cDl$u7N!a)1vHNRwzLMUNOx)&`&kYyIL@wNV{3V z+RvCa&&1*@Tu=bZ?5$Xf`azocNrrO3|6#A!x&oGBxjd=?kL0DP;0JNhA-dULooaz#28>HW|f-dj>r?_uPS}g@j%yq-e1Do*G-SX8dcuiBlXkqn#fnj?( z!Cx_2hqAt0pczI!H|wBef3FpJ+?N&}^TK)y3#OWG9=k;c<(dMXc}Dge z%(9nfJ<<5Or<&3X9K+2Bau>6}w+up67dyqsmwI4&Fw?RGsDkwVXz)>g_GIRcMlZ&d z%q~2CW=f&DR+ea7`n2}_qlCB_>j3^_+BB*@faH_-L%J8BF){?1f7EvrE{ru81crU@ zkj@VLC!RS-pbIa&t1n-v0AsE(z;if+kVH$U1cVCi8P>opaoyh$!Hko#g107UovEyi z=G^Qlu{H=?AYW<9=@GxfG_(X76=V`bYnJ36b%;(FPPP-rvVkb>*KwfuWl^=IAR zZ=%aI!Q0vCrnC4546)8jif*;m;3G8lEM32C5jWW_6TEZPDZQf%I#D510>ffg(YI@X z5X?J}v3#VDN0+o(Nl|RX_s5+-p;QwzEEZ}ldoVRM@jf!jLMKC{hb{UeJ1um5F?3;l zziPbL$mc!_lox_D4hlJ9;5=H1%2X2=f`V9R(H{eikdDpal?X>Wy{umAHHF<>(HQ|E z`Uq^{JAXarZ7#u#bT)WtvWf#kNs$wPYiC;_vDxA~90Bry_>8jE?x~-@n&mC5#)olM z*wzM4+9fA`36&~K&IiSP4whhekUlm$$`n5C+njts2%l}3#D2Q%{8=)@iVmtwdA0rE z#!eb}fxR0f3xBz4a_$zpd3|KJpT0cU?r)8L0lL`3XK>!w)98483_H9eDIdW4z?Aj& zzkHDKMy%+$w+}MkRK!CI} W{i+SOEA$_gR$T^}fuiz{0000rhX=L* literal 0 HcmV?d00001 diff --git a/web/config.py b/web/config.py new file mode 100644 index 00000000..c2dec313 --- /dev/null +++ b/web/config.py @@ -0,0 +1,53 @@ +import web.util + +import libflagship.httpapi +import libflagship.logincache + +import cli.util +import cli.config + + +def config_show(config): + config_output = f'''

      Account:

      + user_id: {config.account.user_id[:10]}...[REDACTED]
      + auth_token: {config.account.auth_token[:10]}...[REDACTED]
      + email: {config.account.email}
      + region: {config.account.region.upper()}

      + duid: {p.p2p_duid}
      + sn: {p.sn}
      + ip: {p.ip_addr}
      + wifi_mac: {cli.util.pretty_mac(p.wifi_mac)}
      + api_hosts: {', '.join(p.api_hosts)}
      + p2p_hosts: {', '.join(p.p2p_hosts)}

      + ''' + return config_output + + +def config_import(login_file, config): + # extract auth token + cache = libflagship.logincache.load(login_file.stream.read())["data"] + auth_token = cache["auth_token"] + + # extract account region + region = libflagship.logincache.guess_region(cache["ab_code"]) + + try: + newConfig = cli.config.load_config_from_api(auth_token, region, False) + except libflagship.httpapi.APIError as E: + message = f"Config import failed: {E}
      Auth token might be expired: make sure Ankermake Slicer can connect, then try again" + return web.util.flash_redirect(message, 'danger') + except Exception as E: + message = f"Config import failed: {E}" + return web.util.flash_redirect(message, 'danger') + + try: + config.save("default", newConfig) + except Exception as E: + message = f"Config import failed: {E}" + return web.util.flash_redirect(message, 'danger') + message = "AnkerMake Login Configuration imported successfully! Reloading..." + return web.util.flash_redirect(message, 'success') diff --git a/web/platform.py b/web/platform.py new file mode 100644 index 00000000..1397e0ef --- /dev/null +++ b/web/platform.py @@ -0,0 +1,19 @@ +def os_platform(os_family: str): + if os_family.startswith('Mac OS'): + return 'macos' + if os_family.startswith('Windows'): + return 'windows' + if os_family.__contains__('Linux'): + return 'linux' + else: + return None + + +def login_path(platform: str): + if platform == 'macos': + return '~/Library/Application Support/AnkerMake/AnkerMake_64bit_fp/login.json' + if platform == 'windows': + return r'%LOCALAPPDATA%\Ankermake\AnkerMake_64bit_fp\login.json' + else: + return 'Unsupported OS: You must supply path to login.json' + \ No newline at end of file diff --git a/web/util.py b/web/util.py new file mode 100644 index 00000000..a69eee8f --- /dev/null +++ b/web/util.py @@ -0,0 +1,10 @@ +from flask import flash, redirect + +def allowed_file(filename: str, allowed_ext: object): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_ext + + +def flash_redirect(message: str | None = None, category = 'info', path = '/'): + if message: + flash(message, category) + return redirect(path) From 99bb6b354272ce9aada9b33ce2898bda3b6a0b7d Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Wed, 3 May 2023 22:54:43 -0700 Subject: [PATCH 136/405] Updating front-end --- .gitignore | 1 + .hintrc | 13 + .prettierrc | 5 + .vscode/settings.json | 7 +- ankerctl.py | 7 +- static/ankersrv.css | 40 +- static/ankersrv.js | 86 +- static/index.html | 279 ++- .../font/bootstrap-icons.json | 1955 ----------------- web/__init__.py | 53 +- web/config.py | 12 +- web/platform.py | 6 +- web/service/filetransfer.py | 3 +- web/service/mqtt.py | 2 +- web/service/video.py | 3 +- web/util.py | 4 - 16 files changed, 313 insertions(+), 2163 deletions(-) create mode 100644 .hintrc create mode 100644 .prettierrc delete mode 100644 static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json diff --git a/.gitignore b/.gitignore index 16ba6fdd..98ba9ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__ .DS_Store /settings.json +.vscode diff --git a/.hintrc b/.hintrc new file mode 100644 index 00000000..e099e671 --- /dev/null +++ b/.hintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/aria": [ + "default", + { + "aria-valid-attr-value": "off" + } + ] + } +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..9eafbe64 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "useTabs": false, + "printWidth": 160 +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d5d00188..450e9505 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,8 @@ { - "files.insertFinalNewline": true + "files.insertFinalNewline": true, + "[python]": { + "editor.defaultFormatter": "ms-python.autopep8" + }, + "python.formatting.provider": "none", + "python.formatting.autopep8Args": ["--ignore", "E402"] } diff --git a/ankerctl.py b/ankerctl.py index abdec0be..400764a3 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -35,10 +35,9 @@ def __init__(self): def load_config(self, required=True): with self.config.open() as config: if not getattr(config, 'printers', False): - if not required: - log.info("No printers found in config. Please upload configuration using web browser") - else: - log.critical("No printers found in config. Please import configuration using 'config import'") + log.warning("No printers found in config. Please upload configuration using the webserver or 'ankerctl.py config import'") + if required: + exit(1) def upgrade_config_if_needed(self): try: diff --git a/static/ankersrv.css b/static/ankersrv.css index 07189f49..0b7e7a5e 100644 --- a/static/ankersrv.css +++ b/static/ankersrv.css @@ -1,21 +1,41 @@ -.nav-link { - color: #88f387!important; +.nav-link, +.btn-link { + color: #88f387; + text-decoration: none; } -.nav-link.active, .btn-primary { - color: #000000!important; - background-color: #88f387!important; - border:#41a03f!important; +.nav-link.active, +.btn-primary { + color: #000000 !important; + background-color: #88f387 !important; + border: #41a03f; } -.nav-link:hover, .btn:hover { - color: #000000!important; +.nav-link:hover, +.btn:hover { + color: #000000; background-color: #adf3ac; border-color: #41a03f; /*set the color you want here*/ } -pre, code { +a.text-light { + text-decoration: none; +} + +a.text-light:hover { + color: #88f387 !important; +} + +pre, +code { color: #88f387; - background-color: #292929 + background-color: #292929; +} + +.form-label { + font-weight: bolder; } +code#loginFilePath { + font-weight: lighter; +} diff --git a/static/ankersrv.js b/static/ankersrv.js index 39867168..5e19c2cd 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -1,82 +1,78 @@ $(function () { + function updateClipboard(text) { + navigator.clipboard.writeText(text); + console.log(`Copied ${text} to clipboard`); + } + + $("#configData").on("click", function () { + updateClipboard($("#octoPrintHost").val()); + }); + + $("#copyFilePath").on("click", function () { + updateClipboard($("#loginFilePath").val()); + }); + + let alert_list = document.querySelectorAll(".alert"); + alert_list.forEach(function (alert) { + new bootstrap.Alert(alert); + + let alert_timeout = alert.getAttribute("data-timeout"); + setTimeout(() => { + bootstrap.Alert.getInstance(alert).close(); + }, +alert_timeout); + }); socket = new WebSocket("ws://" + location.host + "/ws/mqtt"); - socket.addEventListener('message', ev => { + socket.addEventListener("message", (ev) => { console.log(JSON.parse(ev.data)); }); - var jmuxer; jmuxer = new JMuxer({ - node: 'player', - mode: 'video', + node: "player", + mode: "video", flushingTime: 0, fps: 15, // debug: true, - onReady: function(data) { + onReady: function (data) { console.log(data); }, - onError: function(data) { + onError: function (data) { console.log(data); - } + }, }); var ws = new WebSocket("ws://" + location.host + "/ws/video"); - ws.binaryType = 'arraybuffer'; - ws.addEventListener('message',function(event) { + ws.binaryType = "arraybuffer"; + ws.addEventListener("message", function (event) { jmuxer.feed({ - video: new Uint8Array(event.data) + video: new Uint8Array(event.data), }); }); - ws.addEventListener('error', function(e) { - console.log('Socket Error'); + ws.addEventListener("error", function (e) { + console.log("Socket Error"); }); var wsctrl = new WebSocket("ws://" + location.host + "/ws/ctrl"); - wsctrl.addEventListener("open", (event) => { - // Set initial state of Light & Quality (Defaults to Off:Low) - wsctrl.send(JSON.stringify({"light": false})); - wsctrl.send(JSON.stringify({"quality": 0})); - }); - $('#light-on').on('click', function() { - wsctrl.send(JSON.stringify({"light": true})); + $("#light-on").on("click", function () { + wsctrl.send(JSON.stringify({ light: true })); return false; }); - $('#light-off').on('click', function() { - wsctrl.send(JSON.stringify({"light": false})); + $("#light-off").on("click", function () { + wsctrl.send(JSON.stringify({ light: false })); return false; }); - $('#quality-low').on('click', function() { - wsctrl.send(JSON.stringify({"quality": 0})); + $("#quality-low").on("click", function () { + wsctrl.send(JSON.stringify({ quality: 0 })); return false; }); - $('#quality-high').on('click', function() { - wsctrl.send(JSON.stringify({"quality": 1})); - return false; - }); - - $('#configData').on('click',function(){ - navigator.clipboard.writeText($('#octoPrintHost').val()); + $("#quality-high").on("click", function () { + wsctrl.send(JSON.stringify({ quality: 1 })); return false; }); - - $('#copyFilePath').on('click',function(){ - navigator.clipboard.writeText($('#loginFilePath').val()); - return false; - }); - - let alert_list = document.querySelectorAll('.alert'); - alert_list.forEach(function(alert) { - new bootstrap.Alert(alert); - - let alert_timeout = alert.getAttribute('data-timeout'); - setTimeout(() => { - bootstrap.Alert.getInstance(alert).close(); - }, +alert_timeout); - }); }); diff --git a/static/index.html b/static/index.html index b1afe391..2790b822 100644 --- a/static/index.html +++ b/static/index.html @@ -1,98 +1,132 @@ - - + + ankerctl - - - - - - - + + + -
      -
      -

      ankerctl

      -

      Congratulations on running ankerctl

      +
      +
      +

      ankerctl

      +
      + - -
      - {% with messages = get_flashed_messages(with_categories=true) %} - - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} -
      -
      -
      -
      - {% if configure %} -
      -
      - -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - {% else %} -
      -
      - No printer configuration found, upload config in Setup -
      -
      - {% endif %} -
      -
      - {% if configure %} -
      -
      -
      -
      -

      Connecting PrusaSlicer/SuperSlicer

      -
        -
      1. Go to "Printer settings" tab
      2. -
      3. Click "Gear"⚙️ (Add/Edit Physical Printer)
      4. -
      5. Fill in "Descriptive name for the printer" with whatever you like
      6. -
      7. Select your Ankermake printer instructions from the dropdown menu
      8. -
      9. Under "Host Type" select "OctoPrint"
      10. -
      11. In "Hostname, IP or URL" fill in with: {{ request_host }}:{{ request_port }} 
      12. -
      13. Leave "API Key / Password" blank
      14. +
        + {% with messages = get_flashed_messages(with_categories=true) %} + + {% if messages %} {% for category, message in messages %} + + {% endfor %} {% endif %} {% endwith %} +
        +
        + {% if configure %} +
        +
        +
        +
        + +
        +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +

        Connecting PrusaSlicer/SuperSlicer

        +
          +
        1. Go to "Printer settings" tab
        2. +
        3. Click "Gear"⚙️ (Add/Edit Physical Printer)
        4. +
        5. Fill in "Descriptive name for the printer" with whatever you like
        6. +
        7. Select your Ankermake printer instructions from the dropdown menu
        8. +
        9. Under "Host Type" select "OctoPrint"
        10. +
        11. + In "Hostname, IP or URL" fill in with: + {{ request_host }}:{{ request_port }}  +
        12. +
        13. Leave "API Key / Password" blank
        14. Click "Test" to validate input
        15. Click "Ok" to close the window
        16. Slice your file as normal
        17. @@ -100,13 +134,13 @@

          Connecting PrusaSlicer/SuperSlicer

        18. Click "Upload and print" - the "Upload"-only is not supported
        19. Enjoy printing with ankerctl 😉
        -
        Hint: Ctrl+Shift+G will slice and open the upload to printer window +
        Hint: Ctrl+Shift+G will slice and open the upload to printer window
        - step 1 - step 2 - step 3 - step 4 + step 1 + step 2 + step 3 + step 4
        @@ -116,31 +150,68 @@

        Connecting PrusaSlicer/SuperSlicer

        -
        +
        - +
        - - + +
        - Location: {{ login_file_path }}  + Location: {{ login_file_path }}  + +
        - - - {% autoescape false %} - {{ anker_config }} - {% endautoescape %} - + + {% autoescape false %} {{ anker_config }} {% endautoescape %}
        +
        diff --git a/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json b/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json deleted file mode 100644 index d85eaaf2..00000000 --- a/static/vendor/bootstrap-icons-1.10.5/font/bootstrap-icons.json +++ /dev/null @@ -1,1955 +0,0 @@ -{ - "123": 63103, - "alarm-fill": 61697, - "alarm": 61698, - "align-bottom": 61699, - "align-center": 61700, - "align-end": 61701, - "align-middle": 61702, - "align-start": 61703, - "align-top": 61704, - "alt": 61705, - "app-indicator": 61706, - "app": 61707, - "archive-fill": 61708, - "archive": 61709, - "arrow-90deg-down": 61710, - "arrow-90deg-left": 61711, - "arrow-90deg-right": 61712, - "arrow-90deg-up": 61713, - "arrow-bar-down": 61714, - "arrow-bar-left": 61715, - "arrow-bar-right": 61716, - "arrow-bar-up": 61717, - "arrow-clockwise": 61718, - "arrow-counterclockwise": 61719, - "arrow-down-circle-fill": 61720, - "arrow-down-circle": 61721, - "arrow-down-left-circle-fill": 61722, - "arrow-down-left-circle": 61723, - "arrow-down-left-square-fill": 61724, - "arrow-down-left-square": 61725, - "arrow-down-left": 61726, - "arrow-down-right-circle-fill": 61727, - "arrow-down-right-circle": 61728, - "arrow-down-right-square-fill": 61729, - "arrow-down-right-square": 61730, - "arrow-down-right": 61731, - "arrow-down-short": 61732, - "arrow-down-square-fill": 61733, - "arrow-down-square": 61734, - "arrow-down-up": 61735, - "arrow-down": 61736, - "arrow-left-circle-fill": 61737, - "arrow-left-circle": 61738, - "arrow-left-right": 61739, - "arrow-left-short": 61740, - "arrow-left-square-fill": 61741, - "arrow-left-square": 61742, - "arrow-left": 61743, - "arrow-repeat": 61744, - "arrow-return-left": 61745, - "arrow-return-right": 61746, - "arrow-right-circle-fill": 61747, - "arrow-right-circle": 61748, - "arrow-right-short": 61749, - "arrow-right-square-fill": 61750, - "arrow-right-square": 61751, - "arrow-right": 61752, - "arrow-up-circle-fill": 61753, - "arrow-up-circle": 61754, - "arrow-up-left-circle-fill": 61755, - "arrow-up-left-circle": 61756, - "arrow-up-left-square-fill": 61757, - "arrow-up-left-square": 61758, - "arrow-up-left": 61759, - "arrow-up-right-circle-fill": 61760, - "arrow-up-right-circle": 61761, - "arrow-up-right-square-fill": 61762, - "arrow-up-right-square": 61763, - "arrow-up-right": 61764, - "arrow-up-short": 61765, - "arrow-up-square-fill": 61766, - "arrow-up-square": 61767, - "arrow-up": 61768, - "arrows-angle-contract": 61769, - "arrows-angle-expand": 61770, - "arrows-collapse": 61771, - "arrows-expand": 61772, - "arrows-fullscreen": 61773, - "arrows-move": 61774, - "aspect-ratio-fill": 61775, - "aspect-ratio": 61776, - "asterisk": 61777, - "at": 61778, - "award-fill": 61779, - "award": 61780, - "back": 61781, - "backspace-fill": 61782, - "backspace-reverse-fill": 61783, - "backspace-reverse": 61784, - "backspace": 61785, - "badge-3d-fill": 61786, - "badge-3d": 61787, - "badge-4k-fill": 61788, - "badge-4k": 61789, - "badge-8k-fill": 61790, - "badge-8k": 61791, - "badge-ad-fill": 61792, - "badge-ad": 61793, - "badge-ar-fill": 61794, - "badge-ar": 61795, - "badge-cc-fill": 61796, - "badge-cc": 61797, - "badge-hd-fill": 61798, - "badge-hd": 61799, - "badge-tm-fill": 61800, - "badge-tm": 61801, - "badge-vo-fill": 61802, - "badge-vo": 61803, - "badge-vr-fill": 61804, - "badge-vr": 61805, - "badge-wc-fill": 61806, - "badge-wc": 61807, - "bag-check-fill": 61808, - "bag-check": 61809, - "bag-dash-fill": 61810, - "bag-dash": 61811, - "bag-fill": 61812, - "bag-plus-fill": 61813, - "bag-plus": 61814, - "bag-x-fill": 61815, - "bag-x": 61816, - "bag": 61817, - "bar-chart-fill": 61818, - "bar-chart-line-fill": 61819, - "bar-chart-line": 61820, - "bar-chart-steps": 61821, - "bar-chart": 61822, - "basket-fill": 61823, - "basket": 61824, - "basket2-fill": 61825, - "basket2": 61826, - "basket3-fill": 61827, - "basket3": 61828, - "battery-charging": 61829, - "battery-full": 61830, - "battery-half": 61831, - "battery": 61832, - "bell-fill": 61833, - "bell": 61834, - "bezier": 61835, - "bezier2": 61836, - "bicycle": 61837, - "binoculars-fill": 61838, - "binoculars": 61839, - "blockquote-left": 61840, - "blockquote-right": 61841, - "book-fill": 61842, - "book-half": 61843, - "book": 61844, - "bookmark-check-fill": 61845, - "bookmark-check": 61846, - "bookmark-dash-fill": 61847, - "bookmark-dash": 61848, - "bookmark-fill": 61849, - "bookmark-heart-fill": 61850, - "bookmark-heart": 61851, - "bookmark-plus-fill": 61852, - "bookmark-plus": 61853, - "bookmark-star-fill": 61854, - "bookmark-star": 61855, - "bookmark-x-fill": 61856, - "bookmark-x": 61857, - "bookmark": 61858, - "bookmarks-fill": 61859, - "bookmarks": 61860, - "bookshelf": 61861, - "bootstrap-fill": 61862, - "bootstrap-reboot": 61863, - "bootstrap": 61864, - "border-all": 61865, - "border-bottom": 61866, - "border-center": 61867, - "border-inner": 61868, - "border-left": 61869, - "border-middle": 61870, - "border-outer": 61871, - "border-right": 61872, - "border-style": 61873, - "border-top": 61874, - "border-width": 61875, - "border": 61876, - "bounding-box-circles": 61877, - "bounding-box": 61878, - "box-arrow-down-left": 61879, - "box-arrow-down-right": 61880, - "box-arrow-down": 61881, - "box-arrow-in-down-left": 61882, - "box-arrow-in-down-right": 61883, - "box-arrow-in-down": 61884, - "box-arrow-in-left": 61885, - "box-arrow-in-right": 61886, - "box-arrow-in-up-left": 61887, - "box-arrow-in-up-right": 61888, - "box-arrow-in-up": 61889, - "box-arrow-left": 61890, - "box-arrow-right": 61891, - "box-arrow-up-left": 61892, - "box-arrow-up-right": 61893, - "box-arrow-up": 61894, - "box-seam": 61895, - "box": 61896, - "braces": 61897, - "bricks": 61898, - "briefcase-fill": 61899, - "briefcase": 61900, - "brightness-alt-high-fill": 61901, - "brightness-alt-high": 61902, - "brightness-alt-low-fill": 61903, - "brightness-alt-low": 61904, - "brightness-high-fill": 61905, - "brightness-high": 61906, - "brightness-low-fill": 61907, - "brightness-low": 61908, - "broadcast-pin": 61909, - "broadcast": 61910, - "brush-fill": 61911, - "brush": 61912, - "bucket-fill": 61913, - "bucket": 61914, - "bug-fill": 61915, - "bug": 61916, - "building": 61917, - "bullseye": 61918, - "calculator-fill": 61919, - "calculator": 61920, - "calendar-check-fill": 61921, - "calendar-check": 61922, - "calendar-date-fill": 61923, - "calendar-date": 61924, - "calendar-day-fill": 61925, - "calendar-day": 61926, - "calendar-event-fill": 61927, - "calendar-event": 61928, - "calendar-fill": 61929, - "calendar-minus-fill": 61930, - "calendar-minus": 61931, - "calendar-month-fill": 61932, - "calendar-month": 61933, - "calendar-plus-fill": 61934, - "calendar-plus": 61935, - "calendar-range-fill": 61936, - "calendar-range": 61937, - "calendar-week-fill": 61938, - "calendar-week": 61939, - "calendar-x-fill": 61940, - "calendar-x": 61941, - "calendar": 61942, - "calendar2-check-fill": 61943, - "calendar2-check": 61944, - "calendar2-date-fill": 61945, - "calendar2-date": 61946, - "calendar2-day-fill": 61947, - "calendar2-day": 61948, - "calendar2-event-fill": 61949, - "calendar2-event": 61950, - "calendar2-fill": 61951, - "calendar2-minus-fill": 61952, - "calendar2-minus": 61953, - "calendar2-month-fill": 61954, - "calendar2-month": 61955, - "calendar2-plus-fill": 61956, - "calendar2-plus": 61957, - "calendar2-range-fill": 61958, - "calendar2-range": 61959, - "calendar2-week-fill": 61960, - "calendar2-week": 61961, - "calendar2-x-fill": 61962, - "calendar2-x": 61963, - "calendar2": 61964, - "calendar3-event-fill": 61965, - "calendar3-event": 61966, - "calendar3-fill": 61967, - "calendar3-range-fill": 61968, - "calendar3-range": 61969, - "calendar3-week-fill": 61970, - "calendar3-week": 61971, - "calendar3": 61972, - "calendar4-event": 61973, - "calendar4-range": 61974, - "calendar4-week": 61975, - "calendar4": 61976, - "camera-fill": 61977, - "camera-reels-fill": 61978, - "camera-reels": 61979, - "camera-video-fill": 61980, - "camera-video-off-fill": 61981, - "camera-video-off": 61982, - "camera-video": 61983, - "camera": 61984, - "camera2": 61985, - "capslock-fill": 61986, - "capslock": 61987, - "card-checklist": 61988, - "card-heading": 61989, - "card-image": 61990, - "card-list": 61991, - "card-text": 61992, - "caret-down-fill": 61993, - "caret-down-square-fill": 61994, - "caret-down-square": 61995, - "caret-down": 61996, - "caret-left-fill": 61997, - "caret-left-square-fill": 61998, - "caret-left-square": 61999, - "caret-left": 62000, - "caret-right-fill": 62001, - "caret-right-square-fill": 62002, - "caret-right-square": 62003, - "caret-right": 62004, - "caret-up-fill": 62005, - "caret-up-square-fill": 62006, - "caret-up-square": 62007, - "caret-up": 62008, - "cart-check-fill": 62009, - "cart-check": 62010, - "cart-dash-fill": 62011, - "cart-dash": 62012, - "cart-fill": 62013, - "cart-plus-fill": 62014, - "cart-plus": 62015, - "cart-x-fill": 62016, - "cart-x": 62017, - "cart": 62018, - "cart2": 62019, - "cart3": 62020, - "cart4": 62021, - "cash-stack": 62022, - "cash": 62023, - "cast": 62024, - "chat-dots-fill": 62025, - "chat-dots": 62026, - "chat-fill": 62027, - "chat-left-dots-fill": 62028, - "chat-left-dots": 62029, - "chat-left-fill": 62030, - "chat-left-quote-fill": 62031, - "chat-left-quote": 62032, - "chat-left-text-fill": 62033, - "chat-left-text": 62034, - "chat-left": 62035, - "chat-quote-fill": 62036, - "chat-quote": 62037, - "chat-right-dots-fill": 62038, - "chat-right-dots": 62039, - "chat-right-fill": 62040, - "chat-right-quote-fill": 62041, - "chat-right-quote": 62042, - "chat-right-text-fill": 62043, - "chat-right-text": 62044, - "chat-right": 62045, - "chat-square-dots-fill": 62046, - "chat-square-dots": 62047, - "chat-square-fill": 62048, - "chat-square-quote-fill": 62049, - "chat-square-quote": 62050, - "chat-square-text-fill": 62051, - "chat-square-text": 62052, - "chat-square": 62053, - "chat-text-fill": 62054, - "chat-text": 62055, - "chat": 62056, - "check-all": 62057, - "check-circle-fill": 62058, - "check-circle": 62059, - "check-square-fill": 62060, - "check-square": 62061, - "check": 62062, - "check2-all": 62063, - "check2-circle": 62064, - "check2-square": 62065, - "check2": 62066, - "chevron-bar-contract": 62067, - "chevron-bar-down": 62068, - "chevron-bar-expand": 62069, - "chevron-bar-left": 62070, - "chevron-bar-right": 62071, - "chevron-bar-up": 62072, - "chevron-compact-down": 62073, - "chevron-compact-left": 62074, - "chevron-compact-right": 62075, - "chevron-compact-up": 62076, - "chevron-contract": 62077, - "chevron-double-down": 62078, - "chevron-double-left": 62079, - "chevron-double-right": 62080, - "chevron-double-up": 62081, - "chevron-down": 62082, - "chevron-expand": 62083, - "chevron-left": 62084, - "chevron-right": 62085, - "chevron-up": 62086, - "circle-fill": 62087, - "circle-half": 62088, - "circle-square": 62089, - "circle": 62090, - "clipboard-check": 62091, - "clipboard-data": 62092, - "clipboard-minus": 62093, - "clipboard-plus": 62094, - "clipboard-x": 62095, - "clipboard": 62096, - "clock-fill": 62097, - "clock-history": 62098, - "clock": 62099, - "cloud-arrow-down-fill": 62100, - "cloud-arrow-down": 62101, - "cloud-arrow-up-fill": 62102, - "cloud-arrow-up": 62103, - "cloud-check-fill": 62104, - "cloud-check": 62105, - "cloud-download-fill": 62106, - "cloud-download": 62107, - "cloud-drizzle-fill": 62108, - "cloud-drizzle": 62109, - "cloud-fill": 62110, - "cloud-fog-fill": 62111, - "cloud-fog": 62112, - "cloud-fog2-fill": 62113, - "cloud-fog2": 62114, - "cloud-hail-fill": 62115, - "cloud-hail": 62116, - "cloud-haze-fill": 62118, - "cloud-haze": 62119, - "cloud-haze2-fill": 62120, - "cloud-lightning-fill": 62121, - "cloud-lightning-rain-fill": 62122, - "cloud-lightning-rain": 62123, - "cloud-lightning": 62124, - "cloud-minus-fill": 62125, - "cloud-minus": 62126, - "cloud-moon-fill": 62127, - "cloud-moon": 62128, - "cloud-plus-fill": 62129, - "cloud-plus": 62130, - "cloud-rain-fill": 62131, - "cloud-rain-heavy-fill": 62132, - "cloud-rain-heavy": 62133, - "cloud-rain": 62134, - "cloud-slash-fill": 62135, - "cloud-slash": 62136, - "cloud-sleet-fill": 62137, - "cloud-sleet": 62138, - "cloud-snow-fill": 62139, - "cloud-snow": 62140, - "cloud-sun-fill": 62141, - "cloud-sun": 62142, - "cloud-upload-fill": 62143, - "cloud-upload": 62144, - "cloud": 62145, - "clouds-fill": 62146, - "clouds": 62147, - "cloudy-fill": 62148, - "cloudy": 62149, - "code-slash": 62150, - "code-square": 62151, - "code": 62152, - "collection-fill": 62153, - "collection-play-fill": 62154, - "collection-play": 62155, - "collection": 62156, - "columns-gap": 62157, - "columns": 62158, - "command": 62159, - "compass-fill": 62160, - "compass": 62161, - "cone-striped": 62162, - "cone": 62163, - "controller": 62164, - "cpu-fill": 62165, - "cpu": 62166, - "credit-card-2-back-fill": 62167, - "credit-card-2-back": 62168, - "credit-card-2-front-fill": 62169, - "credit-card-2-front": 62170, - "credit-card-fill": 62171, - "credit-card": 62172, - "crop": 62173, - "cup-fill": 62174, - "cup-straw": 62175, - "cup": 62176, - "cursor-fill": 62177, - "cursor-text": 62178, - "cursor": 62179, - "dash-circle-dotted": 62180, - "dash-circle-fill": 62181, - "dash-circle": 62182, - "dash-square-dotted": 62183, - "dash-square-fill": 62184, - "dash-square": 62185, - "dash": 62186, - "diagram-2-fill": 62187, - "diagram-2": 62188, - "diagram-3-fill": 62189, - "diagram-3": 62190, - "diamond-fill": 62191, - "diamond-half": 62192, - "diamond": 62193, - "dice-1-fill": 62194, - "dice-1": 62195, - "dice-2-fill": 62196, - "dice-2": 62197, - "dice-3-fill": 62198, - "dice-3": 62199, - "dice-4-fill": 62200, - "dice-4": 62201, - "dice-5-fill": 62202, - "dice-5": 62203, - "dice-6-fill": 62204, - "dice-6": 62205, - "disc-fill": 62206, - "disc": 62207, - "discord": 62208, - "display-fill": 62209, - "display": 62210, - "distribute-horizontal": 62211, - "distribute-vertical": 62212, - "door-closed-fill": 62213, - "door-closed": 62214, - "door-open-fill": 62215, - "door-open": 62216, - "dot": 62217, - "download": 62218, - "droplet-fill": 62219, - "droplet-half": 62220, - "droplet": 62221, - "earbuds": 62222, - "easel-fill": 62223, - "easel": 62224, - "egg-fill": 62225, - "egg-fried": 62226, - "egg": 62227, - "eject-fill": 62228, - "eject": 62229, - "emoji-angry-fill": 62230, - "emoji-angry": 62231, - "emoji-dizzy-fill": 62232, - "emoji-dizzy": 62233, - "emoji-expressionless-fill": 62234, - "emoji-expressionless": 62235, - "emoji-frown-fill": 62236, - "emoji-frown": 62237, - "emoji-heart-eyes-fill": 62238, - "emoji-heart-eyes": 62239, - "emoji-laughing-fill": 62240, - "emoji-laughing": 62241, - "emoji-neutral-fill": 62242, - "emoji-neutral": 62243, - "emoji-smile-fill": 62244, - "emoji-smile-upside-down-fill": 62245, - "emoji-smile-upside-down": 62246, - "emoji-smile": 62247, - "emoji-sunglasses-fill": 62248, - "emoji-sunglasses": 62249, - "emoji-wink-fill": 62250, - "emoji-wink": 62251, - "envelope-fill": 62252, - "envelope-open-fill": 62253, - "envelope-open": 62254, - "envelope": 62255, - "eraser-fill": 62256, - "eraser": 62257, - "exclamation-circle-fill": 62258, - "exclamation-circle": 62259, - "exclamation-diamond-fill": 62260, - "exclamation-diamond": 62261, - "exclamation-octagon-fill": 62262, - "exclamation-octagon": 62263, - "exclamation-square-fill": 62264, - "exclamation-square": 62265, - "exclamation-triangle-fill": 62266, - "exclamation-triangle": 62267, - "exclamation": 62268, - "exclude": 62269, - "eye-fill": 62270, - "eye-slash-fill": 62271, - "eye-slash": 62272, - "eye": 62273, - "eyedropper": 62274, - "eyeglasses": 62275, - "facebook": 62276, - "file-arrow-down-fill": 62277, - "file-arrow-down": 62278, - "file-arrow-up-fill": 62279, - "file-arrow-up": 62280, - "file-bar-graph-fill": 62281, - "file-bar-graph": 62282, - "file-binary-fill": 62283, - "file-binary": 62284, - "file-break-fill": 62285, - "file-break": 62286, - "file-check-fill": 62287, - "file-check": 62288, - "file-code-fill": 62289, - "file-code": 62290, - "file-diff-fill": 62291, - "file-diff": 62292, - "file-earmark-arrow-down-fill": 62293, - "file-earmark-arrow-down": 62294, - "file-earmark-arrow-up-fill": 62295, - "file-earmark-arrow-up": 62296, - "file-earmark-bar-graph-fill": 62297, - "file-earmark-bar-graph": 62298, - "file-earmark-binary-fill": 62299, - "file-earmark-binary": 62300, - "file-earmark-break-fill": 62301, - "file-earmark-break": 62302, - "file-earmark-check-fill": 62303, - "file-earmark-check": 62304, - "file-earmark-code-fill": 62305, - "file-earmark-code": 62306, - "file-earmark-diff-fill": 62307, - "file-earmark-diff": 62308, - "file-earmark-easel-fill": 62309, - "file-earmark-easel": 62310, - "file-earmark-excel-fill": 62311, - "file-earmark-excel": 62312, - "file-earmark-fill": 62313, - "file-earmark-font-fill": 62314, - "file-earmark-font": 62315, - "file-earmark-image-fill": 62316, - "file-earmark-image": 62317, - "file-earmark-lock-fill": 62318, - "file-earmark-lock": 62319, - "file-earmark-lock2-fill": 62320, - "file-earmark-lock2": 62321, - "file-earmark-medical-fill": 62322, - "file-earmark-medical": 62323, - "file-earmark-minus-fill": 62324, - "file-earmark-minus": 62325, - "file-earmark-music-fill": 62326, - "file-earmark-music": 62327, - "file-earmark-person-fill": 62328, - "file-earmark-person": 62329, - "file-earmark-play-fill": 62330, - "file-earmark-play": 62331, - "file-earmark-plus-fill": 62332, - "file-earmark-plus": 62333, - "file-earmark-post-fill": 62334, - "file-earmark-post": 62335, - "file-earmark-ppt-fill": 62336, - "file-earmark-ppt": 62337, - "file-earmark-richtext-fill": 62338, - "file-earmark-richtext": 62339, - "file-earmark-ruled-fill": 62340, - "file-earmark-ruled": 62341, - "file-earmark-slides-fill": 62342, - "file-earmark-slides": 62343, - "file-earmark-spreadsheet-fill": 62344, - "file-earmark-spreadsheet": 62345, - "file-earmark-text-fill": 62346, - "file-earmark-text": 62347, - "file-earmark-word-fill": 62348, - "file-earmark-word": 62349, - "file-earmark-x-fill": 62350, - "file-earmark-x": 62351, - "file-earmark-zip-fill": 62352, - "file-earmark-zip": 62353, - "file-earmark": 62354, - "file-easel-fill": 62355, - "file-easel": 62356, - "file-excel-fill": 62357, - "file-excel": 62358, - "file-fill": 62359, - "file-font-fill": 62360, - "file-font": 62361, - "file-image-fill": 62362, - "file-image": 62363, - "file-lock-fill": 62364, - "file-lock": 62365, - "file-lock2-fill": 62366, - "file-lock2": 62367, - "file-medical-fill": 62368, - "file-medical": 62369, - "file-minus-fill": 62370, - "file-minus": 62371, - "file-music-fill": 62372, - "file-music": 62373, - "file-person-fill": 62374, - "file-person": 62375, - "file-play-fill": 62376, - "file-play": 62377, - "file-plus-fill": 62378, - "file-plus": 62379, - "file-post-fill": 62380, - "file-post": 62381, - "file-ppt-fill": 62382, - "file-ppt": 62383, - "file-richtext-fill": 62384, - "file-richtext": 62385, - "file-ruled-fill": 62386, - "file-ruled": 62387, - "file-slides-fill": 62388, - "file-slides": 62389, - "file-spreadsheet-fill": 62390, - "file-spreadsheet": 62391, - "file-text-fill": 62392, - "file-text": 62393, - "file-word-fill": 62394, - "file-word": 62395, - "file-x-fill": 62396, - "file-x": 62397, - "file-zip-fill": 62398, - "file-zip": 62399, - "file": 62400, - "files-alt": 62401, - "files": 62402, - "film": 62403, - "filter-circle-fill": 62404, - "filter-circle": 62405, - "filter-left": 62406, - "filter-right": 62407, - "filter-square-fill": 62408, - "filter-square": 62409, - "filter": 62410, - "flag-fill": 62411, - "flag": 62412, - "flower1": 62413, - "flower2": 62414, - "flower3": 62415, - "folder-check": 62416, - "folder-fill": 62417, - "folder-minus": 62418, - "folder-plus": 62419, - "folder-symlink-fill": 62420, - "folder-symlink": 62421, - "folder-x": 62422, - "folder": 62423, - "folder2-open": 62424, - "folder2": 62425, - "fonts": 62426, - "forward-fill": 62427, - "forward": 62428, - "front": 62429, - "fullscreen-exit": 62430, - "fullscreen": 62431, - "funnel-fill": 62432, - "funnel": 62433, - "gear-fill": 62434, - "gear-wide-connected": 62435, - "gear-wide": 62436, - "gear": 62437, - "gem": 62438, - "geo-alt-fill": 62439, - "geo-alt": 62440, - "geo-fill": 62441, - "geo": 62442, - "gift-fill": 62443, - "gift": 62444, - "github": 62445, - "globe": 62446, - "globe2": 62447, - "google": 62448, - "graph-down": 62449, - "graph-up": 62450, - "grid-1x2-fill": 62451, - "grid-1x2": 62452, - "grid-3x2-gap-fill": 62453, - "grid-3x2-gap": 62454, - "grid-3x2": 62455, - "grid-3x3-gap-fill": 62456, - "grid-3x3-gap": 62457, - "grid-3x3": 62458, - "grid-fill": 62459, - "grid": 62460, - "grip-horizontal": 62461, - "grip-vertical": 62462, - "hammer": 62463, - "hand-index-fill": 62464, - "hand-index-thumb-fill": 62465, - "hand-index-thumb": 62466, - "hand-index": 62467, - "hand-thumbs-down-fill": 62468, - "hand-thumbs-down": 62469, - "hand-thumbs-up-fill": 62470, - "hand-thumbs-up": 62471, - "handbag-fill": 62472, - "handbag": 62473, - "hash": 62474, - "hdd-fill": 62475, - "hdd-network-fill": 62476, - "hdd-network": 62477, - "hdd-rack-fill": 62478, - "hdd-rack": 62479, - "hdd-stack-fill": 62480, - "hdd-stack": 62481, - "hdd": 62482, - "headphones": 62483, - "headset": 62484, - "heart-fill": 62485, - "heart-half": 62486, - "heart": 62487, - "heptagon-fill": 62488, - "heptagon-half": 62489, - "heptagon": 62490, - "hexagon-fill": 62491, - "hexagon-half": 62492, - "hexagon": 62493, - "hourglass-bottom": 62494, - "hourglass-split": 62495, - "hourglass-top": 62496, - "hourglass": 62497, - "house-door-fill": 62498, - "house-door": 62499, - "house-fill": 62500, - "house": 62501, - "hr": 62502, - "hurricane": 62503, - "image-alt": 62504, - "image-fill": 62505, - "image": 62506, - "images": 62507, - "inbox-fill": 62508, - "inbox": 62509, - "inboxes-fill": 62510, - "inboxes": 62511, - "info-circle-fill": 62512, - "info-circle": 62513, - "info-square-fill": 62514, - "info-square": 62515, - "info": 62516, - "input-cursor-text": 62517, - "input-cursor": 62518, - "instagram": 62519, - "intersect": 62520, - "journal-album": 62521, - "journal-arrow-down": 62522, - "journal-arrow-up": 62523, - "journal-bookmark-fill": 62524, - "journal-bookmark": 62525, - "journal-check": 62526, - "journal-code": 62527, - "journal-medical": 62528, - "journal-minus": 62529, - "journal-plus": 62530, - "journal-richtext": 62531, - "journal-text": 62532, - "journal-x": 62533, - "journal": 62534, - "journals": 62535, - "joystick": 62536, - "justify-left": 62537, - "justify-right": 62538, - "justify": 62539, - "kanban-fill": 62540, - "kanban": 62541, - "key-fill": 62542, - "key": 62543, - "keyboard-fill": 62544, - "keyboard": 62545, - "ladder": 62546, - "lamp-fill": 62547, - "lamp": 62548, - "laptop-fill": 62549, - "laptop": 62550, - "layer-backward": 62551, - "layer-forward": 62552, - "layers-fill": 62553, - "layers-half": 62554, - "layers": 62555, - "layout-sidebar-inset-reverse": 62556, - "layout-sidebar-inset": 62557, - "layout-sidebar-reverse": 62558, - "layout-sidebar": 62559, - "layout-split": 62560, - "layout-text-sidebar-reverse": 62561, - "layout-text-sidebar": 62562, - "layout-text-window-reverse": 62563, - "layout-text-window": 62564, - "layout-three-columns": 62565, - "layout-wtf": 62566, - "life-preserver": 62567, - "lightbulb-fill": 62568, - "lightbulb-off-fill": 62569, - "lightbulb-off": 62570, - "lightbulb": 62571, - "lightning-charge-fill": 62572, - "lightning-charge": 62573, - "lightning-fill": 62574, - "lightning": 62575, - "link-45deg": 62576, - "link": 62577, - "linkedin": 62578, - "list-check": 62579, - "list-nested": 62580, - "list-ol": 62581, - "list-stars": 62582, - "list-task": 62583, - "list-ul": 62584, - "list": 62585, - "lock-fill": 62586, - "lock": 62587, - "mailbox": 62588, - "mailbox2": 62589, - "map-fill": 62590, - "map": 62591, - "markdown-fill": 62592, - "markdown": 62593, - "mask": 62594, - "megaphone-fill": 62595, - "megaphone": 62596, - "menu-app-fill": 62597, - "menu-app": 62598, - "menu-button-fill": 62599, - "menu-button-wide-fill": 62600, - "menu-button-wide": 62601, - "menu-button": 62602, - "menu-down": 62603, - "menu-up": 62604, - "mic-fill": 62605, - "mic-mute-fill": 62606, - "mic-mute": 62607, - "mic": 62608, - "minecart-loaded": 62609, - "minecart": 62610, - "moisture": 62611, - "moon-fill": 62612, - "moon-stars-fill": 62613, - "moon-stars": 62614, - "moon": 62615, - "mouse-fill": 62616, - "mouse": 62617, - "mouse2-fill": 62618, - "mouse2": 62619, - "mouse3-fill": 62620, - "mouse3": 62621, - "music-note-beamed": 62622, - "music-note-list": 62623, - "music-note": 62624, - "music-player-fill": 62625, - "music-player": 62626, - "newspaper": 62627, - "node-minus-fill": 62628, - "node-minus": 62629, - "node-plus-fill": 62630, - "node-plus": 62631, - "nut-fill": 62632, - "nut": 62633, - "octagon-fill": 62634, - "octagon-half": 62635, - "octagon": 62636, - "option": 62637, - "outlet": 62638, - "paint-bucket": 62639, - "palette-fill": 62640, - "palette": 62641, - "palette2": 62642, - "paperclip": 62643, - "paragraph": 62644, - "patch-check-fill": 62645, - "patch-check": 62646, - "patch-exclamation-fill": 62647, - "patch-exclamation": 62648, - "patch-minus-fill": 62649, - "patch-minus": 62650, - "patch-plus-fill": 62651, - "patch-plus": 62652, - "patch-question-fill": 62653, - "patch-question": 62654, - "pause-btn-fill": 62655, - "pause-btn": 62656, - "pause-circle-fill": 62657, - "pause-circle": 62658, - "pause-fill": 62659, - "pause": 62660, - "peace-fill": 62661, - "peace": 62662, - "pen-fill": 62663, - "pen": 62664, - "pencil-fill": 62665, - "pencil-square": 62666, - "pencil": 62667, - "pentagon-fill": 62668, - "pentagon-half": 62669, - "pentagon": 62670, - "people-fill": 62671, - "people": 62672, - "percent": 62673, - "person-badge-fill": 62674, - "person-badge": 62675, - "person-bounding-box": 62676, - "person-check-fill": 62677, - "person-check": 62678, - "person-circle": 62679, - "person-dash-fill": 62680, - "person-dash": 62681, - "person-fill": 62682, - "person-lines-fill": 62683, - "person-plus-fill": 62684, - "person-plus": 62685, - "person-square": 62686, - "person-x-fill": 62687, - "person-x": 62688, - "person": 62689, - "phone-fill": 62690, - "phone-landscape-fill": 62691, - "phone-landscape": 62692, - "phone-vibrate-fill": 62693, - "phone-vibrate": 62694, - "phone": 62695, - "pie-chart-fill": 62696, - "pie-chart": 62697, - "pin-angle-fill": 62698, - "pin-angle": 62699, - "pin-fill": 62700, - "pin": 62701, - "pip-fill": 62702, - "pip": 62703, - "play-btn-fill": 62704, - "play-btn": 62705, - "play-circle-fill": 62706, - "play-circle": 62707, - "play-fill": 62708, - "play": 62709, - "plug-fill": 62710, - "plug": 62711, - "plus-circle-dotted": 62712, - "plus-circle-fill": 62713, - "plus-circle": 62714, - "plus-square-dotted": 62715, - "plus-square-fill": 62716, - "plus-square": 62717, - "plus": 62718, - "power": 62719, - "printer-fill": 62720, - "printer": 62721, - "puzzle-fill": 62722, - "puzzle": 62723, - "question-circle-fill": 62724, - "question-circle": 62725, - "question-diamond-fill": 62726, - "question-diamond": 62727, - "question-octagon-fill": 62728, - "question-octagon": 62729, - "question-square-fill": 62730, - "question-square": 62731, - "question": 62732, - "rainbow": 62733, - "receipt-cutoff": 62734, - "receipt": 62735, - "reception-0": 62736, - "reception-1": 62737, - "reception-2": 62738, - "reception-3": 62739, - "reception-4": 62740, - "record-btn-fill": 62741, - "record-btn": 62742, - "record-circle-fill": 62743, - "record-circle": 62744, - "record-fill": 62745, - "record": 62746, - "record2-fill": 62747, - "record2": 62748, - "reply-all-fill": 62749, - "reply-all": 62750, - "reply-fill": 62751, - "reply": 62752, - "rss-fill": 62753, - "rss": 62754, - "rulers": 62755, - "save-fill": 62756, - "save": 62757, - "save2-fill": 62758, - "save2": 62759, - "scissors": 62760, - "screwdriver": 62761, - "search": 62762, - "segmented-nav": 62763, - "server": 62764, - "share-fill": 62765, - "share": 62766, - "shield-check": 62767, - "shield-exclamation": 62768, - "shield-fill-check": 62769, - "shield-fill-exclamation": 62770, - "shield-fill-minus": 62771, - "shield-fill-plus": 62772, - "shield-fill-x": 62773, - "shield-fill": 62774, - "shield-lock-fill": 62775, - "shield-lock": 62776, - "shield-minus": 62777, - "shield-plus": 62778, - "shield-shaded": 62779, - "shield-slash-fill": 62780, - "shield-slash": 62781, - "shield-x": 62782, - "shield": 62783, - "shift-fill": 62784, - "shift": 62785, - "shop-window": 62786, - "shop": 62787, - "shuffle": 62788, - "signpost-2-fill": 62789, - "signpost-2": 62790, - "signpost-fill": 62791, - "signpost-split-fill": 62792, - "signpost-split": 62793, - "signpost": 62794, - "sim-fill": 62795, - "sim": 62796, - "skip-backward-btn-fill": 62797, - "skip-backward-btn": 62798, - "skip-backward-circle-fill": 62799, - "skip-backward-circle": 62800, - "skip-backward-fill": 62801, - "skip-backward": 62802, - "skip-end-btn-fill": 62803, - "skip-end-btn": 62804, - "skip-end-circle-fill": 62805, - "skip-end-circle": 62806, - "skip-end-fill": 62807, - "skip-end": 62808, - "skip-forward-btn-fill": 62809, - "skip-forward-btn": 62810, - "skip-forward-circle-fill": 62811, - "skip-forward-circle": 62812, - "skip-forward-fill": 62813, - "skip-forward": 62814, - "skip-start-btn-fill": 62815, - "skip-start-btn": 62816, - "skip-start-circle-fill": 62817, - "skip-start-circle": 62818, - "skip-start-fill": 62819, - "skip-start": 62820, - "slack": 62821, - "slash-circle-fill": 62822, - "slash-circle": 62823, - "slash-square-fill": 62824, - "slash-square": 62825, - "slash": 62826, - "sliders": 62827, - "smartwatch": 62828, - "snow": 62829, - "snow2": 62830, - "snow3": 62831, - "sort-alpha-down-alt": 62832, - "sort-alpha-down": 62833, - "sort-alpha-up-alt": 62834, - "sort-alpha-up": 62835, - "sort-down-alt": 62836, - "sort-down": 62837, - "sort-numeric-down-alt": 62838, - "sort-numeric-down": 62839, - "sort-numeric-up-alt": 62840, - "sort-numeric-up": 62841, - "sort-up-alt": 62842, - "sort-up": 62843, - "soundwave": 62844, - "speaker-fill": 62845, - "speaker": 62846, - "speedometer": 62847, - "speedometer2": 62848, - "spellcheck": 62849, - "square-fill": 62850, - "square-half": 62851, - "square": 62852, - "stack": 62853, - "star-fill": 62854, - "star-half": 62855, - "star": 62856, - "stars": 62857, - "stickies-fill": 62858, - "stickies": 62859, - "sticky-fill": 62860, - "sticky": 62861, - "stop-btn-fill": 62862, - "stop-btn": 62863, - "stop-circle-fill": 62864, - "stop-circle": 62865, - "stop-fill": 62866, - "stop": 62867, - "stoplights-fill": 62868, - "stoplights": 62869, - "stopwatch-fill": 62870, - "stopwatch": 62871, - "subtract": 62872, - "suit-club-fill": 62873, - "suit-club": 62874, - "suit-diamond-fill": 62875, - "suit-diamond": 62876, - "suit-heart-fill": 62877, - "suit-heart": 62878, - "suit-spade-fill": 62879, - "suit-spade": 62880, - "sun-fill": 62881, - "sun": 62882, - "sunglasses": 62883, - "sunrise-fill": 62884, - "sunrise": 62885, - "sunset-fill": 62886, - "sunset": 62887, - "symmetry-horizontal": 62888, - "symmetry-vertical": 62889, - "table": 62890, - "tablet-fill": 62891, - "tablet-landscape-fill": 62892, - "tablet-landscape": 62893, - "tablet": 62894, - "tag-fill": 62895, - "tag": 62896, - "tags-fill": 62897, - "tags": 62898, - "telegram": 62899, - "telephone-fill": 62900, - "telephone-forward-fill": 62901, - "telephone-forward": 62902, - "telephone-inbound-fill": 62903, - "telephone-inbound": 62904, - "telephone-minus-fill": 62905, - "telephone-minus": 62906, - "telephone-outbound-fill": 62907, - "telephone-outbound": 62908, - "telephone-plus-fill": 62909, - "telephone-plus": 62910, - "telephone-x-fill": 62911, - "telephone-x": 62912, - "telephone": 62913, - "terminal-fill": 62914, - "terminal": 62915, - "text-center": 62916, - "text-indent-left": 62917, - "text-indent-right": 62918, - "text-left": 62919, - "text-paragraph": 62920, - "text-right": 62921, - "textarea-resize": 62922, - "textarea-t": 62923, - "textarea": 62924, - "thermometer-half": 62925, - "thermometer-high": 62926, - "thermometer-low": 62927, - "thermometer-snow": 62928, - "thermometer-sun": 62929, - "thermometer": 62930, - "three-dots-vertical": 62931, - "three-dots": 62932, - "toggle-off": 62933, - "toggle-on": 62934, - "toggle2-off": 62935, - "toggle2-on": 62936, - "toggles": 62937, - "toggles2": 62938, - "tools": 62939, - "tornado": 62940, - "trash-fill": 62941, - "trash": 62942, - "trash2-fill": 62943, - "trash2": 62944, - "tree-fill": 62945, - "tree": 62946, - "triangle-fill": 62947, - "triangle-half": 62948, - "triangle": 62949, - "trophy-fill": 62950, - "trophy": 62951, - "tropical-storm": 62952, - "truck-flatbed": 62953, - "truck": 62954, - "tsunami": 62955, - "tv-fill": 62956, - "tv": 62957, - "twitch": 62958, - "twitter": 62959, - "type-bold": 62960, - "type-h1": 62961, - "type-h2": 62962, - "type-h3": 62963, - "type-italic": 62964, - "type-strikethrough": 62965, - "type-underline": 62966, - "type": 62967, - "ui-checks-grid": 62968, - "ui-checks": 62969, - "ui-radios-grid": 62970, - "ui-radios": 62971, - "umbrella-fill": 62972, - "umbrella": 62973, - "union": 62974, - "unlock-fill": 62975, - "unlock": 62976, - "upc-scan": 62977, - "upc": 62978, - "upload": 62979, - "vector-pen": 62980, - "view-list": 62981, - "view-stacked": 62982, - "vinyl-fill": 62983, - "vinyl": 62984, - "voicemail": 62985, - "volume-down-fill": 62986, - "volume-down": 62987, - "volume-mute-fill": 62988, - "volume-mute": 62989, - "volume-off-fill": 62990, - "volume-off": 62991, - "volume-up-fill": 62992, - "volume-up": 62993, - "vr": 62994, - "wallet-fill": 62995, - "wallet": 62996, - "wallet2": 62997, - "watch": 62998, - "water": 62999, - "whatsapp": 63000, - "wifi-1": 63001, - "wifi-2": 63002, - "wifi-off": 63003, - "wifi": 63004, - "wind": 63005, - "window-dock": 63006, - "window-sidebar": 63007, - "window": 63008, - "wrench": 63009, - "x-circle-fill": 63010, - "x-circle": 63011, - "x-diamond-fill": 63012, - "x-diamond": 63013, - "x-octagon-fill": 63014, - "x-octagon": 63015, - "x-square-fill": 63016, - "x-square": 63017, - "x": 63018, - "youtube": 63019, - "zoom-in": 63020, - "zoom-out": 63021, - "bank": 63022, - "bank2": 63023, - "bell-slash-fill": 63024, - "bell-slash": 63025, - "cash-coin": 63026, - "check-lg": 63027, - "coin": 63028, - "currency-bitcoin": 63029, - "currency-dollar": 63030, - "currency-euro": 63031, - "currency-exchange": 63032, - "currency-pound": 63033, - "currency-yen": 63034, - "dash-lg": 63035, - "exclamation-lg": 63036, - "file-earmark-pdf-fill": 63037, - "file-earmark-pdf": 63038, - "file-pdf-fill": 63039, - "file-pdf": 63040, - "gender-ambiguous": 63041, - "gender-female": 63042, - "gender-male": 63043, - "gender-trans": 63044, - "headset-vr": 63045, - "info-lg": 63046, - "mastodon": 63047, - "messenger": 63048, - "piggy-bank-fill": 63049, - "piggy-bank": 63050, - "pin-map-fill": 63051, - "pin-map": 63052, - "plus-lg": 63053, - "question-lg": 63054, - "recycle": 63055, - "reddit": 63056, - "safe-fill": 63057, - "safe2-fill": 63058, - "safe2": 63059, - "sd-card-fill": 63060, - "sd-card": 63061, - "skype": 63062, - "slash-lg": 63063, - "translate": 63064, - "x-lg": 63065, - "safe": 63066, - "apple": 63067, - "microsoft": 63069, - "windows": 63070, - "behance": 63068, - "dribbble": 63071, - "line": 63072, - "medium": 63073, - "paypal": 63074, - "pinterest": 63075, - "signal": 63076, - "snapchat": 63077, - "spotify": 63078, - "stack-overflow": 63079, - "strava": 63080, - "wordpress": 63081, - "vimeo": 63082, - "activity": 63083, - "easel2-fill": 63084, - "easel2": 63085, - "easel3-fill": 63086, - "easel3": 63087, - "fan": 63088, - "fingerprint": 63089, - "graph-down-arrow": 63090, - "graph-up-arrow": 63091, - "hypnotize": 63092, - "magic": 63093, - "person-rolodex": 63094, - "person-video": 63095, - "person-video2": 63096, - "person-video3": 63097, - "person-workspace": 63098, - "radioactive": 63099, - "webcam-fill": 63100, - "webcam": 63101, - "yin-yang": 63102, - "bandaid-fill": 63104, - "bandaid": 63105, - "bluetooth": 63106, - "body-text": 63107, - "boombox": 63108, - "boxes": 63109, - "dpad-fill": 63110, - "dpad": 63111, - "ear-fill": 63112, - "ear": 63113, - "envelope-check-fill": 63115, - "envelope-check": 63116, - "envelope-dash-fill": 63118, - "envelope-dash": 63119, - "envelope-exclamation-fill": 63121, - "envelope-exclamation": 63122, - "envelope-plus-fill": 63123, - "envelope-plus": 63124, - "envelope-slash-fill": 63126, - "envelope-slash": 63127, - "envelope-x-fill": 63129, - "envelope-x": 63130, - "explicit-fill": 63131, - "explicit": 63132, - "git": 63133, - "infinity": 63134, - "list-columns-reverse": 63135, - "list-columns": 63136, - "meta": 63137, - "nintendo-switch": 63140, - "pc-display-horizontal": 63141, - "pc-display": 63142, - "pc-horizontal": 63143, - "pc": 63144, - "playstation": 63145, - "plus-slash-minus": 63146, - "projector-fill": 63147, - "projector": 63148, - "qr-code-scan": 63149, - "qr-code": 63150, - "quora": 63151, - "quote": 63152, - "robot": 63153, - "send-check-fill": 63154, - "send-check": 63155, - "send-dash-fill": 63156, - "send-dash": 63157, - "send-exclamation-fill": 63159, - "send-exclamation": 63160, - "send-fill": 63161, - "send-plus-fill": 63162, - "send-plus": 63163, - "send-slash-fill": 63164, - "send-slash": 63165, - "send-x-fill": 63166, - "send-x": 63167, - "send": 63168, - "steam": 63169, - "terminal-dash": 63171, - "terminal-plus": 63172, - "terminal-split": 63173, - "ticket-detailed-fill": 63174, - "ticket-detailed": 63175, - "ticket-fill": 63176, - "ticket-perforated-fill": 63177, - "ticket-perforated": 63178, - "ticket": 63179, - "tiktok": 63180, - "window-dash": 63181, - "window-desktop": 63182, - "window-fullscreen": 63183, - "window-plus": 63184, - "window-split": 63185, - "window-stack": 63186, - "window-x": 63187, - "xbox": 63188, - "ethernet": 63189, - "hdmi-fill": 63190, - "hdmi": 63191, - "usb-c-fill": 63192, - "usb-c": 63193, - "usb-fill": 63194, - "usb-plug-fill": 63195, - "usb-plug": 63196, - "usb-symbol": 63197, - "usb": 63198, - "boombox-fill": 63199, - "displayport": 63201, - "gpu-card": 63202, - "memory": 63203, - "modem-fill": 63204, - "modem": 63205, - "motherboard-fill": 63206, - "motherboard": 63207, - "optical-audio-fill": 63208, - "optical-audio": 63209, - "pci-card": 63210, - "router-fill": 63211, - "router": 63212, - "thunderbolt-fill": 63215, - "thunderbolt": 63216, - "usb-drive-fill": 63217, - "usb-drive": 63218, - "usb-micro-fill": 63219, - "usb-micro": 63220, - "usb-mini-fill": 63221, - "usb-mini": 63222, - "cloud-haze2": 63223, - "device-hdd-fill": 63224, - "device-hdd": 63225, - "device-ssd-fill": 63226, - "device-ssd": 63227, - "displayport-fill": 63228, - "mortarboard-fill": 63229, - "mortarboard": 63230, - "terminal-x": 63231, - "arrow-through-heart-fill": 63232, - "arrow-through-heart": 63233, - "badge-sd-fill": 63234, - "badge-sd": 63235, - "bag-heart-fill": 63236, - "bag-heart": 63237, - "balloon-fill": 63238, - "balloon-heart-fill": 63239, - "balloon-heart": 63240, - "balloon": 63241, - "box2-fill": 63242, - "box2-heart-fill": 63243, - "box2-heart": 63244, - "box2": 63245, - "braces-asterisk": 63246, - "calendar-heart-fill": 63247, - "calendar-heart": 63248, - "calendar2-heart-fill": 63249, - "calendar2-heart": 63250, - "chat-heart-fill": 63251, - "chat-heart": 63252, - "chat-left-heart-fill": 63253, - "chat-left-heart": 63254, - "chat-right-heart-fill": 63255, - "chat-right-heart": 63256, - "chat-square-heart-fill": 63257, - "chat-square-heart": 63258, - "clipboard-check-fill": 63259, - "clipboard-data-fill": 63260, - "clipboard-fill": 63261, - "clipboard-heart-fill": 63262, - "clipboard-heart": 63263, - "clipboard-minus-fill": 63264, - "clipboard-plus-fill": 63265, - "clipboard-pulse": 63266, - "clipboard-x-fill": 63267, - "clipboard2-check-fill": 63268, - "clipboard2-check": 63269, - "clipboard2-data-fill": 63270, - "clipboard2-data": 63271, - "clipboard2-fill": 63272, - "clipboard2-heart-fill": 63273, - "clipboard2-heart": 63274, - "clipboard2-minus-fill": 63275, - "clipboard2-minus": 63276, - "clipboard2-plus-fill": 63277, - "clipboard2-plus": 63278, - "clipboard2-pulse-fill": 63279, - "clipboard2-pulse": 63280, - "clipboard2-x-fill": 63281, - "clipboard2-x": 63282, - "clipboard2": 63283, - "emoji-kiss-fill": 63284, - "emoji-kiss": 63285, - "envelope-heart-fill": 63286, - "envelope-heart": 63287, - "envelope-open-heart-fill": 63288, - "envelope-open-heart": 63289, - "envelope-paper-fill": 63290, - "envelope-paper-heart-fill": 63291, - "envelope-paper-heart": 63292, - "envelope-paper": 63293, - "filetype-aac": 63294, - "filetype-ai": 63295, - "filetype-bmp": 63296, - "filetype-cs": 63297, - "filetype-css": 63298, - "filetype-csv": 63299, - "filetype-doc": 63300, - "filetype-docx": 63301, - "filetype-exe": 63302, - "filetype-gif": 63303, - "filetype-heic": 63304, - "filetype-html": 63305, - "filetype-java": 63306, - "filetype-jpg": 63307, - "filetype-js": 63308, - "filetype-jsx": 63309, - "filetype-key": 63310, - "filetype-m4p": 63311, - "filetype-md": 63312, - "filetype-mdx": 63313, - "filetype-mov": 63314, - "filetype-mp3": 63315, - "filetype-mp4": 63316, - "filetype-otf": 63317, - "filetype-pdf": 63318, - "filetype-php": 63319, - "filetype-png": 63320, - "filetype-ppt": 63322, - "filetype-psd": 63323, - "filetype-py": 63324, - "filetype-raw": 63325, - "filetype-rb": 63326, - "filetype-sass": 63327, - "filetype-scss": 63328, - "filetype-sh": 63329, - "filetype-svg": 63330, - "filetype-tiff": 63331, - "filetype-tsx": 63332, - "filetype-ttf": 63333, - "filetype-txt": 63334, - "filetype-wav": 63335, - "filetype-woff": 63336, - "filetype-xls": 63338, - "filetype-xml": 63339, - "filetype-yml": 63340, - "heart-arrow": 63341, - "heart-pulse-fill": 63342, - "heart-pulse": 63343, - "heartbreak-fill": 63344, - "heartbreak": 63345, - "hearts": 63346, - "hospital-fill": 63347, - "hospital": 63348, - "house-heart-fill": 63349, - "house-heart": 63350, - "incognito": 63351, - "magnet-fill": 63352, - "magnet": 63353, - "person-heart": 63354, - "person-hearts": 63355, - "phone-flip": 63356, - "plugin": 63357, - "postage-fill": 63358, - "postage-heart-fill": 63359, - "postage-heart": 63360, - "postage": 63361, - "postcard-fill": 63362, - "postcard-heart-fill": 63363, - "postcard-heart": 63364, - "postcard": 63365, - "search-heart-fill": 63366, - "search-heart": 63367, - "sliders2-vertical": 63368, - "sliders2": 63369, - "trash3-fill": 63370, - "trash3": 63371, - "valentine": 63372, - "valentine2": 63373, - "wrench-adjustable-circle-fill": 63374, - "wrench-adjustable-circle": 63375, - "wrench-adjustable": 63376, - "filetype-json": 63377, - "filetype-pptx": 63378, - "filetype-xlsx": 63379, - "1-circle-fill": 63382, - "1-circle": 63383, - "1-square-fill": 63384, - "1-square": 63385, - "2-circle-fill": 63388, - "2-circle": 63389, - "2-square-fill": 63390, - "2-square": 63391, - "3-circle-fill": 63394, - "3-circle": 63395, - "3-square-fill": 63396, - "3-square": 63397, - "4-circle-fill": 63400, - "4-circle": 63401, - "4-square-fill": 63402, - "4-square": 63403, - "5-circle-fill": 63406, - "5-circle": 63407, - "5-square-fill": 63408, - "5-square": 63409, - "6-circle-fill": 63412, - "6-circle": 63413, - "6-square-fill": 63414, - "6-square": 63415, - "7-circle-fill": 63418, - "7-circle": 63419, - "7-square-fill": 63420, - "7-square": 63421, - "8-circle-fill": 63424, - "8-circle": 63425, - "8-square-fill": 63426, - "8-square": 63427, - "9-circle-fill": 63430, - "9-circle": 63431, - "9-square-fill": 63432, - "9-square": 63433, - "airplane-engines-fill": 63434, - "airplane-engines": 63435, - "airplane-fill": 63436, - "airplane": 63437, - "alexa": 63438, - "alipay": 63439, - "android": 63440, - "android2": 63441, - "box-fill": 63442, - "box-seam-fill": 63443, - "browser-chrome": 63444, - "browser-edge": 63445, - "browser-firefox": 63446, - "browser-safari": 63447, - "c-circle-fill": 63450, - "c-circle": 63451, - "c-square-fill": 63452, - "c-square": 63453, - "capsule-pill": 63454, - "capsule": 63455, - "car-front-fill": 63456, - "car-front": 63457, - "cassette-fill": 63458, - "cassette": 63459, - "cc-circle-fill": 63462, - "cc-circle": 63463, - "cc-square-fill": 63464, - "cc-square": 63465, - "cup-hot-fill": 63466, - "cup-hot": 63467, - "currency-rupee": 63468, - "dropbox": 63469, - "escape": 63470, - "fast-forward-btn-fill": 63471, - "fast-forward-btn": 63472, - "fast-forward-circle-fill": 63473, - "fast-forward-circle": 63474, - "fast-forward-fill": 63475, - "fast-forward": 63476, - "filetype-sql": 63477, - "fire": 63478, - "google-play": 63479, - "h-circle-fill": 63482, - "h-circle": 63483, - "h-square-fill": 63484, - "h-square": 63485, - "indent": 63486, - "lungs-fill": 63487, - "lungs": 63488, - "microsoft-teams": 63489, - "p-circle-fill": 63492, - "p-circle": 63493, - "p-square-fill": 63494, - "p-square": 63495, - "pass-fill": 63496, - "pass": 63497, - "prescription": 63498, - "prescription2": 63499, - "r-circle-fill": 63502, - "r-circle": 63503, - "r-square-fill": 63504, - "r-square": 63505, - "repeat-1": 63506, - "repeat": 63507, - "rewind-btn-fill": 63508, - "rewind-btn": 63509, - "rewind-circle-fill": 63510, - "rewind-circle": 63511, - "rewind-fill": 63512, - "rewind": 63513, - "train-freight-front-fill": 63514, - "train-freight-front": 63515, - "train-front-fill": 63516, - "train-front": 63517, - "train-lightrail-front-fill": 63518, - "train-lightrail-front": 63519, - "truck-front-fill": 63520, - "truck-front": 63521, - "ubuntu": 63522, - "unindent": 63523, - "unity": 63524, - "universal-access-circle": 63525, - "universal-access": 63526, - "virus": 63527, - "virus2": 63528, - "wechat": 63529, - "yelp": 63530, - "sign-stop-fill": 63531, - "sign-stop-lights-fill": 63532, - "sign-stop-lights": 63533, - "sign-stop": 63534, - "sign-turn-left-fill": 63535, - "sign-turn-left": 63536, - "sign-turn-right-fill": 63537, - "sign-turn-right": 63538, - "sign-turn-slight-left-fill": 63539, - "sign-turn-slight-left": 63540, - "sign-turn-slight-right-fill": 63541, - "sign-turn-slight-right": 63542, - "sign-yield-fill": 63543, - "sign-yield": 63544, - "ev-station-fill": 63545, - "ev-station": 63546, - "fuel-pump-diesel-fill": 63547, - "fuel-pump-diesel": 63548, - "fuel-pump-fill": 63549, - "fuel-pump": 63550, - "0-circle-fill": 63551, - "0-circle": 63552, - "0-square-fill": 63553, - "0-square": 63554, - "rocket-fill": 63555, - "rocket-takeoff-fill": 63556, - "rocket-takeoff": 63557, - "rocket": 63558, - "stripe": 63559, - "subscript": 63560, - "superscript": 63561, - "trello": 63562, - "envelope-at-fill": 63563, - "envelope-at": 63564, - "regex": 63565, - "text-wrap": 63566, - "sign-dead-end-fill": 63567, - "sign-dead-end": 63568, - "sign-do-not-enter-fill": 63569, - "sign-do-not-enter": 63570, - "sign-intersection-fill": 63571, - "sign-intersection-side-fill": 63572, - "sign-intersection-side": 63573, - "sign-intersection-t-fill": 63574, - "sign-intersection-t": 63575, - "sign-intersection-y-fill": 63576, - "sign-intersection-y": 63577, - "sign-intersection": 63578, - "sign-merge-left-fill": 63579, - "sign-merge-left": 63580, - "sign-merge-right-fill": 63581, - "sign-merge-right": 63582, - "sign-no-left-turn-fill": 63583, - "sign-no-left-turn": 63584, - "sign-no-parking-fill": 63585, - "sign-no-parking": 63586, - "sign-no-right-turn-fill": 63587, - "sign-no-right-turn": 63588, - "sign-railroad-fill": 63589, - "sign-railroad": 63590, - "building-add": 63591, - "building-check": 63592, - "building-dash": 63593, - "building-down": 63594, - "building-exclamation": 63595, - "building-fill-add": 63596, - "building-fill-check": 63597, - "building-fill-dash": 63598, - "building-fill-down": 63599, - "building-fill-exclamation": 63600, - "building-fill-gear": 63601, - "building-fill-lock": 63602, - "building-fill-slash": 63603, - "building-fill-up": 63604, - "building-fill-x": 63605, - "building-fill": 63606, - "building-gear": 63607, - "building-lock": 63608, - "building-slash": 63609, - "building-up": 63610, - "building-x": 63611, - "buildings-fill": 63612, - "buildings": 63613, - "bus-front-fill": 63614, - "bus-front": 63615, - "ev-front-fill": 63616, - "ev-front": 63617, - "globe-americas": 63618, - "globe-asia-australia": 63619, - "globe-central-south-asia": 63620, - "globe-europe-africa": 63621, - "house-add-fill": 63622, - "house-add": 63623, - "house-check-fill": 63624, - "house-check": 63625, - "house-dash-fill": 63626, - "house-dash": 63627, - "house-down-fill": 63628, - "house-down": 63629, - "house-exclamation-fill": 63630, - "house-exclamation": 63631, - "house-gear-fill": 63632, - "house-gear": 63633, - "house-lock-fill": 63634, - "house-lock": 63635, - "house-slash-fill": 63636, - "house-slash": 63637, - "house-up-fill": 63638, - "house-up": 63639, - "house-x-fill": 63640, - "house-x": 63641, - "person-add": 63642, - "person-down": 63643, - "person-exclamation": 63644, - "person-fill-add": 63645, - "person-fill-check": 63646, - "person-fill-dash": 63647, - "person-fill-down": 63648, - "person-fill-exclamation": 63649, - "person-fill-gear": 63650, - "person-fill-lock": 63651, - "person-fill-slash": 63652, - "person-fill-up": 63653, - "person-fill-x": 63654, - "person-gear": 63655, - "person-lock": 63656, - "person-slash": 63657, - "person-up": 63658, - "scooter": 63659, - "taxi-front-fill": 63660, - "taxi-front": 63661, - "amd": 63662, - "database-add": 63663, - "database-check": 63664, - "database-dash": 63665, - "database-down": 63666, - "database-exclamation": 63667, - "database-fill-add": 63668, - "database-fill-check": 63669, - "database-fill-dash": 63670, - "database-fill-down": 63671, - "database-fill-exclamation": 63672, - "database-fill-gear": 63673, - "database-fill-lock": 63674, - "database-fill-slash": 63675, - "database-fill-up": 63676, - "database-fill-x": 63677, - "database-fill": 63678, - "database-gear": 63679, - "database-lock": 63680, - "database-slash": 63681, - "database-up": 63682, - "database-x": 63683, - "database": 63684, - "houses-fill": 63685, - "houses": 63686, - "nvidia": 63687, - "person-vcard-fill": 63688, - "person-vcard": 63689, - "sina-weibo": 63690, - "tencent-qq": 63691, - "wikipedia": 63692 -} \ No newline at end of file diff --git a/web/__init__.py b/web/__init__.py index a50ececc..53bc32f4 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -13,11 +13,6 @@ import web.platform import web.util -import web.service.pppp -import web.service.video -import web.service.mqtt -import web.service.filetransfer - import cli.util import cli.config @@ -35,6 +30,7 @@ sock = Sock(app) +<<<<<<< HEAD # autopep8: off import web.service.pppp @@ -42,6 +38,12 @@ import web.service.mqtt import web.service.filetransfer # autopep8: on +======= +import web.service.pppp # nopep8 +import web.service.video # nopep8 +import web.service.mqtt # nopep8 +import web.service.filetransfer # nopep8 +>>>>>>> 4c82ae5 (Updating front-end) @app.before_first_request @@ -49,7 +51,8 @@ def startup(): app.svc.register("pppp", web.service.pppp.PPPPService()) app.svc.register("videoqueue", web.service.video.VideoQueue()) app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) - app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) + app.svc.register( + "filetransfer", web.service.filetransfer.FileTransferService()) def shutdown(): @@ -57,8 +60,8 @@ def shutdown(): app.svc.unregister("videoqueue") app.svc.unregister("mqttqueue") app.svc.unregister("filetransfer") - - + + def restart(): shutdown() startup() @@ -115,16 +118,19 @@ def app_root(): user_agent = user_agent_parse(request.headers.get('User-Agent')) host = request.host.split(':') - request_port = host[1] if len(host) > 1 else '80' # If there is no 2nd array entry, the request port is 80 + # If there is no 2nd array entry, the request port is 80 + request_port = host[1] if len(host) > 1 else '80' no_config = '

        No printers found, please load your login config...

        ' - anker_config = str(web.config.config_show(cfg)) if app.config["login"] else no_config + anker_config = str(web.config.config_show( + cfg)) if app.config["login"] else no_config return render_template( "index.html", request_port=request_port, request_host=host[0], configure=app.config["login"], - login_file_path=web.platform.login_path(web.platform.os_platform(user_agent.os.family)), + login_file_path=web.platform.login_path( + web.platform.os_platform(user_agent.os.family)), anker_config=anker_config ) @@ -138,19 +144,18 @@ def app_api_version(): } -@app.post('/api/ankerctl/config/upload') -def app_api_ankerctl_config_upload(): - with tempfile.TemporaryDirectory(prefix='ankerctl_') as tmpdir: - if request.method != 'POST': - return web.util.flash_redirect() - if 'login_file' not in request.files: - return web.util.flash_redirect('No file found', 'danger') - file = request.files['login_file'] - if file.filename == '': - return web.util.flash_redirect('No file uploaded', 'danger') - else: - web.config.config_import(file, app.config['config']) - return web.util.flash_redirect(path = '/reload') +@app.post('/api/web/config/upload') +def app_api_web_config_upload(): + if request.method != 'POST': + return web.util.flash_redirect() + if 'login_file' not in request.files: + return web.util.flash_redirect('No file found', 'danger') + file = request.files['login_file'] + try: + response = web.config.config_import(file, app.config['config']) + web.util.flash_redirect(response, path='/reload') + except Exception as e: + web.util.flash_redirect(e, 'danger') @app.post("/api/files/local") diff --git a/web/config.py b/web/config.py index c2dec313..e022e7ca 100644 --- a/web/config.py +++ b/web/config.py @@ -38,16 +38,12 @@ def config_import(login_file, config): try: newConfig = cli.config.load_config_from_api(auth_token, region, False) except libflagship.httpapi.APIError as E: - message = f"Config import failed: {E}
        Auth token might be expired: make sure Ankermake Slicer can connect, then try again" - return web.util.flash_redirect(message, 'danger') + raise f"Config import failed: {E}
        Auth token might be expired: make sure Ankermake Slicer can connect, then try again" except Exception as E: - message = f"Config import failed: {E}" - return web.util.flash_redirect(message, 'danger') + raise f"Config import failed: {E}" try: config.save("default", newConfig) except Exception as E: - message = f"Config import failed: {E}" - return web.util.flash_redirect(message, 'danger') - message = "AnkerMake Login Configuration imported successfully! Reloading..." - return web.util.flash_redirect(message, 'success') + raise f"Config import failed: {E}" + return "AnkerMake Login Configuration imported successfully" diff --git a/web/platform.py b/web/platform.py index 1397e0ef..a9c689d4 100644 --- a/web/platform.py +++ b/web/platform.py @@ -1,9 +1,9 @@ def os_platform(os_family: str): if os_family.startswith('Mac OS'): return 'macos' - if os_family.startswith('Windows'): + elif os_family.startswith('Windows'): return 'windows' - if os_family.__contains__('Linux'): + elif 'Linux' in os_family: return 'linux' else: return None @@ -12,7 +12,7 @@ def os_platform(os_family: str): def login_path(platform: str): if platform == 'macos': return '~/Library/Application Support/AnkerMake/AnkerMake_64bit_fp/login.json' - if platform == 'windows': + elif platform == 'windows': return r'%LOCALAPPDATA%\Ankermake\AnkerMake_64bit_fp\login.json' else: return 'Unsupported OS: You must supply path to login.json' diff --git a/web/service/filetransfer.py b/web/service/filetransfer.py index b05f9f1d..0bad8471 100644 --- a/web/service/filetransfer.py +++ b/web/service/filetransfer.py @@ -4,6 +4,7 @@ from multiprocessing import Queue from ..lib.service import Service +from .. import app from libflagship.pppp import P2PCmdType, Aabb, FileTransfer from libflagship.ppppapi import FileUploadInfo, PPPPError @@ -57,7 +58,6 @@ def handler(self, data): self._tap.put(msg) def worker_start(self): - from web import app self.pppp = app.svc.get("pppp") self._tap = Queue() @@ -67,7 +67,6 @@ def worker_run(self, timeout): self.idle(timeout=timeout) def worker_stop(self): - from web import app self.pppp.handlers.remove(self.handler) del self._tap diff --git a/web/service/mqtt.py b/web/service/mqtt.py index 3f780710..00c547da 100644 --- a/web/service/mqtt.py +++ b/web/service/mqtt.py @@ -1,6 +1,7 @@ import logging as log from ..lib.service import Service +from .. import app from libflagship.util import enhex @@ -10,7 +11,6 @@ class MqttQueue(Service): def worker_start(self): - from web import app self.client = cli.mqtt.mqtt_open(app.config["config"], True) def worker_run(self, timeout): diff --git a/web/service/video.py b/web/service/video.py index 62c3ff9a..d353c612 100644 --- a/web/service/video.py +++ b/web/service/video.py @@ -5,6 +5,7 @@ from multiprocessing import Queue from ..lib.service import Service, ServiceRestartSignal +from .. import app from libflagship.pppp import P2PSubCmdType, Xzyh @@ -48,7 +49,6 @@ def worker_init(self): self.saved_video_mode = None def worker_start(self): - from web import app self.pppp = app.svc.get("pppp") self.api_id = id(self.pppp._api) @@ -73,7 +73,6 @@ def worker_run(self, timeout): raise ServiceRestartSignal("New pppp connection detected, restarting video feed") def worker_stop(self): - from web import app try: self.api_stop_live() except ConnectionError: diff --git a/web/util.py b/web/util.py index a69eee8f..5cb97100 100644 --- a/web/util.py +++ b/web/util.py @@ -1,9 +1,5 @@ from flask import flash, redirect -def allowed_file(filename: str, allowed_ext: object): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_ext - - def flash_redirect(message: str | None = None, category = 'info', path = '/'): if message: flash(message, category) From 0b9d80ea6bbba010f77718df5709e8ae87153fa9 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Wed, 3 May 2023 22:54:43 -0700 Subject: [PATCH 137/405] Updating front-end --- web/__init__.py | 7 ------- web/service/pppp.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 53bc32f4..dddd9ee0 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -30,7 +30,6 @@ sock = Sock(app) -<<<<<<< HEAD # autopep8: off import web.service.pppp @@ -38,12 +37,6 @@ import web.service.mqtt import web.service.filetransfer # autopep8: on -======= -import web.service.pppp # nopep8 -import web.service.video # nopep8 -import web.service.mqtt # nopep8 -import web.service.filetransfer # nopep8 ->>>>>>> 4c82ae5 (Updating front-end) @app.before_first_request diff --git a/web/service/pppp.py b/web/service/pppp.py index 99c5f21e..8ac251d8 100644 --- a/web/service/pppp.py +++ b/web/service/pppp.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from ..lib.service import Service, ServiceRestartSignal +from .. import app from libflagship.pppp import P2PCmdType, PktClose, Duid, Type, Xzyh, Aabb from libflagship.ppppapi import AnkerPPPPAsyncApi, PPPPState @@ -25,7 +26,6 @@ def api_command(self, commandType, **kwargs): ) def worker_start(self): - from web import app config = app.config["config"] deadline = datetime.now() + timedelta(seconds=2) From a967cd60a8304f228b546ee73fd30d82359343eb Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 4 May 2023 20:35:11 -0700 Subject: [PATCH 138/405] Format fixes and remove ignored vscode --- .prettierrc | 5 +++- .vscode/settings.json | 8 ------ web/__init__.py | 61 ++++++++++++++++++------------------------- web/config.py | 12 ++++----- web/util.py | 3 ++- 5 files changed, 37 insertions(+), 52 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.prettierrc b/.prettierrc index 9eafbe64..7e1cc8af 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,8 @@ { "tabWidth": 4, "useTabs": false, - "printWidth": 160 + "printWidth": 160, + "singleQuote": true, + "jsxSingleQuote": true, + "quoteProps": "as-needed" } diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 450e9505..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "files.insertFinalNewline": true, - "[python]": { - "editor.defaultFormatter": "ms-python.autopep8" - }, - "python.formatting.provider": "none", - "python.formatting.autopep8Args": ["--ignore", "E402"] -} diff --git a/web/__init__.py b/web/__init__.py index dddd9ee0..4aa20eb3 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,5 +1,4 @@ import json -import tempfile import logging as log from secrets import token_urlsafe as token @@ -17,12 +16,7 @@ import cli.config -app = Flask( - __name__, - root_path=".", - static_folder="static", - template_folder="static" -) +app = Flask(__name__, root_path=".", static_folder="static", template_folder="static") # secret_key is required for flash() to function app.secret_key = token(24) app.config.from_prefixed_env() @@ -44,8 +38,7 @@ def startup(): app.svc.register("pppp", web.service.pppp.PPPPService()) app.svc.register("videoqueue", web.service.video.VideoQueue()) app.svc.register("mqttqueue", web.service.mqtt.MqttQueue()) - app.svc.register( - "filetransfer", web.service.filetransfer.FileTransferService()) + app.svc.register("filetransfer", web.service.filetransfer.FileTransferService()) def shutdown(): @@ -101,21 +94,22 @@ def generate(): for msg in app.svc.stream("videoqueue"): yield msg.data - return Response(generate(), mimetype='video/mp4') + return Response(generate(), mimetype="video/mp4") @app.get("/") def app_root(): config = app.config["config"] with config.open() as cfg: - user_agent = user_agent_parse(request.headers.get('User-Agent')) + user_agent = user_agent_parse(request.headers.get("User-Agent")) - host = request.host.split(':') + host = request.host.split(":") # If there is no 2nd array entry, the request port is 80 - request_port = host[1] if len(host) > 1 else '80' - no_config = '

        No printers found, please load your login config...

        ' - anker_config = str(web.config.config_show( - cfg)) if app.config["login"] else no_config + request_port = host[1] if len(host) > 1 else "80" + no_config = "

        No printers found, please load your login config...

        " + anker_config = ( + str(web.config.config_show(cfg)) if app.config["login"] else no_config + ) return render_template( "index.html", @@ -123,32 +117,29 @@ def app_root(): request_host=host[0], configure=app.config["login"], login_file_path=web.platform.login_path( - web.platform.os_platform(user_agent.os.family)), - anker_config=anker_config + web.platform.os_platform(user_agent.os.family) + ), + anker_config=anker_config, ) @app.get("/api/version") def app_api_version(): - return { - "api": "0.1", - "server": "1.9.0", - "text": "OctoPrint 1.9.0" - } + return {"api": "0.1", "server": "1.9.0", "text": "OctoPrint 1.9.0"} -@app.post('/api/web/config/upload') +@app.post("/api/web/config/upload") def app_api_web_config_upload(): - if request.method != 'POST': + if request.method != "POST": return web.util.flash_redirect() - if 'login_file' not in request.files: - return web.util.flash_redirect('No file found', 'danger') - file = request.files['login_file'] + if "login_file" not in request.files: + return web.util.flash_redirect("No file found", "danger") + file = request.files["login_file"] try: - response = web.config.config_import(file, app.config['config']) - web.util.flash_redirect(response, path='/reload') + response = web.config.config_import(file, app.config["config"]) + web.util.flash_redirect(response, path="/reload") except Exception as e: - web.util.flash_redirect(e, 'danger') + web.util.flash_redirect(e, "danger") @app.post("/api/files/local") @@ -172,12 +163,12 @@ def app_api_files_local(): def reload_webserver(): config = app.config["config"] with config.open() as cfg: - if not getattr(cfg, 'printers', False): - return web.util.flash_redirect('No printers found in config', 'warning') + if not getattr(cfg, "printers", False): + return web.util.flash_redirect("No printers found in config", "warning") app.config["login"] = True - session['_flashes'].clear() + session["_flashes"].clear() restart() - return web.util.flash_redirect('Configuration Loaded', 'success') + return web.util.flash_redirect("Configuration Loaded", "success") def webserver(config, host, port, **kwargs): diff --git a/web/config.py b/web/config.py index e022e7ca..9e4cd459 100644 --- a/web/config.py +++ b/web/config.py @@ -1,5 +1,3 @@ -import web.util - import libflagship.httpapi import libflagship.logincache @@ -8,22 +6,22 @@ def config_show(config): - config_output = f'''

        Account:

        + config_output = f"""

        Account:

        user_id: {config.account.user_id[:10]}...[REDACTED]
        auth_token: {config.account.auth_token[:10]}...[REDACTED]
        email: {config.account.email}
        region: {config.account.region.upper()}

        + config_output += f"""

        duid: {p.p2p_duid}
        sn: {p.sn}
        ip: {p.ip_addr}
        wifi_mac: {cli.util.pretty_mac(p.wifi_mac)}
        api_hosts: {', '.join(p.api_hosts)}
        p2p_hosts: {', '.join(p.p2p_hosts)}


        - ''' + """ return config_output diff --git a/web/util.py b/web/util.py index 5cb97100..7a04c96a 100644 --- a/web/util.py +++ b/web/util.py @@ -1,6 +1,7 @@ from flask import flash, redirect -def flash_redirect(message: str | None = None, category = 'info', path = '/'): + +def flash_redirect(message: str | None = None, category="info", path="/"): if message: flash(message, category) return redirect(path) From 8943c286793040b51b3e96044ba720a13299554c Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 4 May 2023 22:07:47 -0700 Subject: [PATCH 139/405] Fixing reload errors on config upload --- .prettierrc | 8 -------- ankerctl.py | 10 ++++++---- setup.cfg | 4 ++++ static/index.html | 2 +- web/__init__.py | 27 +++++++++++++++++++-------- web/config.py | 21 +++++++++++++-------- web/util.py | 4 +++- 7 files changed, 46 insertions(+), 30 deletions(-) delete mode 100644 .prettierrc create mode 100644 setup.cfg diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 7e1cc8af..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tabWidth": 4, - "useTabs": false, - "printWidth": 160, - "singleQuote": true, - "jsxSingleQuote": true, - "quoteProps": "as-needed" -} diff --git a/ankerctl.py b/ankerctl.py index 400764a3..5a7e7da5 100755 --- a/ankerctl.py +++ b/ankerctl.py @@ -5,7 +5,7 @@ import platform import logging as log from os import path -from rich import print # you need python3 +from rich import print # you need python3 from tqdm import tqdm import cli.config @@ -14,7 +14,7 @@ import cli.mqtt import cli.util import cli.pppp -import cli.checkver # check python version +import cli.checkver # check python version import libflagship.httpapi import libflagship.logincache @@ -35,9 +35,11 @@ def __init__(self): def load_config(self, required=True): with self.config.open() as config: if not getattr(config, 'printers', False): - log.warning("No printers found in config. Please upload configuration using the webserver or 'ankerctl.py config import'") + msg = "No printers found in config. Please upload configuration using the webserver or 'ankerctl.py config import'" if required: - exit(1) + log.critical(msg) + else: + log.warning(msg) def upgrade_config_if_needed(self): try: diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..3ca433b0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[pycodestyle] +max_line_length = 120 +ignore = E221,E226,E231,E241,E261,E203,E741 +exclude = libflagship/pppp.py,libflagship/mqtt.py,libflagship/amtypes.py diff --git a/static/index.html b/static/index.html index 2790b822..fefa31f9 100644 --- a/static/index.html +++ b/static/index.html @@ -78,7 +78,7 @@

        ankerctl

        - +
        diff --git a/web/__init__.py b/web/__init__.py index 4aa20eb3..c20f7493 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -131,15 +131,20 @@ def app_api_version(): @app.post("/api/web/config/upload") def app_api_web_config_upload(): if request.method != "POST": - return web.util.flash_redirect() + return web.util.flash_redirect("/") if "login_file" not in request.files: - return web.util.flash_redirect("No file found", "danger") + return web.util.flash_redirect("/", "No file found", "danger") file = request.files["login_file"] + try: - response = web.config.config_import(file, app.config["config"]) - web.util.flash_redirect(response, path="/reload") + web.config.config_import(file, app.config["config"]) + return web.util.flash_redirect("/reload", "AnkerMake Config Imported!", "success") + except web.config.ConfigAPIError as e: + log.error(e) + return web.util.flash_redirect("/", f"Error: {e}", "danger") except Exception as e: - web.util.flash_redirect(e, "danger") + log.error(traceback.format_exc) + return web.util.flash_redirect("/", f"Unexpected Error occurred: {e}", "danger") @app.post("/api/files/local") @@ -162,13 +167,19 @@ def app_api_files_local(): @app.get("/reload") def reload_webserver(): config = app.config["config"] + with config.open() as cfg: if not getattr(cfg, "printers", False): - return web.util.flash_redirect("No printers found in config", "warning") + return web.util.flash_redirect("/", "No printers found in config", "warning") app.config["login"] = True session["_flashes"].clear() - restart() - return web.util.flash_redirect("Configuration Loaded", "success") + + try: + restart() + except Exception as e: + return web.util.flash_redirect("/", f"Anerctl could not be reloaded: {e}", "danger") + + return web.util.flash_redirect("/", "Anerctl reloaded successfully", "success") def webserver(config, host, port, **kwargs): diff --git a/web/config.py b/web/config.py index 9e4cd459..9cc3ca59 100644 --- a/web/config.py +++ b/web/config.py @@ -5,6 +5,10 @@ import cli.config +class ConfigAPIError(Exception): + """Raised when there is an error with the config api""" + + def config_show(config): config_output = f"""

        Account:

        user_id: {config.account.user_id[:10]}...[REDACTED]
        @@ -34,14 +38,15 @@ def config_import(login_file, config): region = libflagship.logincache.guess_region(cache["ab_code"]) try: - newConfig = cli.config.load_config_from_api(auth_token, region, False) - except libflagship.httpapi.APIError as E: - raise f"Config import failed: {E}
        Auth token might be expired: make sure Ankermake Slicer can connect, then try again" - except Exception as E: - raise f"Config import failed: {E}" + new_config = cli.config.load_config_from_api(auth_token, region, False) + except libflagship.httpapi.APIError as err: + raise ConfigAPIError( + f"Config import failed: {err}. Auth token might be expired: make sure Ankermake Slicer can connect, then try again") + except Exception as err: + raise ConfigAPIError(f"Config import failed: {err}") try: - config.save("default", newConfig) + config.save("default", new_config) except Exception as E: - raise f"Config import failed: {E}" - return "AnkerMake Login Configuration imported successfully" + raise ConfigAPIError(f"Config import failed: {E}") + return new_config diff --git a/web/util.py b/web/util.py index 7a04c96a..5978695f 100644 --- a/web/util.py +++ b/web/util.py @@ -1,7 +1,9 @@ from flask import flash, redirect -def flash_redirect(message: str | None = None, category="info", path="/"): +def flash_redirect(path: str, message: str | None, category="info"): + if not path: + raise ValueError("Redirect path is required") if message: flash(message, category) return redirect(path) From b5c31a560ae2b9d4437cf7d4377457dcc37d1cb7 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 4 May 2023 22:24:34 -0700 Subject: [PATCH 140/405] Fixing various other issues and updateClipboard command not working --- static/ankersrv.css | 28 ++++++++-------- static/ankersrv.js | 10 ++++-- static/index.html | 78 +++++++++++++++++++++++++++++++++++---------- web/__init__.py | 13 ++++---- web/config.py | 8 ++--- web/util.py | 2 +- 6 files changed, 95 insertions(+), 44 deletions(-) diff --git a/static/ankersrv.css b/static/ankersrv.css index 0b7e7a5e..e453f7bf 100644 --- a/static/ankersrv.css +++ b/static/ankersrv.css @@ -1,41 +1,41 @@ .nav-link, .btn-link { - color: #88f387; - text-decoration: none; + color: #88f387; + text-decoration: none; } .nav-link.active, .btn-primary { - color: #000000 !important; - background-color: #88f387 !important; - border: #41a03f; + color: #000000 !important; + background-color: #88f387 !important; + border: #41a03f; } .nav-link:hover, .btn:hover { - color: #000000; - background-color: #adf3ac; - border-color: #41a03f; /*set the color you want here*/ + color: #000000; + background-color: #adf3ac; + border-color: #41a03f; } a.text-light { - text-decoration: none; + text-decoration: none; } a.text-light:hover { - color: #88f387 !important; + color: #88f387 !important; } pre, code { - color: #88f387; - background-color: #292929; + color: #88f387; + background-color: #292929; } .form-label { - font-weight: bolder; + font-weight: bolder; } code#loginFilePath { - font-weight: lighter; + font-weight: lighter; } diff --git a/static/ankersrv.js b/static/ankersrv.js index 5e19c2cd..935394b0 100644 --- a/static/ankersrv.js +++ b/static/ankersrv.js @@ -1,15 +1,21 @@ $(function () { + /** + * Copies provided text to the OS clipboard + * @param {string} text + */ function updateClipboard(text) { navigator.clipboard.writeText(text); console.log(`Copied ${text} to clipboard`); } $("#configData").on("click", function () { - updateClipboard($("#octoPrintHost").val()); + const value = $("#octoPrintHost").text(); + updateClipboard(value); }); $("#copyFilePath").on("click", function () { - updateClipboard($("#loginFilePath").val()); + const value = $("#loginFilePath").text(); + updateClipboard(value); }); let alert_list = document.querySelectorAll(".alert"); diff --git a/static/index.html b/static/index.html index fefa31f9..97962be4 100644 --- a/static/index.html +++ b/static/index.html @@ -4,8 +4,14 @@ ankerctl - - + + @@ -66,15 +72,29 @@

        ankerctl

        {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} -
        {% if configure %} -
        +
        @@ -110,8 +130,12 @@

        Connecting PrusaSlicer/SuperSlicer

        1. Go to "Printer settings" tab
        2. Click "Gear"⚙️ (Add/Edit Physical Printer)
        3. -
        4. Fill in "Descriptive name for the printer" with whatever you like
        5. -
        6. Select your Ankermake printer instructions from the dropdown menu
        7. +
        8. + Fill in "Descriptive name for the printer" with whatever you like +
        9. +
        10. + Select your Ankermake printer instructions from the dropdown menu +
        11. Under "Host Type" select "OctoPrint"
        12. In "Hostname, IP or URL" fill in with: @@ -130,11 +154,17 @@

          Connecting PrusaSlicer/SuperSlicer

        13. Click "Test" to validate input
        14. Click "Ok" to close the window
        15. Slice your file as normal
        16. -
        17. Click "G>" button to start direct upload to the printer
        18. -
        19. Click "Upload and print" - the "Upload"-only is not supported
        20. +
        21. + Click "G>" button to start direct upload to the printer +
        22. +
        23. + Click "Upload and print" - the "Upload"-only is not supported +
        24. Enjoy printing with ankerctl 😉
        -
        Hint: Ctrl+Shift+G will slice and open the upload to printer window +
        Hint: Ctrl+Shift+G will slice and open the upload to printer window
        step 1 @@ -146,11 +176,20 @@

        Connecting PrusaSlicer/SuperSlicer

        {% endif %} -
        +
        -
        +
        @@ -178,7 +217,9 @@

        Connecting PrusaSlicer/SuperSlicer

        - {% autoescape false %} {{ anker_config }} {% endautoescape %} + + {% autoescape false %} {{ anker_config }} {% endautoescape %} +
        @@ -208,10 +249,13 @@

        Connecting PrusaSlicer/SuperSlicer

        > View on GitHub
        - - - - + + + + diff --git a/web/__init__.py b/web/__init__.py index c20f7493..4cb198fc 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,5 +1,6 @@ import json import logging as log +import traceback from secrets import token_urlsafe as token from flask import Flask, request, render_template, Response, session, url_for @@ -139,12 +140,12 @@ def app_api_web_config_upload(): try: web.config.config_import(file, app.config["config"]) return web.util.flash_redirect("/reload", "AnkerMake Config Imported!", "success") - except web.config.ConfigAPIError as e: - log.error(e) - return web.util.flash_redirect("/", f"Error: {e}", "danger") - except Exception as e: - log.error(traceback.format_exc) - return web.util.flash_redirect("/", f"Unexpected Error occurred: {e}", "danger") + except web.config.ConfigImportError as err: + log.error(err) + return web.util.flash_redirect("/", f"Error: {err}", "danger") + except Exception as err: + log.error(traceback.format_exc, err) + return web.util.flash_redirect("/", f"Unexpected Error occurred: {err}", "danger") @app.post("/api/files/local") diff --git a/web/config.py b/web/config.py index 9cc3ca59..fcbec43f 100644 --- a/web/config.py +++ b/web/config.py @@ -5,7 +5,7 @@ import cli.config -class ConfigAPIError(Exception): +class ConfigImportError(Exception): """Raised when there is an error with the config api""" @@ -40,13 +40,13 @@ def config_import(login_file, config): try: new_config = cli.config.load_config_from_api(auth_token, region, False) except libflagship.httpapi.APIError as err: - raise ConfigAPIError( + raise ConfigImportError( f"Config import failed: {err}. Auth token might be expired: make sure Ankermake Slicer can connect, then try again") except Exception as err: - raise ConfigAPIError(f"Config import failed: {err}") + raise ConfigImportError(f"Config import failed: {err}") try: config.save("default", new_config) except Exception as E: - raise ConfigAPIError(f"Config import failed: {E}") + raise ConfigImportError(f"Config import failed: {E}") return new_config diff --git a/web/util.py b/web/util.py index 5978695f..cdd57135 100644 --- a/web/util.py +++ b/web/util.py @@ -1,7 +1,7 @@ from flask import flash, redirect -def flash_redirect(path: str, message: str | None, category="info"): +def flash_redirect(path: str, message: str | None = None, category="info"): if not path: raise ValueError("Redirect path is required") if message: From 41bf543215be83fd59d862e9112c828525f8579e Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 4 May 2023 22:27:21 -0700 Subject: [PATCH 141/405] Fixed spelling mistake in reload message --- web/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/__init__.py b/web/__init__.py index 4cb198fc..0a78f4e8 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -178,9 +178,9 @@ def reload_webserver(): try: restart() except Exception as e: - return web.util.flash_redirect("/", f"Anerctl could not be reloaded: {e}", "danger") + return web.util.flash_redirect("/", f"Ankerctl could not be reloaded: {e}", "danger") - return web.util.flash_redirect("/", "Anerctl reloaded successfully", "success") + return web.util.flash_redirect("/", "Ankerctl reloaded successfully", "success") def webserver(config, host, port, **kwargs): From 6adb0b8bc4d8e7ff8363ce330cd83ebe525e0078 Mon Sep 17 00:00:00 2001 From: BillyJBryant <3013565+billyjbryant@users.noreply.github.com> Date: Thu, 4 May 2023 23:06:03 -0700 Subject: [PATCH 142/405] Uploading PrettierRC Adding Printer Serial to Header & Titlebar --- .prettierrc | 8 ++++++++ static/index.html | 5 ++++- web/__init__.py | 16 +++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..17496da0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 4, + "useTabs": false, + "printWidth": 120, + "singleQuote": false, + "jsxSingleQuote": true, + "quoteProps": "as-needed" +} diff --git a/static/index.html b/static/index.html index 97962be4..89e817ad 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ - ankerctl + ankerctl{% if printer_serial %} - {{ printer_serial }}{% endif %}

        ankerctl

        + {%if printer_serial %} +

        {{ printer_serial }}

        + {% endif %}