From 51200ecec23ff59235cc9d80ba1884751eddee0d Mon Sep 17 00:00:00 2001
From: Jan Grewe <jan@faked.org>
Date: Sat, 5 Oct 2024 01:27:07 +0200
Subject: [PATCH] rewrite in python

---
 .dockerignore                                |   1 +
 .gitignore                                   |   5 +-
 .gitlab-ci.yml                               |  37 +++
 Dockerfile                                   |  12 +
 README.md                                    |  21 ++
 api.php                                      |  85 -------
 index.html                                   | 114 ---------
 js/chitui.js                                 | 141 -----------
 js/reconnecting-websocket.min.js             |   1 -
 main.py                                      | 193 +++++++++++++++
 requirements.txt                             |   4 +
 web/assets/apple-touch-icon.png              | Bin 0 -> 2505 bytes
 web/assets/favicon-48x48.png                 | Bin 0 -> 651 bytes
 web/assets/favicon.ico                       | Bin 0 -> 15086 bytes
 web/assets/favicon.svg                       |  23 ++
 web/assets/site.webmanifest                  |  21 ++
 web/assets/web-app-manifest-192x192.png      | Bin 0 -> 2920 bytes
 web/assets/web-app-manifest-512x512.png      | Bin 0 -> 11588 bytes
 {css => web/css}/bootstrap-icons.min.css     |   0
 {css => web/css}/bootstrap.min.css           |   0
 {css => web/css}/bootstrap.min.css.map       |   0
 {css => web/css}/chitui.css                  |   0
 {css => web/css}/fonts/bootstrap-icons.woff  | Bin
 {css => web/css}/fonts/bootstrap-icons.woff2 | Bin
 {img => web/img}/elegoo_saturn4ultra.webp    | Bin
 web/index.html                               | 150 +++++++++++
 {js => web/js}/bootstrap.bundle.min.js       |   0
 {js => web/js}/bootstrap.bundle.min.js.map   |   0
 web/js/chitui.js                             | 248 +++++++++++++++++++
 {js => web/js}/color-modes.js                |   0
 {js => web/js}/jquery-3.7.1.min.js           |   0
 {js => web/js}/jquery-3.7.1.min.map          |   0
 web/js/sdcp.js                               |  95 +++++++
 web/js/socket.io.min.js                      |   7 +
 34 files changed, 815 insertions(+), 343 deletions(-)
 create mode 100644 .dockerignore
 create mode 100644 .gitlab-ci.yml
 create mode 100644 Dockerfile
 delete mode 100644 api.php
 delete mode 100644 index.html
 delete mode 100644 js/chitui.js
 delete mode 100644 js/reconnecting-websocket.min.js
 create mode 100644 main.py
 create mode 100644 requirements.txt
 create mode 100644 web/assets/apple-touch-icon.png
 create mode 100644 web/assets/favicon-48x48.png
 create mode 100644 web/assets/favicon.ico
 create mode 100644 web/assets/favicon.svg
 create mode 100644 web/assets/site.webmanifest
 create mode 100644 web/assets/web-app-manifest-192x192.png
 create mode 100644 web/assets/web-app-manifest-512x512.png
 rename {css => web/css}/bootstrap-icons.min.css (100%)
 rename {css => web/css}/bootstrap.min.css (100%)
 rename {css => web/css}/bootstrap.min.css.map (100%)
 rename {css => web/css}/chitui.css (100%)
 rename {css => web/css}/fonts/bootstrap-icons.woff (100%)
 rename {css => web/css}/fonts/bootstrap-icons.woff2 (100%)
 rename {img => web/img}/elegoo_saturn4ultra.webp (100%)
 create mode 100644 web/index.html
 rename {js => web/js}/bootstrap.bundle.min.js (100%)
 rename {js => web/js}/bootstrap.bundle.min.js.map (100%)
 create mode 100644 web/js/chitui.js
 rename {js => web/js}/color-modes.js (100%)
 rename {js => web/js}/jquery-3.7.1.min.js (100%)
 rename {js => web/js}/jquery-3.7.1.min.map (100%)
 create mode 100644 web/js/sdcp.js
 create mode 100644 web/js/socket.io.min.js

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..1d17dae
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.venv
diff --git a/.gitignore b/.gitignore
index aca564c..f63a77d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
-printers.json
-/printers.json
+.venv
+test.py
+__pycache__
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..df73862
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,37 @@
+stages:
+  - build
+  - push
+
+build:
+  stage: build
+  image:
+    name: gcr.io/kaniko-project/executor:debug
+    entrypoint: [""]
+  only:
+    - master
+  script:
+    - /kaniko/executor
+      --cache=true
+      --context "${CI_PROJECT_DIR}"
+      --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
+      --destination "dcr.faked.org/chitui:${CI_COMMIT_SHORT_SHA}"
+
+push_latest:
+  stage: push
+  image:
+    name: gcr.io/go-containerregistry/crane:debug
+    entrypoint: [""]
+  only:
+    - master
+  script:
+    - crane copy dcr.faked.org/chitui:${CI_COMMIT_SHORT_SHA} dcr.faked.org/chitui:latest
+
+push_tag:
+  stage: push
+  image:
+    name: gcr.io/go-containerregistry/crane:debug
+    entrypoint: [""]
+  only:
+    - tags
+  script:
+    - crane copy dcr.faked.org/chitui:${CI_COMMIT_SHORT_SHA} dcr.faked.org/chitui:${CI_COMMIT_TAG}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f5f3519
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.12-alpine
+
+WORKDIR /app
+
+RUN pip install gevent==24.2.1 gevent-websocket==0.10.1
+
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+COPY . .
+
+ENTRYPOINT ["python", "main.py"]
diff --git a/README.md b/README.md
index 5700946..6c3cc2c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,24 @@
 # ChitUI
 
 A web UI for Chitubox SDCP 3.0 resin printers
+
+## Setup
+```
+python -mvenv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+## Usage
+After creating the virtual environment and installing the requirements, you can run ChitUI like this:
+```
+python main.py
+```
+and then access the web interface on port 54780, e.g. http://127.0.0.1:54780/
+
+## Docker
+As ChitUI needs to broadcast UDP messages on your network segment, running ChitUI in a Docker container requires host networking to be enabled for the container:
+```
+docker build -t chitui:latest .
+docker run --rm --name chitui --net=host chitui:latest
+```
diff --git a/api.php b/api.php
deleted file mode 100644
index 2e0647d..0000000
--- a/api.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-$printersJson = 'printers.json';
-
-$output = '{"msg": "Nothing to see here..."}';
-if (isset($_GET['get'])) {
-  switch ($_GET['get']) {
-    case 'printers':
-      if (file_exists($printersJson)) {
-        $output = file_get_contents($printersJson);
-      } else {
-        $output = discoverPrinters();
-      }
-      break;
-    default:
-      break;
-  }
-} elseif (isset($_GET['action'])) {
-  switch ($_GET['action']) {
-    case 'discover':
-      $output = discoverPrinters();
-      break;
-    default:
-      break;
-  }
-}
-header('Content-Type: application/json; charset=utf-8');
-echo $output;
-die;
-
-function savePrinterInfo($response) {
-    global $printersJson;
-
-    $printers = new stdClass();
-    if (file_exists($printersJson)) {
-      $printers = json_decode(file_get_contents($printersJson));
-    }
-    $info = json_decode($response);
-    $data = $info->Data;
-    $id = $info->Id;
-    $printer = array(
-      'name' => $data->Name,
-      'model' => $data->MachineName,
-      'brand' => $data->BrandName,
-      'ip'  => $data->MainboardIP,
-      'mainboard'  => $data->MainboardID,
-      'protocol' => $data->ProtocolVersion,
-      'firmware' => $data->FirmwareVersion,
-    );
-    $printers->$id = $printer;
-    if (file_put_contents($printersJson, json_encode($printers, JSON_PRETTY_PRINT))) {
-      return true;
-    }
-    return false;
-}
-
-function discoverPrinters() {
-  global $printersJson;
-  $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
-  $sockets = array($socket);
-  $null = NULL;
-  $socketTimeout = 3;
-  $socketOpen = true;
-  $msg = "M99999";
-
-  socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);
-  socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
-  socket_set_option($socket, SOL_SOCKET, SO_REUSEPORT, 1);
-  socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$socketTimeout, "usec"=>0));
-  socket_bind($socket, '0.0.0.0');
-  socket_sendto($socket, $msg, strlen($msg), 0, '255.255.255.255', 3000);
-  
-  while($socketOpen) {
-    if (socket_recv($socket, $data, 9999, 0)) {
-      //echo "Response received: ".$data.PHP_EOL;
-      savePrinterInfo($data);
-    }
-    $socketOpen = socket_select($sockets, $null, $null, $socketTimeout);
-  }
-  if (file_exists($printersJson)) {
-    return file_get_contents($printersJson);
-  } else {
-    return "{}";
-  }
-}
diff --git a/index.html b/index.html
deleted file mode 100644
index 9431c9c..0000000
--- a/index.html
+++ /dev/null
@@ -1,114 +0,0 @@
-<!doctype html>
-<html lang="en" data-bs-theme="auto">
-  <head>
-    <script src="js/color-modes.js"></script>
-
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <meta name="description" content="Chitubox SDCP WebUI">
-    <meta name="author" content="Jan Grewe">
-    <title>ChitUI</title>
-    <link href="css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
-
-    <!-- Favicons 
-    <link rel="apple-touch-icon" href="/docs/5.3/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
-    <link rel="icon" href="/docs/5.3/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
-    <link rel="icon" href="/docs/5.3/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
-    <link rel="manifest" href="/docs/5.3/assets/img/favicons/manifest.json">
-    <link rel="mask-icon" href="/docs/5.3/assets/img/favicons/safari-pinned-tab.svg" color="#712cf9">
-    <link rel="icon" href="/docs/5.3/assets/img/favicons/favicon.ico">
-    -->
-    <meta name="theme-color" content="#712cf9">
-    <link href="css/bootstrap-icons.min.css" rel="stylesheet">
-    <link href="css/chitui.css" rel="stylesheet">
-  </head>
-  <body>
-    <svg xmlns="http://www.w3.org/2000/svg" class="d-none">
-      <symbol id="check2" viewBox="0 0 16 16">
-        <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
-      </symbol>
-      <symbol id="circle-half" viewBox="0 0 16 16">
-        <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/>
-      </symbol>
-      <symbol id="moon-stars-fill" viewBox="0 0 16 16">
-        <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
-        <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
-      </symbol>
-      <symbol id="sun-fill" viewBox="0 0 16 16">
-        <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
-      </symbol>
-    </svg>
-    <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
-      <button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
-              id="bd-theme"
-              type="button"
-              aria-expanded="false"
-              data-bs-toggle="dropdown"
-              aria-label="Toggle theme (auto)">
-        <svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg>
-        <span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
-      </button>
-      <ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
-        <li>
-          <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
-            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#sun-fill"></use></svg>
-            Light
-            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
-          </button>
-        </li>
-        <li>
-          <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
-            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg>
-            Dark
-            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
-          </button>
-        </li>
-        <li>
-          <button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
-            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#circle-half"></use></svg>
-            Auto
-            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
-          </button>
-        </li>
-      </ul>
-    </div>
-
-<main class="d-flex flex-nowrap">
-
-  <div class="d-flex flex-column align-items-stretch flex-shrink-0 bg-body-tertiary shadow" style="width: 380px;">
-
-    <a href="/" class="d-flex flex-row justify-content-center align-items-center flex-shrink-0 ps-0 p-3 link-body-emphasis text-decoration-none border-bottom">
-      <i class="bi-cloud-upload-fill fs-4 me-2"></i>
-      <span class="fs-4 fw-semibold">ChitUI</span>
-    </a>
-
-    <div id="printersList" class="list-group list-group-flush border-bottom scrollarea"></div>
-
-  </div>
-  <div class="ps-3">foobar</div>
-
-</main>
-
-<template id="printersListItem">
-  <a href="#" id="" class="printerListItem list-group-item list-group-item-action py-3 lh-sm" data-connection-id="" data-printer-id="">
-    <div class="d-flex flex-row">
-      <img src="img/elegoo_saturn4ultra.webp" width="64px">
-      <div class="d-flex flex-column w-100">
-        <div class="d-flex flex-row align-items-center justify-content-between">
-          <strong class="printerName text-body-emphasis mb-1"></strong>
-          <small class="printerStatus text-body-secondary"><i class="bi-circle"></i></small>
-        </div>
-        <div class="printerType col-10 mb-1 small"></div>
-        <div class="printerInfo col-10 mb-1 small">?</div>
-      </div>
-  </div>
-  </a>
-</template>
-
-
-<script src="js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
-<script src="js/jquery-3.7.1.min.js"></script>
-<script src="js/reconnecting-websocket.min.js"></script>
-<script src="js/chitui.js"></script>
-</body>
-</html>
diff --git a/js/chitui.js b/js/chitui.js
deleted file mode 100644
index 46cfe49..0000000
--- a/js/chitui.js
+++ /dev/null
@@ -1,141 +0,0 @@
-var websockets = []
-var printers = {}
-
-const SDCP_MACHINE_STATUS_IDLE = 0  // Idle
-const SDCP_MACHINE_STATUS_PRINTING = 1  // Executing print task
-const SDCP_MACHINE_STATUS_FILE_TRANSFERRING = 2  // File transfer in progress
-const SDCP_MACHINE_STATUS_EXPOSURE_TESTING = 3  // Exposure test in progress
-const SDCP_MACHINE_STATUS_DEVICES_TESTING = 4  //Device self-check in progress
-
-$( document ).ready(function() {
-    getPrinters()
-});
-
-function getPrinters() {
-  $.getJSON( "api.php", {
-    'get': 'printers'
-  })
-  .done(function(data) {
-    printers = data
-    addPrinters(data)
-    //addPrinters(printers) // REMOVE_ME: Testing
-  })
-  .fail(function() {
-    alert( "error" )
-  })
-}
-
-function addPrinters(printers) {
-  $.each(printers, function(id, printer) {
-    var template = $("#printersListItem").html()
-    var item = $(template)
-    item.attr('id', 'printer_'+ printer.mainboard)
-    item.attr("data-connection-id", id)
-    item.attr("data-printer-id", printer.mainboard)
-    item.find(".printerName").text(printer.name)
-    item.find(".printerType").text(printer.brand + ' ' + printer.model)
-    item.on('click', function() {
-      $.each($('.printerListItem'), function() {
-        $(this).removeClass('active')
-      })
-      $(this).addClass('active')
-      showPrinter($(this).data('connection-id'))
-    })
-    $("#printersList").append(item)
-    connectPrinter(id, printer)
-  });
-}
-
-function showPrinter(id) {
-  console.log(printers)
-}
-
-function connectPrinter(id, printer) {
-  var wsUrl = 'wss://'+window.location.hostname+'/ws/'+printer['ip']
-  var ws = new ReconnectingWebSocket(wsUrl);
-  ws.onopen = function() {
-    sendRequest(id, printer, 0)
-  };
-  ws.onmessage = function(data) {
-    msg = JSON.parse(data.data)
-    handleMsg(msg)
-  };
-  websockets[id] = ws
-}
-
-function handleMsg(msg) {
-  id = msg.Id
-  topic = msg.Topic.split("/")[1]
-  console.log('topic: '+ topic)
-  switch(topic) {
-    case 'response':
-      console.log(msg)
-      console.log('id: '+ id)
-      break
-    case 'status':
-      updateStatus(msg)
-      break
-    default:
-      break
-  }
-}
-
-function updateStatus(data) {
-  console.log(data)
-  var info = $('#printer_'+data.MainboardID).find('.printerInfo')
-  switch(data.Status.CurrentStatus[0]) {
-    case SDCP_MACHINE_STATUS_IDLE:
-      info.text("Idle")
-      setPrinterStatus(data.MainboardID, "success")
-      break
-    case SDCP_MACHINE_STATUS_PRINTING:
-      break
-    case SDCP_MACHINE_STATUS_FILE_TRANSFERRING:
-      break
-    case SDCP_MACHINE_STATUS_EXPOSURE_TESTING:
-      break
-    case SDCP_MACHINE_STATUS_DEVICES_TESTING:
-      break
-    default:
-      break
-  }
-}
-
-function setPrinterStatus(id, style) {
-  var status = $('#printer_'+id).find('.printerStatus')
-  status.removeClass(function(index, css) {
-    return (css.match(/\btext-\S+/g) || []).join(' ');
-  }).addClass("text-"+style);
-  status.find('i').removeClass().addClass('bi-circle-fill')
-}
-
-function sendRequest(id, printer, cmd) {
-  var ts = new Date().getTime() / 1000;
-  var payload = {
-    "Id": id,
-    "Data":{
-      "Cmd": cmd,
-      "Data": {},
-      "RequestID": generateRequestId(16),
-      "MainboardID": printer["mainboard"],
-      "TimeStamp": ts,
-      "From": 0
-    },
-    "Topic": "sdcp/request/"+printer["mainboard"]
-  }
-  websockets[id].send(JSON.stringify(payload))
-}
-
-
-function generateRequestId(size) {
-  return [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
-} 
-
-/* global bootstrap: false */
-(() => {
-  'use strict'
-  const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
-  tooltipTriggerList.forEach(tooltipTriggerEl => {
-    new bootstrap.Tooltip(tooltipTriggerEl)
-  })
-})()
diff --git a/js/reconnecting-websocket.min.js b/js/reconnecting-websocket.min.js
deleted file mode 100644
index 3015099..0000000
--- a/js/reconnecting-websocket.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a});
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..7372e72
--- /dev/null
+++ b/main.py
@@ -0,0 +1,193 @@
+from flask import Flask, send_file
+from flask_socketio import SocketIO
+from threading import Thread
+from loguru import logger
+import socket
+import json
+import os
+import websocket
+import time
+import sys
+
+debug = False
+log_level = "INFO"
+if os.environ.get("DEBUG") is not None and os.environ.get("DEBUG"):
+    debug = True
+    log_level = "DEBUG"
+
+logger.add(sys.stdout, colorize=debug, level=log_level)
+
+port = 54780
+discovery_timeout = 1
+app = Flask(__name__,
+            static_url_path='',
+            static_folder='web')
+socketio = SocketIO(app)
+websockets = {}
+printers = {}
+
+
+@app.route("/")
+def web_index():
+    return app.send_static_file('index.html')
+
+
+@socketio.on('connect')
+def sio_handle_connect(auth):
+    logger.info('Client connected')
+    socketio.emit('printers', printers)
+
+
+@socketio.on('disconnect')
+def sio_handle_disconnect():
+    logger.info('Client disconnected')
+
+
+@socketio.on('printers')
+def sio_handle_printers(data):
+    logger.debug('client.printers >> '+data)
+    main()
+
+
+@socketio.on('printer_info')
+def sio_handle_printer_status(data):
+    logger.debug('client.printer_info >> '+data['id'])
+    get_printer_status(data['id'])
+    get_printer_attributes(data['id'])
+
+
+@socketio.on('printer_files')
+def sio_handle_printer_files(data):
+    logger.debug('client.printer_files >> '+json.dumps(data))
+    get_printer_files(data['id'], data['url'])
+
+
+def get_printer_status(id):
+    send_printer_cmd(id, 0)
+
+
+def get_printer_attributes(id):
+    send_printer_cmd(id, 1)
+
+
+def get_printer_files(id, url):
+    send_printer_cmd(id, 258, {"Url": url})
+
+
+def send_printer_cmd(id, cmd, data={}):
+    printer = printers[id]
+    ts = int(time.time())
+    payload = {
+        "Id": printer['connection'],
+        "Data": {
+            "Cmd": cmd,
+            "Data": data,
+            "RequestID": os.urandom(8).hex(),
+            "MainboardID": id,
+            "TimeStamp": ts,
+            "From": 0
+        },
+        "Topic": "sdcp/request/" + id
+    }
+    logger.debug("printer << \n{p}", p=json.dumps(payload, indent=4))
+    if id in websockets:
+        websockets[id].send(json.dumps(payload))
+
+
+def discover_printers():
+    logger.info("Starting printer discovery.")
+    msg = b'M99999'
+    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
+                         socket.IPPROTO_UDP)  # UDP
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+    sock.settimeout(discovery_timeout)
+    sock.bind(('', 54781))
+    sock.sendto(msg, ("255.255.255.255", 3000))
+    socketOpen = True
+    printers = None
+    while (socketOpen):
+        try:
+            data = sock.recv(8192)
+            printers = save_discovered_printer(data)
+        except TimeoutError:
+            sock.close()
+            break
+    logger.info("Discovery done.")
+    return printers
+
+
+def save_discovered_printer(data):
+    j = json.loads(data.decode('utf-8'))
+    printer = {}
+    printer['connection'] = j['Id']
+    printer['name'] = j['Data']['Name']
+    printer['model'] = j['Data']['MachineName']
+    printer['brand'] = j['Data']['BrandName']
+    printer['ip'] = j['Data']['MainboardIP']
+    printer['protocol'] = j['Data']['ProtocolVersion']
+    printer['firmware'] = j['Data']['FirmwareVersion']
+    printers[j['Data']['MainboardID']] = printer
+    logger.info("Discovered: {n} ({i})".format(
+        n=printer['name'], i=printer['ip']))
+    return printers
+
+
+def connect_printers(printers):
+    for id, printer in printers.items():
+        url = "ws://{ip}:3030/websocket".format(ip=printer['ip'])
+        logger.info("Connecting to: {n}".format(n=printer['name']))
+        websocket.setdefaulttimeout(1)
+        ws = websocket.WebSocketApp(url,
+                                    on_message=ws_msg_handler,
+                                    on_open=lambda _: ws_connected_handler(
+                                        printer['name']),
+                                    on_close=lambda _, s, m: logger.info(
+                                        "Connection to '{n}' closed: {m} ({s})".format(n=printer['name'], m=m, s=s)),
+                                    on_error=lambda _, e: logger.info(
+                                        "Connection to '{n}' error: {e}".format(n=printer['name'], e=e))
+                                    )
+        websockets[id] = ws
+        Thread(target=lambda: ws.run_forever(reconnect=1), daemon=True).start()
+
+    return True
+
+
+def ws_connected_handler(name):
+    logger.info("Connected to: {n}".format(n=name))
+    socketio.emit('printers', printers)
+
+
+def ws_msg_handler(ws, msg):
+    data = json.loads(msg)
+    logger.debug("printer >> \n{m}", m=json.dumps(data, indent=4))
+    if data['Topic'].startswith("sdcp/response/"):
+        socketio.emit('printer_response', data)
+    elif data['Topic'].startswith("sdcp/status/"):
+        socketio.emit('printer_status', data)
+    elif data['Topic'].startswith("sdcp/attributes/"):
+        socketio.emit('printer_attributes', data)
+    elif data['Topic'].startswith("sdcp/error/"):
+        socketio.emit('printer_error', data)
+    elif data['Topic'].startswith("sdcp/notice/"):
+        socketio.emit('printer_notice', data)
+    else:
+        logger.warning("--- UNKNOWN MESSAGE ---")
+        logger.warning(data)
+        logger.warning("--- UNKNOWN MESSAGE ---")
+
+
+def main():
+    printers = discover_printers()
+    if len(printers) > 0:
+        connect_printers(printers)
+        logger.info("Setting up connections: done.")
+        socketio.emit('printers', printers)
+    else:
+        logger.error("No printers discovered.")
+
+
+if __name__ == "__main__":
+    main()
+
+    socketio.run(app, host='0.0.0.0', port=port,
+                 debug=debug, use_reloader=debug, log_output=True)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2b66ca5
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+flask==3.0.3
+flask-socketio==5.4.1
+websocket-client==1.8.0 
+loguru==0.7.2
diff --git a/web/assets/apple-touch-icon.png b/web/assets/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..70edbf40e10a082867692f1cab805b789fb00dfe
GIT binary patch
literal 2505
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
A-T(jq

literal 0
HcmV?d00001

diff --git a/web/assets/favicon-48x48.png b/web/assets/favicon-48x48.png
new file mode 100644
index 0000000000000000000000000000000000000000..129b7a559561df298c454bf56fb5f1e66507076f
GIT binary patch
literal 651
zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<Y|L7py-AsNnZBQ9=bbmTc(
z=D;4(!1tno>jI<b0wya5))ftWFBn8G9ISr-&hqoQedTH4LXy=oljZGi`sBUy$y56I
z%djfhTd85v&2FA4J`ApGO3eOY4z;a*%Z1;TSzF2Qy%N!y%4Is+_tp2`A*-*36oxQ_
ztrqP)p1kY+`&G`h^19Q#rFhsLRU|R2s9Ap5(Dz<yt&Lo;n~ECSh1Xv*cHi|nm+-r6
zckHbkvz&U1F2*abOLxWSEppy*u0nYG?cUe(eswaccq&;rywaJgYtMfmY2yU3&?oj%
zy)OTf*jz5gKRCnGaP4Q-;pS%@(RvIWS8~jhJEkp7@Z@GJY(DXHrZ|I8Tg24B%J@YI
zk8A9N>;BIcXE@e!q5o<Zr+&kPBvs!#Kc9Eb5pVEcbL(x{ssjr{W-y;oFVbP~sM~*k
zdH>F+wMFN+9xY~L_+_A(^i7_x-Eq=mmQdyotnAv8PpZ7-nN#<+o6+s!%WJPqvk#dv
z8_e~){6v>`R`U+mG$sY>&AYb-X_qAE@Ak91W$MF_@Q{I{WO3N))J2!_gby!b5vWXK
zV(99y=3m(SAacg;N&UO#a59J=30k}Pv){!-N7w?E97$zlc%3lclzZX*<QbnMBN^`8
zPG)QnK9(^*W3Lp0m1tDiy=7rLE-dhE>V5t7*X`UpIdZRfQ@XEZm~b>jeu)>%R{k8j
zG=O1A?Av0-f+h2#eeb;gS#z%R|KsIp42zn}R?8}H>gIAQeObTgU9~BL_=B5E<&!(h
zX1VhDGY5Rk`w_^J5aAQL;zYx$oHlt^HlFU4TwlNO)G!NYzi8O=t$H~F0|SGntDnm{
Hr-UW|aCI9e

literal 0
HcmV?d00001

diff --git a/web/assets/favicon.ico b/web/assets/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..ee071b49af9e724bef92a4a5e37307df1412f4b7
GIT binary patch
literal 15086
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

literal 0
HcmV?d00001

diff --git a/web/assets/favicon.svg b/web/assets/favicon.svg
new file mode 100644
index 0000000..4526e91
--- /dev/null
+++ b/web/assets/favicon.svg
@@ -0,0 +1,23 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><style>
+    #light-icon {
+      display: inline;
+    }
+    #dark-icon {
+      display: none;
+    }
+
+    @media (prefers-color-scheme: dark) {
+      #light-icon {
+        display: none;
+      }
+      #dark-icon {
+        display: inline;
+      }
+    }
+  </style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1052)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(50,0,0,50,100,124.5)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="16" height="16"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-printer-fill" viewBox="0 0 16 16">
+  <path d="M5 1a2 2 0 0 0-2 2v1h10V3a2 2 0 0 0-2-2zm6 8H5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1"></path>
+  <path d="M0 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-1v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2H2a2 2 0 0 1-2-2zm2.5 1a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1"></path>
+</svg></svg></g></g><defs><clipPath id="SvgjsClipPath1052"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><g><g transform="matrix(62.5,0,0,62.5,0,30.75)" style="filter: contrast(0.3846153846153846) brightness(4.5)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="16" height="16"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-printer-fill" viewBox="0 0 16 16">
+  <path d="M5 1a2 2 0 0 0-2 2v1h10V3a2 2 0 0 0-2-2zm6 8H5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1"></path>
+  <path d="M0 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-1v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2H2a2 2 0 0 1-2-2zm2.5 1a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1"></path>
+</svg></svg></g></g></svg></g></svg>
\ No newline at end of file
diff --git a/web/assets/site.webmanifest b/web/assets/site.webmanifest
new file mode 100644
index 0000000..54ffefa
--- /dev/null
+++ b/web/assets/site.webmanifest
@@ -0,0 +1,21 @@
+{
+  "name": "ChitUI",
+  "short_name": "ChitUI",
+  "icons": [
+    {
+      "src": "/assets/web-app-manifest-192x192.png",
+      "sizes": "192x192",
+      "type": "image/png",
+      "purpose": "maskable"
+    },
+    {
+      "src": "/assets/web-app-manifest-512x512.png",
+      "sizes": "512x512",
+      "type": "image/png",
+      "purpose": "maskable"
+    }
+  ],
+  "theme_color": "#ffffff",
+  "background_color": "#ffffff",
+  "display": "standalone"
+}
\ No newline at end of file
diff --git a/web/assets/web-app-manifest-192x192.png b/web/assets/web-app-manifest-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..03de22c2024d16108f2136ec0af96cad924a37c6
GIT binary patch
literal 2920
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

literal 0
HcmV?d00001

diff --git a/web/assets/web-app-manifest-512x512.png b/web/assets/web-app-manifest-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..602ce94ae1907b5cb9162231a96178cf6c15061f
GIT binary patch
literal 11588
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

literal 0
HcmV?d00001

diff --git a/css/bootstrap-icons.min.css b/web/css/bootstrap-icons.min.css
similarity index 100%
rename from css/bootstrap-icons.min.css
rename to web/css/bootstrap-icons.min.css
diff --git a/css/bootstrap.min.css b/web/css/bootstrap.min.css
similarity index 100%
rename from css/bootstrap.min.css
rename to web/css/bootstrap.min.css
diff --git a/css/bootstrap.min.css.map b/web/css/bootstrap.min.css.map
similarity index 100%
rename from css/bootstrap.min.css.map
rename to web/css/bootstrap.min.css.map
diff --git a/css/chitui.css b/web/css/chitui.css
similarity index 100%
rename from css/chitui.css
rename to web/css/chitui.css
diff --git a/css/fonts/bootstrap-icons.woff b/web/css/fonts/bootstrap-icons.woff
similarity index 100%
rename from css/fonts/bootstrap-icons.woff
rename to web/css/fonts/bootstrap-icons.woff
diff --git a/css/fonts/bootstrap-icons.woff2 b/web/css/fonts/bootstrap-icons.woff2
similarity index 100%
rename from css/fonts/bootstrap-icons.woff2
rename to web/css/fonts/bootstrap-icons.woff2
diff --git a/img/elegoo_saturn4ultra.webp b/web/img/elegoo_saturn4ultra.webp
similarity index 100%
rename from img/elegoo_saturn4ultra.webp
rename to web/img/elegoo_saturn4ultra.webp
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..a788f29
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html lang="en" data-bs-theme="auto">
+  <head>
+    <script src="js/color-modes.js"></script>
+
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="description" content="Chitubox SDCP WebUI" />
+    <meta name="author" content="Jan Grewe" />
+    <title>ChitUI</title>
+    <link href="css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
+
+    <link rel="icon" type="image/png" href="/assets/favicon-48x48.png" sizes="48x48" />
+    <link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
+    <link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
+    <link rel="shortcut icon" href="/assets/favicon.ico" />
+    <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
+    <meta name="apple-mobile-web-app-title" content="ChitUI" />
+    <link rel="manifest" href="/assets/site.webmanifest" />
+
+    <meta name="theme-color" content="#712cf9" />
+    <link href="css/bootstrap-icons.min.css" rel="stylesheet" />
+    <link href="css/chitui.css" rel="stylesheet" />
+  </head>
+  <body>
+    <svg xmlns="http://www.w3.org/2000/svg" class="d-none">
+      <symbol id="check2" viewBox="0 0 16 16">
+        <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
+      </symbol>
+      <symbol id="circle-half" viewBox="0 0 16 16">
+        <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
+      </symbol>
+      <symbol id="moon-stars-fill" viewBox="0 0 16 16">
+        <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
+        <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
+      </symbol>
+      <symbol id="sun-fill" viewBox="0 0 16 16">
+        <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
+      </symbol>
+    </svg>
+    <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
+      <button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" aria-label="Toggle theme (auto)">
+        <svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg>
+        <span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
+        <li>
+          <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
+            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#sun-fill"></use></svg>
+            Light
+            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
+          </button>
+        </li>
+        <li>
+          <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
+            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg>
+            Dark
+            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
+          </button>
+        </li>
+        <li>
+          <button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
+            <svg class="bi me-2 opacity-50" width="1em" height="1em"><use href="#circle-half"></use></svg>
+            Auto
+            <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
+          </button>
+        </li>
+      </ul>
+    </div>
+
+    <main class="d-flex flex-nowrap">
+      <div class="d-flex flex-column align-items-stretch flex-shrink-0 bg-body-tertiary shadow" style="width: 300px">
+        <a href="#" class="d-flex flex-row justify-content-center align-items-center flex-shrink-0 ps-0 p-3 link-body-emphasis text-decoration-none border-bottom">
+          <i class="serverStatus bi-cloud text-danger fs-4 me-2"></i>
+          <span class="fs-4 fw-semibold">ChitUI</span>
+        </a>
+
+        <div id="printersList" class="list-group list-group-flush border-bottom scrollarea"></div>
+      </div>
+      <!-- main column -->
+      <div class="container w-100">
+
+        <div class="row p-3">
+          <div class="col-12 col-lg-8 offset-lg-2">
+            <div class="card bg-body-secondary mb-3 w-100">
+              <div class="row g-0">
+                <div class="col-md-4">
+                  <img src="" class="img-fluid rounded-start" id="printerIcon" alt="..." />
+                </div>
+                <div class="col-md-8">
+                  <div class="card-body">
+                    <h3 class="card-title" id="printerName"></h3>
+                    <p class="card-text" id="printerType"></p>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="card">
+          <div class="card-header">
+            <ul class="nav nav-tabs card-header-tabs" role="tablist" id="navTabs"></ul>
+          </div>
+          <div class="tab-content" id="navPanes"></div>
+        </div>
+
+      </div>
+    </main>
+
+    <template id="tmplPrintersListItem">
+      <a href="#" id="" class="printerListItem list-group-item list-group-item-action py-3 lh-sm" data-connection-id="" data-printer-id="">
+        <div class="d-flex flex-row">
+          <img class="printerIcon" src="" width="64px" />
+          <div class="d-flex flex-column w-100">
+            <div class="d-flex flex-row align-items-center justify-content-between">
+              <strong class="printerName text-body-emphasis mb-1"></strong>
+              <small class="printerStatus text-body-secondary"><i class="bi-circle"></i></small>
+              <div class="printerSpinner text-body-secondary spinner-border spinner-border-sm visually-hidden" role="status">
+                <span class="visually-hidden">Loading...</span>
+              </div>
+            </div>
+            <div class="printerType col-10 mb-1 small"></div>
+            <div class="printerInfo col-10 mb-1 small">?</div>
+          </div>
+        </div>
+      </a>
+    </template>
+
+    <template id="tmplNavTab">
+      <li class="nav-item" role="presentation">
+        <button class="nav-link" id="" data-bs-toggle="tab" data-bs-target="" type="button" role="tab"></button>
+      </li>
+    </template>
+
+    <template id="tmplNavPane">
+      <div class="tab-pane" id="" role="tabpanel" tabindex="0">
+        <table class="table">
+          <tbody id=""></tbody>
+        </table>
+      </div>
+    </template>
+
+    <script src="js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+    <script src="js/jquery-3.7.1.min.js"></script>
+    <script src="js/socket.io.min.js"></script>
+    <script src="js/sdcp.js"></script>
+    <script src="js/chitui.js"></script>
+  </body>
+</html>
diff --git a/js/bootstrap.bundle.min.js b/web/js/bootstrap.bundle.min.js
similarity index 100%
rename from js/bootstrap.bundle.min.js
rename to web/js/bootstrap.bundle.min.js
diff --git a/js/bootstrap.bundle.min.js.map b/web/js/bootstrap.bundle.min.js.map
similarity index 100%
rename from js/bootstrap.bundle.min.js.map
rename to web/js/bootstrap.bundle.min.js.map
diff --git a/web/js/chitui.js b/web/js/chitui.js
new file mode 100644
index 0000000..95f3814
--- /dev/null
+++ b/web/js/chitui.js
@@ -0,0 +1,248 @@
+const socket = io();
+var websockets = []
+var printers = {}
+
+socket.on("connect", () => {
+  console.log('socket.io connected: ' + socket.id);
+  setServerStatus(true)
+});
+
+socket.on("disconnect", () => {
+  console.log("socket.io disconnected"); // undefined
+  setServerStatus(false)
+});
+
+socket.on("printers", (data) => {
+  console.log(JSON.stringify(data))
+  printers = data
+  $("#printersList").empty()
+  addPrinters(data)
+});
+
+socket.on("printer_response", (data) => {
+  switch (data.Data.Cmd) {
+    case SDCP_CMD_STATUS:
+    case SDCP_CMD_ATTRIBUTES:
+      //console.log(JSON.stringify(data))
+      break
+    case SDCP_CMD_RETRIEVE_FILE_LIST:
+      handle_printer_files(data.Data.MainboardID, data.Data.Data.FileList)
+      break
+    default:
+      console.log(data)
+      break
+  }
+});
+
+socket.on("printer_error", (data) => {
+  console.log("=== ERROR ===")
+  console.log(data)
+  alert("Error Code:" + data.Data.Data.ErrorCode)
+});
+
+socket.on("printer_notice", (data) => {
+  console.log("=== NOTICE ===")
+  console.log(data)
+  alert("Notice:" + data.Data.Data.Message)
+});
+
+socket.on("printer_status", (data) => {
+  //console.log(JSON.stringify(data))
+  var fields = {}
+  var filter = ['CurrentStatus', 'PrintScreen', 'ReleaseFilm', 'TempOfUVLED', 'TimeLapseStatus', 'PrintInfo']
+  $.each(data.Status, function (key, val) {
+    if (filter.includes(key)) {
+      fields[key] = val
+    }
+  })
+  printers[data.MainboardID]['status'] = fields
+  updatePrinterStatus(data)
+});
+
+socket.on("printer_attributes", (data) => {
+  //console.log(JSON.stringify(data))
+  var fields = {}
+  var filter = ['Resolution', 'XYZsize', 'NumberOfVideoStreamConnected', 'MaximumVideoStreamAllowed', 'UsbDiskStatus', 'Capabilities', 'SupportFileType', 'DevicesStatus', 'ReleaseFilmMax', 'CameraStatus', 'RemainingMemory', 'TLPNoCapPos', 'TLPStartCapPos', 'TLPInterLayers']
+  $.each(data.Attributes, function (key, val) {
+    if (filter.includes(key)) {
+      fields[key] = val
+    }
+  })
+  printers[data.MainboardID]['attributes'] = fields
+  //updatePrinterAttributes(data)
+});
+
+function handle_printer_files(id, data) {
+  if (printers[id]['files'] == undefined) {
+    printers[id]['files'] = []
+  }
+  $.each(data, function (i, f) {
+    if (f.type === 0) {
+      getPrinterFiles(id, f.name)
+    } else {
+      printers[id]['files'].push(f.name)
+      createTable('Files', [f.name])
+    }
+  })
+}
+
+
+function addPrinters(printers) {
+  $.each(printers, function (id, printer) {
+    var template = $("#tmplPrintersListItem").html()
+    var item = $(template)
+    var printerIcon = (printer.brand + '_' + printer.model).split(" ").join("").toLowerCase()
+    item.attr('id', 'printer_' + id)
+    item.attr("data-connection-id", printer.connection)
+    item.attr("data-printer-id", id)
+    item.find(".printerName").text(printer.name)
+    item.find(".printerType").text(printer.brand + ' ' + printer.model)
+    item.find(".printerIcon").attr("src", 'img/' + printerIcon + '.webp')
+    item.on('click', function () {
+      // $.each($('.printerListItem'), function () {
+      //   $(this).removeClass('active')
+      // })
+      // $(this).addClass('active')
+      showPrinter($(this).data('printer-id'))
+    })
+    $("#printersList").append(item)
+    socket.emit("printer_info", { id: id })
+  });
+}
+
+function showPrinter(id) {
+  //console.log(JSON.stringify(printers[id]))
+  var p = printers[id]
+  var printerIcon = (p.brand + '_' + p.model).split(" ").join("").toLowerCase()
+  $('#printerName').text(p.name)
+  $('#printerType').text(p.brand + ' ' + p.model)
+  $("#printerIcon").attr("src", 'img/' + printerIcon + '.webp')
+
+  createTable('Status', p.status, true)
+  createTable('Attributes', p.attributes)
+  createTable('Print', p.status.PrintInfo)
+  
+  // only get files once
+  if (printers[id]['files'] == undefined) {
+    getPrinterFiles(id, '/local')
+    if (p.attributes.UsbDiskStatus == 1) {
+      getPrinterFiles(id, '/usb')
+    }
+  }
+}
+
+function createTable(name, data, active = false) {
+  if ($('#tab-'+name).length == 0) {
+    var tTab = $("#tmplNavTab").html()
+    var tab = $(tTab)
+    tab.find('button').attr('id', 'tab-'+name)
+    tab.find('button').attr('data-bs-target', '#tab'+name)
+    tab.find('button').text(name)
+    if(active) {
+      tab.find('button').addClass('active')
+    }
+    $('#navTabs').append(tab)
+  }
+
+  if ($('#tab'+name).length == 0) {
+    var tPane = $("#tmplNavPane").html()
+    var pane = $(tPane)
+    pane.attr('id', 'tab'+name)
+    pane.find('tbody').attr('id', 'table'+name)
+    if(active) {
+      pane.addClass('active')
+    }
+    $('#navPanes').append(pane)
+  }
+  fillTable(name, data)
+}
+
+function fillTable(table, data) {
+  var t = $('#table'+table)
+  $.each(data, function (key, val) {
+    if (typeof val === 'object') {
+      val = JSON.stringify(val)
+    }
+    var row = $('<tr><td>' + key + '</td><td>' + val + '</td></tr>')
+    t.append(row)
+  })
+}
+
+function getPrinterFiles(id, url) {
+  socket.emit("printer_files", { id: id, url: url })
+}
+
+function updatePrinterStatus(data) {
+  var info = $('#printer_' + data.MainboardID).find('.printerInfo')
+  switch (data.Status.CurrentStatus[0]) {
+    case SDCP_MACHINE_STATUS_IDLE:
+      info.text("Idle")
+      updatePrinterStatusIcon(data.MainboardID, "success", false)
+      break
+    case SDCP_MACHINE_STATUS_PRINTING:
+      info.text("Printing")
+      updatePrinterStatusIcon(data.MainboardID, "primary", true)
+      break
+    case SDCP_MACHINE_STATUS_FILE_TRANSFERRING:
+      info.text("File Transfer")
+      updatePrinterStatusIcon(data.MainboardID, "warning", true)
+      break
+    case SDCP_MACHINE_STATUS_EXPOSURE_TESTING:
+      info.text("Exposure Test")
+      updatePrinterStatusIcon(data.MainboardID, "info", true)
+      break
+    case SDCP_MACHINE_STATUS_DEVICES_TESTING:
+      info.text("Device Self-Test")
+      updatePrinterStatusIcon(data.MainboardID, "warning", true)
+      break
+    default:
+      break
+  }
+}
+
+function updatePrinterStatusIcon(id, style, spinner) {
+  var el = 'printerStatus'
+  if (spinner) {
+    el = 'printerSpinner'
+    $('.printerStatus').addClass('visually-hidden')
+    $('.printerSpinner').removeClass('visually-hidden')
+  } else {
+    $('.printerStatus').removeClass('visually-hidden')
+    $('.printerSpinner').addClass('visually-hidden')
+  }
+  var status = $('#printer_' + id).find('.' + el)
+  status.removeClass(function (index, css) {
+    return (css.match(/\btext-\S+/g) || []).join(' ');
+  }).addClass("text-" + style);
+  status.find('i').removeClass().addClass('bi-circle-fill')
+}
+
+function setServerStatus(online) {
+  serverStatus = $('.serverStatus')
+  if (online) {
+    serverStatus.removeClass('bi-cloud text-danger').addClass('bi-cloud-check-fill')
+  } else {
+    serverStatus.removeClass('bi-cloud-check-fill').addClass('bi-cloud text-danger')
+  }
+}
+
+$('.serverStatus').on("mouseenter", function (e) {
+  if ($(this).hasClass('bi-cloud-check-fill')) {
+    $(this).removeClass('bi-cloud-check-fill').addClass('bi-cloud-plus text-primary')
+  }
+});
+$('.serverStatus').on("mouseleave", function (e) {
+  $(this).removeClass('bi-cloud-plus text-primary').addClass('bi-cloud-check-fill')
+});
+$('.serverStatus').on('click', function (e) {
+  socket.emit("printers", "{}")
+});
+
+/* global bootstrap: false */
+(() => {
+  'use strict'
+  const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
+  tooltipTriggerList.forEach(tooltipTriggerEl => {
+    new bootstrap.Tooltip(tooltipTriggerEl)
+  })
+})()
diff --git a/js/color-modes.js b/web/js/color-modes.js
similarity index 100%
rename from js/color-modes.js
rename to web/js/color-modes.js
diff --git a/js/jquery-3.7.1.min.js b/web/js/jquery-3.7.1.min.js
similarity index 100%
rename from js/jquery-3.7.1.min.js
rename to web/js/jquery-3.7.1.min.js
diff --git a/js/jquery-3.7.1.min.map b/web/js/jquery-3.7.1.min.map
similarity index 100%
rename from js/jquery-3.7.1.min.map
rename to web/js/jquery-3.7.1.min.map
diff --git a/web/js/sdcp.js b/web/js/sdcp.js
new file mode 100644
index 0000000..e4a2802
--- /dev/null
+++ b/web/js/sdcp.js
@@ -0,0 +1,95 @@
+// MACHINE_STATUS
+const SDCP_MACHINE_STATUS_IDLE = 0  // Idle
+const SDCP_MACHINE_STATUS_PRINTING = 1  // Executing print task
+const SDCP_MACHINE_STATUS_FILE_TRANSFERRING = 2  // File transfer in progress
+const SDCP_MACHINE_STATUS_EXPOSURE_TESTING = 3  // Exposure test in progress
+const SDCP_MACHINE_STATUS_DEVICES_TESTING = 4  //Device self-check in progress
+// PRINT_STATUS
+const SDCP_PRINT_STATUS_IDLE = 0  // Idle
+const SDCP_PRINT_STATUS_HOMING = 1  // Resetting
+const SDCP_PRINT_STATUS_DROPPING = 2  // Descending
+const SDCP_PRINT_STATUS_EXPOSURING = 3  // Exposing
+const SDCP_PRINT_STATUS_LIFTING = 4  // Lifting
+const SDCP_PRINT_STATUS_PAUSING = 5  // Executing Pause Action
+const SDCP_PRINT_STATUS_PAUSED = 6  // Suspended
+const SDCP_PRINT_STATUS_STOPPING = 7  // Executing Stop Action
+const SDCP_PRINT_STATUS_STOPED = 8  // Stopped
+const SDCP_PRINT_STATUS_COMPLETE = 9  // Print Completed
+const SDCP_PRINT_STATUS_FILE_CHECKING = 10 // File Checking in Progress
+// PRINT_ERROR
+const SDCP_PRINT_ERROR_NONE = 0  // Normal
+const SDCP_PRINT_ERROR_CHECK = 1  // File MD5 Check Failed
+const SDCP_PRINT_ERROR_FILEIO = 2  // File Read Failed
+const SDCP_PRINT_ERROR_INVLAID_RESOLUTION = 3  // Resolution Mismatch
+const SDCP_PRINT_ERROR_UNKNOWN_FORMAT = 4  // Format Mismatch
+const SDCP_PRINT_ERROR_UNKNOWN_MODEL = 5  // Machine Model Mismatch
+// FILE_TRANSFER
+const SDCP_FILE_TRANSFER_ACK_SUCCESS = 0  // Success
+const SDCP_FILE_TRANSFER_ACK_NOT_TRANSFER = 1  // The printer is not currently transferring files.
+const SDCP_FILE_TRANSFER_ACK_CHECKING = 2  // The printer is already in the file verification phase.
+const SDCP_FILE_TRANSFER_ACK_NOT_FOUND = 3  // File not found.
+// PRINT_CTRL
+const SDCP_PRINT_CTRL_ACK_OK = 0  // OK
+const SDCP_PRINT_CTRL_ACK_BUSY = 1  // Busy
+const SDCP_PRINT_CTRL_ACK_NOT_FOUND = 2  // File Not Found
+const SDCP_PRINT_CTRL_ACK_MD5_FAILED = 3  // MD5 Verification Failed
+const SDCP_PRINT_CTRL_ACK_FILEIO_FAILED = 4  // File Read Failed
+const SDCP_PRINT_CTRL_ACK_INVLAID_RESOLUTION = 5 // Resolution Mismatch
+const SDCP_PRINT_CTRL_ACK_UNKNOW_FORMAT = 6  // Unrecognized File Format
+const SDCP_PRINT_CTRL_ACK_UNKNOW_MODEL = 7  // Machine Model Mismatch
+// PRINT_CAUSE
+const SDCP_PRINT_CAUSE_OK = 0  // Normal
+const SDCP_PRINT_CAUSE_TEMP_ERROR = 1  // Over-temperature
+const SDCP_PRINT_CAUSE_CALIBRATE_FAILED = 2  // Strain Gauge Calibration Failed
+const SDCP_PRINT_CAUSE_RESIN_LACK = 3  // Resin Level Low Detected
+const SDCP_PRINT_CAUSE_RESIN_OVER = 4  // The volume of resin required by the model exceeds the maximum capacity of the resin vat
+const SDCP_PRINT_CAUSE_PROBE_FAIL = 5  // No Resin Detected
+const SDCP_PRINT_CAUSE_FOREIGN_BODY = 6  // Foreign Object Detected
+const SDCP_PRINT_CAUSE_LEVEL_FAILED = 7  // Auto-leveling Failed
+const SDCP_PRINT_CAUSE_RELEASE_FAILED = 8  // Model Detachment Detected
+const SDCP_PRINT_CAUSE_SG_OFFLINE = 9  // Strain Gauge Not Connected
+const SDCP_PRINT_CAUSE_LCD_DET_FAILED = 10  // LCD Screen Connection Abnormal
+const SDCP_PRINT_CAUSE_RELEASE_OVERCOUNT = 11  // The cumulative release film usage has reached the maximum value
+const SDCP_PRINT_CAUSE_UDISK_REMOVE = 12  // USB drive detected as removed, printing has been stopped
+const SDCP_PRINT_CAUSE_HOME_FAILED_X = 13  // Detection of X-axis motor anomaly, printing has been stopped
+const SDCP_PRINT_CAUSE_HOME_FAILED_Z = 14  // Detection of Z-axis motor anomaly, printing has been stopped
+const SDCP_PRINT_CAUSE_RESIN_ABNORMAL_HIGH = 15  // The resin level has been detected to exceed the maximum value, and printing has been stopped
+const SDCP_PRINT_CAUSE_RESIN_ABNORMAL_LOW = 16  // Resin level detected as too low, printing has been stopped
+const SDCP_PRINT_CAUSE_HOME_FAILED = 17  // Home position calibration failed, please check if the motor or limit switch is functioning properly
+const SDCP_PRINT_CAUSE_PLAT_FAILED = 18  // A model is detected on the platform; please clean it and then restart printing
+const SDCP_PRINT_CAUSE_ERROR = 19  // Printing Exception
+const SDCP_PRINT_CAUSE_MOVE_ABNORMAL = 20  // Motor Movement Abnormality
+const SDCP_PRINT_CAUSE_AIC_MODEL_NONE = 21  // No model detected, please troubleshoot
+const SDCP_PRINT_CAUSE_AIC_MODEL_WARP = 22  // Warping of the model detected, please investigate
+const SDCP_PRINT_CAUSE_HOME_FAILED_Y = 23  // Deprecated
+// PRINT_CAUSED
+const SDCP_PRINT_CAUSED_FILE_ERROR = 24  // Error File
+const SDCP_PRINT_CAUSED_CAMERA_ERROR = 25  // Camera Error. Please check if the camera is properly connected, or you can also disable this feature to continue printing
+const SDCP_PRINT_CAUSED_NETWORK_ERROR = 26  // Network Connection Error. Please check if your network connection is stable, or you can also disable this feature to continue printing
+const SDCP_PRINT_CAUSED_SERVER_CONNECT_FAILED = 27 // Server Connection Failed. Please contact our customer support, or you can also disable this feature to continue printing
+const SDCP_PRINT_CAUSED_DISCONNECT_APP = 28  // This printer is not bound to an app. To perform time-lapse photography, please first enable the remote control feature, or you can also disable this feature to continue printing
+const SDCP_PIRNT_CAUSED_CHECK_AUTO_RESIN_FEEDER = 29  // lease check the installation of the "automatic material extraction / feeding machine"
+const SDCP_PRINT_CAUSED_CONTAINER_RESIN_LOW = 30  // The resin in the container is running low. Add more resin to automatically close this notification, or click "Stop Auto Feeding" to continue printing
+const SDCP_PRINT_CAUSED_BOTTLE_DISCONNECT = 31  // Please ensure that the automatic material extraction/feeding machine is correctly installed and the data cable is connected
+const SDCP_PRINT_CAUSED_FEED_TIMEOUT = 32  // Automatic material extraction timeout, please check if the resin tube is blocked
+const SDCP_PRINT_CAUSE_TANK_TEMP_SENSOR_OFFLINE = 33  // Resin vat temperature sensor not connected
+const SDCP_PRINT_CAUSE_TANK_TEMP_SENSOR_ERRO = 34  // Resin vat temperature sensor indicates an over-temperature condition
+const SDCP_ERROR_CODE_MD5_FAILED = 1  // File Transfer MD5 Check Failed
+const SDCP_ERROR_CODE_FORMAT_FAILED = 2  // File format is incorrect
+// CMD
+const SDCP_CMD_STATUS = 0
+const SDCP_CMD_ATTRIBUTES = 1
+const SDCP_CMD_START_PRINTING = 128
+const SDCP_CMD_PAUSE_PRINTING = 129
+const SDCP_CMD_STOP_PRINTING = 130
+const SDCP_CMD_CONTINUE_PRINTING = 131
+const SDCP_CMD_STOP_FEEDING = 132
+const SDCP_CMD_SKIP_PREHEATING = 133
+const SDCP_CMD_CHANGE_PRINTER_NAME = 192 // "Data": {"Name": "newName"}
+const SDCP_CMD_TERMINATE_FILE_TRANSFER = 255
+const SDCP_CMD_RETRIEVE_FILE_LIST = 258 // "Data": {"Url": "/usb/path"}
+const SDCP_CMD_BATCH_DELETE_FILES = 259 // "Data": {"FileList": ["/usb/xx.x"], "FolderList": ["/usb/yy"]}
+const SDCP_CMD_RETRIEVE_TASKS_HISTORY = 320
+const SDCP_CMD_RETRIEVE_TASK_DETAILS = 321 // "Data": {"Id": ["xxxxxxxxxxx"]}
+const SDCP_CMD_VIDEO_STREAMING = 386 // "Data": {"Enable" : 0/1}
+const SDCP_CMD_TIMELAPSE = 387 // "Data": {"Enable" : 0/1}
diff --git a/web/js/socket.io.min.js b/web/js/socket.io.min.js
new file mode 100644
index 0000000..d6b2d60
--- /dev/null
+++ b/web/js/socket.io.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Socket.IO v4.7.5
+ * (c) 2014-2024 Guillermo Rauch
+ * Released under the MIT License.
+ */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).io=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,(i=r.key,o=void 0,"symbol"==typeof(o=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(i,"string"))?o:String(o)),r)}var i,o}function r(e,t,r){return t&&n(e.prototype,t),r&&n(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function i(){return i=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},i.apply(this,arguments)}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&a(e,t)}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function a(e,t){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},a(e,t)}function c(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function u(e,t,n){return u=c()?Reflect.construct.bind():function(e,t,n){var r=[null];r.push.apply(r,t);var i=new(Function.bind.apply(e,r));return n&&a(i,n.prototype),i},u.apply(null,arguments)}function h(e){var t="function"==typeof Map?new Map:void 0;return h=function(e){if(null===e||(n=e,-1===Function.toString.call(n).indexOf("[native code]")))return e;var n;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return u(e,arguments,s(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),a(r,e)},h(e)}function f(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function l(e){var t=c();return function(){var n,r=s(e);if(t){var i=s(this).constructor;n=Reflect.construct(r,arguments,i)}else n=r.apply(this,arguments);return function(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return f(e)}(this,n)}}function p(){return p="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(e,t,n){var r=function(e,t){for(;!Object.prototype.hasOwnProperty.call(e,t)&&null!==(e=s(e)););return e}(e,t);if(r){var i=Object.getOwnPropertyDescriptor(r,t);return i.get?i.get.call(arguments.length<3?e:n):i.value}},p.apply(this,arguments)}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function y(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?d(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){a=!0,o=e},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var g=Object.create(null);Object.keys(v).forEach((function(e){g[v[e]]=e}));var m,b={type:"error",data:"parser error"},k="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),w="function"==typeof ArrayBuffer,_=function(e){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer},E=function(e,t,n){var r=e.type,i=e.data;return k&&i instanceof Blob?t?n(i):A(i,n):w&&(i instanceof ArrayBuffer||_(i))?t?n(i):A(new Blob([i]),n):n(v[r]+(i||""))},A=function(e,t){var n=new FileReader;return n.onload=function(){var e=n.result.split(",")[1];t("b"+(e||""))},n.readAsDataURL(e)};function O(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}for(var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",R="undefined"==typeof Uint8Array?[]:new Uint8Array(256),C=0;C<64;C++)R[T.charCodeAt(C)]=C;var B,S="function"==typeof ArrayBuffer,N=function(e,t){if("string"!=typeof e)return{type:"message",data:x(e,t)};var n=e.charAt(0);return"b"===n?{type:"message",data:L(e.substring(1),t)}:g[n]?e.length>1?{type:g[n],data:e.substring(1)}:{type:g[n]}:b},L=function(e,t){if(S){var n=function(e){var t,n,r,i,o,s=.75*e.length,a=e.length,c=0;"="===e[e.length-1]&&(s--,"="===e[e.length-2]&&s--);var u=new ArrayBuffer(s),h=new Uint8Array(u);for(t=0;t<a;t+=4)n=R[e.charCodeAt(t)],r=R[e.charCodeAt(t+1)],i=R[e.charCodeAt(t+2)],o=R[e.charCodeAt(t+3)],h[c++]=n<<2|r>>4,h[c++]=(15&r)<<4|i>>2,h[c++]=(3&i)<<6|63&o;return u}(e);return x(n,t)}return{base64:!0,data:e}},x=function(e,t){return"blob"===t?e instanceof Blob?e:new Blob([e]):e instanceof ArrayBuffer?e:e.buffer},P=String.fromCharCode(30);function j(){return new TransformStream({transform:function(e,t){!function(e,t){k&&e.data instanceof Blob?e.data.arrayBuffer().then(O).then(t):w&&(e.data instanceof ArrayBuffer||_(e.data))?t(O(e.data)):E(e,!1,(function(e){m||(m=new TextEncoder),t(m.encode(e))}))}(e,(function(n){var r,i=n.length;if(i<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,i);else if(i<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,i)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(i))}e.data&&"string"!=typeof e.data&&(r[0]|=128),t.enqueue(r),t.enqueue(n)}))}})}function q(e){return e.reduce((function(e,t){return e+t.length}),0)}function D(e,t){if(e[0].length===t)return e.shift();for(var n=new Uint8Array(t),r=0,i=0;i<t;i++)n[i]=e[0][r++],r===e[0].length&&(e.shift(),r=0);return e.length&&r<e[0].length&&(e[0]=e[0].slice(r)),n}function U(e){if(e)return function(e){for(var t in U.prototype)e[t]=U.prototype[t];return e}(e)}U.prototype.on=U.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks["$"+e]=this._callbacks["$"+e]||[]).push(t),this},U.prototype.once=function(e,t){function n(){this.off(e,n),t.apply(this,arguments)}return n.fn=t,this.on(e,n),this},U.prototype.off=U.prototype.removeListener=U.prototype.removeAllListeners=U.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n,r=this._callbacks["$"+e];if(!r)return this;if(1==arguments.length)return delete this._callbacks["$"+e],this;for(var i=0;i<r.length;i++)if((n=r[i])===t||n.fn===t){r.splice(i,1);break}return 0===r.length&&delete this._callbacks["$"+e],this},U.prototype.emit=function(e){this._callbacks=this._callbacks||{};for(var t=new Array(arguments.length-1),n=this._callbacks["$"+e],r=1;r<arguments.length;r++)t[r-1]=arguments[r];if(n){r=0;for(var i=(n=n.slice(0)).length;r<i;++r)n[r].apply(this,t)}return this},U.prototype.emitReserved=U.prototype.emit,U.prototype.listeners=function(e){return this._callbacks=this._callbacks||{},this._callbacks["$"+e]||[]},U.prototype.hasListeners=function(e){return!!this.listeners(e).length};var I="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")();function F(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];return n.reduce((function(t,n){return e.hasOwnProperty(n)&&(t[n]=e[n]),t}),{})}var M=I.setTimeout,V=I.clearTimeout;function H(e,t){t.useNativeTimers?(e.setTimeoutFn=M.bind(I),e.clearTimeoutFn=V.bind(I)):(e.setTimeoutFn=I.setTimeout.bind(I),e.clearTimeoutFn=I.clearTimeout.bind(I))}var K,Y=function(e){o(i,e);var n=l(i);function i(e,r,o){var s;return t(this,i),(s=n.call(this,e)).description=r,s.context=o,s.type="TransportError",s}return r(i)}(h(Error)),W=function(e){o(i,e);var n=l(i);function i(e){var r;return t(this,i),(r=n.call(this)).writable=!1,H(f(r),e),r.opts=e,r.query=e.query,r.socket=e.socket,r}return r(i,[{key:"onError",value:function(e,t,n){return p(s(i.prototype),"emitReserved",this).call(this,"error",new Y(e,t,n)),this}},{key:"open",value:function(){return this.readyState="opening",this.doOpen(),this}},{key:"close",value:function(){return"opening"!==this.readyState&&"open"!==this.readyState||(this.doClose(),this.onClose()),this}},{key:"send",value:function(e){"open"===this.readyState&&this.write(e)}},{key:"onOpen",value:function(){this.readyState="open",this.writable=!0,p(s(i.prototype),"emitReserved",this).call(this,"open")}},{key:"onData",value:function(e){var t=N(e,this.socket.binaryType);this.onPacket(t)}},{key:"onPacket",value:function(e){p(s(i.prototype),"emitReserved",this).call(this,"packet",e)}},{key:"onClose",value:function(e){this.readyState="closed",p(s(i.prototype),"emitReserved",this).call(this,"close",e)}},{key:"pause",value:function(e){}},{key:"createUri",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e+"://"+this._hostname()+this._port()+this.opts.path+this._query(t)}},{key:"_hostname",value:function(){var e=this.opts.hostname;return-1===e.indexOf(":")?e:"["+e+"]"}},{key:"_port",value:function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""}},{key:"_query",value:function(e){var t=function(e){var t="";for(var n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t}(e);return t.length?"?"+t:""}}]),i}(U),z="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),J=64,$={},Q=0,X=0;function G(e){var t="";do{t=z[e%J]+t,e=Math.floor(e/J)}while(e>0);return t}function Z(){var e=G(+new Date);return e!==K?(Q=0,K=e):e+"."+G(Q++)}for(;X<J;X++)$[z[X]]=X;var ee=!1;try{ee="undefined"!=typeof XMLHttpRequest&&"withCredentials"in new XMLHttpRequest}catch(e){}var te=ee;function ne(e){var t=e.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!t||te))return new XMLHttpRequest}catch(e){}if(!t)try{return new(I[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(e){}}function re(){}var ie=null!=new ne({xdomain:!1}).responseType,oe=function(e){o(s,e);var n=l(s);function s(e){var r;if(t(this,s),(r=n.call(this,e)).polling=!1,"undefined"!=typeof location){var i="https:"===location.protocol,o=location.port;o||(o=i?"443":"80"),r.xd="undefined"!=typeof location&&e.hostname!==location.hostname||o!==e.port}var a=e&&e.forceBase64;return r.supportsBinary=ie&&!a,r.opts.withCredentials&&(r.cookieJar=void 0),r}return r(s,[{key:"name",get:function(){return"polling"}},{key:"doOpen",value:function(){this.poll()}},{key:"pause",value:function(e){var t=this;this.readyState="pausing";var n=function(){t.readyState="paused",e()};if(this.polling||!this.writable){var r=0;this.polling&&(r++,this.once("pollComplete",(function(){--r||n()}))),this.writable||(r++,this.once("drain",(function(){--r||n()})))}else n()}},{key:"poll",value:function(){this.polling=!0,this.doPoll(),this.emitReserved("poll")}},{key:"onData",value:function(e){var t=this;(function(e,t){for(var n=e.split(P),r=[],i=0;i<n.length;i++){var o=N(n[i],t);if(r.push(o),"error"===o.type)break}return r})(e,this.socket.binaryType).forEach((function(e){if("opening"===t.readyState&&"open"===e.type&&t.onOpen(),"close"===e.type)return t.onClose({description:"transport closed by the server"}),!1;t.onPacket(e)})),"closed"!==this.readyState&&(this.polling=!1,this.emitReserved("pollComplete"),"open"===this.readyState&&this.poll())}},{key:"doClose",value:function(){var e=this,t=function(){e.write([{type:"close"}])};"open"===this.readyState?t():this.once("open",t)}},{key:"write",value:function(e){var t=this;this.writable=!1,function(e,t){var n=e.length,r=new Array(n),i=0;e.forEach((function(e,o){E(e,!1,(function(e){r[o]=e,++i===n&&t(r.join(P))}))}))}(e,(function(e){t.doWrite(e,(function(){t.writable=!0,t.emitReserved("drain")}))}))}},{key:"uri",value:function(){var e=this.opts.secure?"https":"http",t=this.query||{};return!1!==this.opts.timestampRequests&&(t[this.opts.timestampParam]=Z()),this.supportsBinary||t.sid||(t.b64=1),this.createUri(e,t)}},{key:"request",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return i(e,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new se(this.uri(),e)}},{key:"doWrite",value:function(e,t){var n=this,r=this.request({method:"POST",data:e});r.on("success",t),r.on("error",(function(e,t){n.onError("xhr post error",e,t)}))}},{key:"doPoll",value:function(){var e=this,t=this.request();t.on("data",this.onData.bind(this)),t.on("error",(function(t,n){e.onError("xhr poll error",t,n)})),this.pollXhr=t}}]),s}(W),se=function(e){o(i,e);var n=l(i);function i(e,r){var o;return t(this,i),H(f(o=n.call(this)),r),o.opts=r,o.method=r.method||"GET",o.uri=e,o.data=void 0!==r.data?r.data:null,o.create(),o}return r(i,[{key:"create",value:function(){var e,t=this,n=F(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this.opts.xd;var r=this.xhr=new ne(n);try{r.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders)for(var o in r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this.opts.extraHeaders[o])}catch(e){}if("POST"===this.method)try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(e){}try{r.setRequestHeader("Accept","*/*")}catch(e){}null===(e=this.opts.cookieJar)||void 0===e||e.addCookies(r),"withCredentials"in r&&(r.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(r.timeout=this.opts.requestTimeout),r.onreadystatechange=function(){var e;3===r.readyState&&(null===(e=t.opts.cookieJar)||void 0===e||e.parseCookies(r)),4===r.readyState&&(200===r.status||1223===r.status?t.onLoad():t.setTimeoutFn((function(){t.onError("number"==typeof r.status?r.status:0)}),0))},r.send(this.data)}catch(e){return void this.setTimeoutFn((function(){t.onError(e)}),0)}"undefined"!=typeof document&&(this.index=i.requestsCount++,i.requests[this.index]=this)}},{key:"onError",value:function(e){this.emitReserved("error",e,this.xhr),this.cleanup(!0)}},{key:"cleanup",value:function(e){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=re,e)try{this.xhr.abort()}catch(e){}"undefined"!=typeof document&&delete i.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var e=this.xhr.responseText;null!==e&&(this.emitReserved("data",e),this.emitReserved("success"),this.cleanup())}},{key:"abort",value:function(){this.cleanup()}}]),i}(U);if(se.requestsCount=0,se.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",ae);else if("function"==typeof addEventListener){addEventListener("onpagehide"in I?"pagehide":"unload",ae,!1)}function ae(){for(var e in se.requests)se.requests.hasOwnProperty(e)&&se.requests[e].abort()}var ce="function"==typeof Promise&&"function"==typeof Promise.resolve?function(e){return Promise.resolve().then(e)}:function(e,t){return t(e,0)},ue=I.WebSocket||I.MozWebSocket,he="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),fe=function(e){o(i,e);var n=l(i);function i(e){var r;return t(this,i),(r=n.call(this,e)).supportsBinary=!e.forceBase64,r}return r(i,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var e=this.uri(),t=this.opts.protocols,n=he?{}:F(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=he?new ue(e,t,n):t?new ue(e,t):new ue(e)}catch(e){return this.emitReserved("error",e)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}}},{key:"addEventListeners",value:function(){var e=this;this.ws.onopen=function(){e.opts.autoUnref&&e.ws._socket.unref(),e.onOpen()},this.ws.onclose=function(t){return e.onClose({description:"websocket connection closed",context:t})},this.ws.onmessage=function(t){return e.onData(t.data)},this.ws.onerror=function(t){return e.onError("websocket error",t)}}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;E(n,t.supportsBinary,(function(e){try{t.ws.send(e)}catch(e){}i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;r<e.length;r++)n()}},{key:"doClose",value:function(){void 0!==this.ws&&(this.ws.close(),this.ws=null)}},{key:"uri",value:function(){var e=this.opts.secure?"wss":"ws",t=this.query||{};return this.opts.timestampRequests&&(t[this.opts.timestampParam]=Z()),this.supportsBinary||(t.b64=1),this.createUri(e,t)}},{key:"check",value:function(){return!!ue}}]),i}(W),le=function(e){o(i,e);var n=l(i);function i(){return t(this,i),n.apply(this,arguments)}return r(i,[{key:"name",get:function(){return"webtransport"}},{key:"doOpen",value:function(){var e=this;"function"==typeof WebTransport&&(this.transport=new WebTransport(this.createUri("https"),this.opts.transportOptions[this.name]),this.transport.closed.then((function(){e.onClose()})).catch((function(t){e.onError("webtransport error",t)})),this.transport.ready.then((function(){e.transport.createBidirectionalStream().then((function(t){var n=function(e,t){B||(B=new TextDecoder);var n=[],r=0,i=-1,o=!1;return new TransformStream({transform:function(s,a){for(n.push(s);;){if(0===r){if(q(n)<1)break;var c=D(n,1);o=128==(128&c[0]),i=127&c[0],r=i<126?3:126===i?1:2}else if(1===r){if(q(n)<2)break;var u=D(n,2);i=new DataView(u.buffer,u.byteOffset,u.length).getUint16(0),r=3}else if(2===r){if(q(n)<8)break;var h=D(n,8),f=new DataView(h.buffer,h.byteOffset,h.length),l=f.getUint32(0);if(l>Math.pow(2,21)-1){a.enqueue(b);break}i=l*Math.pow(2,32)+f.getUint32(4),r=3}else{if(q(n)<i)break;var p=D(n,i);a.enqueue(N(o?p:B.decode(p),t)),r=0}if(0===i||i>e){a.enqueue(b);break}}}})}(Number.MAX_SAFE_INTEGER,e.socket.binaryType),r=t.readable.pipeThrough(n).getReader(),i=j();i.readable.pipeTo(t.writable),e.writer=i.writable.getWriter();!function t(){r.read().then((function(n){var r=n.done,i=n.value;r||(e.onPacket(i),t())})).catch((function(e){}))}();var o={type:"open"};e.query.sid&&(o.data='{"sid":"'.concat(e.query.sid,'"}')),e.writer.write(o).then((function(){return e.onOpen()}))}))})))}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;t.writer.write(n).then((function(){i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;r<e.length;r++)n()}},{key:"doClose",value:function(){var e;null===(e=this.transport)||void 0===e||e.close()}}]),i}(W),pe={websocket:fe,webtransport:le,polling:oe},de=/^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,ye=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];function ve(e){var t=e,n=e.indexOf("["),r=e.indexOf("]");-1!=n&&-1!=r&&(e=e.substring(0,n)+e.substring(n,r).replace(/:/g,";")+e.substring(r,e.length));for(var i,o,s=de.exec(e||""),a={},c=14;c--;)a[ye[c]]=s[c]||"";return-1!=n&&-1!=r&&(a.source=t,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a.pathNames=function(e,t){var n=/\/{2,9}/g,r=t.replace(n,"/").split("/");"/"!=t.slice(0,1)&&0!==t.length||r.splice(0,1);"/"==t.slice(-1)&&r.splice(r.length-1,1);return r}(0,a.path),a.queryKey=(i=a.query,o={},i.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(e,t,n){t&&(o[t]=n)})),o),a}var ge=function(n){o(a,n);var s=l(a);function a(n){var r,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return t(this,a),(r=s.call(this)).binaryType="arraybuffer",r.writeBuffer=[],n&&"object"===e(n)&&(o=n,n=null),n?(n=ve(n),o.hostname=n.host,o.secure="https"===n.protocol||"wss"===n.protocol,o.port=n.port,n.query&&(o.query=n.query)):o.host&&(o.hostname=ve(o.host).host),H(f(r),o),r.secure=null!=o.secure?o.secure:"undefined"!=typeof location&&"https:"===location.protocol,o.hostname&&!o.port&&(o.port=r.secure?"443":"80"),r.hostname=o.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=o.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=o.transports||["polling","websocket","webtransport"],r.writeBuffer=[],r.prevBufferLen=0,r.opts=i({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},o),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(e){for(var t={},n=e.split("&"),r=0,i=n.length;r<i;r++){var o=n[r].split("=");t[decodeURIComponent(o[0])]=decodeURIComponent(o[1])}return t}(r.opts.query)),r.id=null,r.upgrades=null,r.pingInterval=null,r.pingTimeout=null,r.pingTimeoutTimer=null,"function"==typeof addEventListener&&(r.opts.closeOnBeforeunload&&(r.beforeunloadEventListener=function(){r.transport&&(r.transport.removeAllListeners(),r.transport.close())},addEventListener("beforeunload",r.beforeunloadEventListener,!1)),"localhost"!==r.hostname&&(r.offlineEventListener=function(){r.onClose("transport close",{description:"network connection lost"})},addEventListener("offline",r.offlineEventListener,!1))),r.open(),r}return r(a,[{key:"createTransport",value:function(e){var t=i({},this.opts.query);t.EIO=4,t.transport=e,this.id&&(t.sid=this.id);var n=i({},this.opts,{query:t,socket:this,hostname:this.hostname,secure:this.secure,port:this.port},this.opts.transportOptions[e]);return new pe[e](n)}},{key:"open",value:function(){var e,t=this;if(this.opts.rememberUpgrade&&a.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket"))e="websocket";else{if(0===this.transports.length)return void this.setTimeoutFn((function(){t.emitReserved("error","No transports available")}),0);e=this.transports[0]}this.readyState="opening";try{e=this.createTransport(e)}catch(e){return this.transports.shift(),void this.open()}e.open(),this.setTransport(e)}},{key:"setTransport",value:function(e){var t=this;this.transport&&this.transport.removeAllListeners(),this.transport=e,e.on("drain",this.onDrain.bind(this)).on("packet",this.onPacket.bind(this)).on("error",this.onError.bind(this)).on("close",(function(e){return t.onClose("transport close",e)}))}},{key:"probe",value:function(e){var t=this,n=this.createTransport(e),r=!1;a.priorWebsocketSuccess=!1;var i=function(){r||(n.send([{type:"ping",data:"probe"}]),n.once("packet",(function(e){if(!r)if("pong"===e.type&&"probe"===e.data){if(t.upgrading=!0,t.emitReserved("upgrading",n),!n)return;a.priorWebsocketSuccess="websocket"===n.name,t.transport.pause((function(){r||"closed"!==t.readyState&&(f(),t.setTransport(n),n.send([{type:"upgrade"}]),t.emitReserved("upgrade",n),n=null,t.upgrading=!1,t.flush())}))}else{var i=new Error("probe error");i.transport=n.name,t.emitReserved("upgradeError",i)}})))};function o(){r||(r=!0,f(),n.close(),n=null)}var s=function(e){var r=new Error("probe error: "+e);r.transport=n.name,o(),t.emitReserved("upgradeError",r)};function c(){s("transport closed")}function u(){s("socket closed")}function h(e){n&&e.name!==n.name&&o()}var f=function(){n.removeListener("open",i),n.removeListener("error",s),n.removeListener("close",c),t.off("close",u),t.off("upgrading",h)};n.once("open",i),n.once("error",s),n.once("close",c),this.once("close",u),this.once("upgrading",h),-1!==this.upgrades.indexOf("webtransport")&&"webtransport"!==e?this.setTimeoutFn((function(){r||n.open()}),200):n.open()}},{key:"onOpen",value:function(){if(this.readyState="open",a.priorWebsocketSuccess="websocket"===this.transport.name,this.emitReserved("open"),this.flush(),"open"===this.readyState&&this.opts.upgrade)for(var e=0,t=this.upgrades.length;e<t;e++)this.probe(this.upgrades[e])}},{key:"onPacket",value:function(e){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState)switch(this.emitReserved("packet",e),this.emitReserved("heartbeat"),this.resetPingTimeout(),e.type){case"open":this.onHandshake(JSON.parse(e.data));break;case"ping":this.sendPacket("pong"),this.emitReserved("ping"),this.emitReserved("pong");break;case"error":var t=new Error("server error");t.code=e.data,this.onError(t);break;case"message":this.emitReserved("data",e.data),this.emitReserved("message",e.data)}}},{key:"onHandshake",value:function(e){this.emitReserved("handshake",e),this.id=e.sid,this.transport.query.sid=e.sid,this.upgrades=this.filterUpgrades(e.upgrades),this.pingInterval=e.pingInterval,this.pingTimeout=e.pingTimeout,this.maxPayload=e.maxPayload,this.onOpen(),"closed"!==this.readyState&&this.resetPingTimeout()}},{key:"resetPingTimeout",value:function(){var e=this;this.clearTimeoutFn(this.pingTimeoutTimer),this.pingTimeoutTimer=this.setTimeoutFn((function(){e.onClose("ping timeout")}),this.pingInterval+this.pingTimeout),this.opts.autoUnref&&this.pingTimeoutTimer.unref()}},{key:"onDrain",value:function(){this.writeBuffer.splice(0,this.prevBufferLen),this.prevBufferLen=0,0===this.writeBuffer.length?this.emitReserved("drain"):this.flush()}},{key:"flush",value:function(){if("closed"!==this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length){var e=this.getWritablePackets();this.transport.send(e),this.prevBufferLen=e.length,this.emitReserved("flush")}}},{key:"getWritablePackets",value:function(){if(!(this.maxPayload&&"polling"===this.transport.name&&this.writeBuffer.length>1))return this.writeBuffer;for(var e,t=1,n=0;n<this.writeBuffer.length;n++){var r=this.writeBuffer[n].data;if(r&&(t+="string"==typeof(e=r)?function(e){for(var t=0,n=0,r=0,i=e.length;r<i;r++)(t=e.charCodeAt(r))<128?n+=1:t<2048?n+=2:t<55296||t>=57344?n+=3:(r++,n+=4);return n}(e):Math.ceil(1.33*(e.byteLength||e.size))),n>0&&t>this.maxPayload)return this.writeBuffer.slice(0,n);t+=2}return this.writeBuffer}},{key:"write",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"send",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"sendPacket",value:function(e,t,n,r){if("function"==typeof t&&(r=t,t=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var i={type:e,data:t,options:n};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),r&&this.once("flush",r),this.flush()}}},{key:"close",value:function(){var e=this,t=function(){e.onClose("forced close"),e.transport.close()},n=function n(){e.off("upgrade",n),e.off("upgradeError",n),t()},r=function(){e.once("upgrade",n),e.once("upgradeError",n)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){e.upgrading?r():t()})):this.upgrading?r():t()),this}},{key:"onError",value:function(e){a.priorWebsocketSuccess=!1,this.emitReserved("error",e),this.onClose("transport error",e)}},{key:"onClose",value:function(e,t){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",e,t),this.writeBuffer=[],this.prevBufferLen=0)}},{key:"filterUpgrades",value:function(e){for(var t=[],n=0,r=e.length;n<r;n++)~this.transports.indexOf(e[n])&&t.push(e[n]);return t}}]),a}(U);ge.protocol=4,ge.protocol;var me="function"==typeof ArrayBuffer,be=function(e){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e.buffer instanceof ArrayBuffer},ke=Object.prototype.toString,we="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===ke.call(Blob),_e="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===ke.call(File);function Ee(e){return me&&(e instanceof ArrayBuffer||be(e))||we&&e instanceof Blob||_e&&e instanceof File}function Ae(t,n){if(!t||"object"!==e(t))return!1;if(Array.isArray(t)){for(var r=0,i=t.length;r<i;r++)if(Ae(t[r]))return!0;return!1}if(Ee(t))return!0;if(t.toJSON&&"function"==typeof t.toJSON&&1===arguments.length)return Ae(t.toJSON(),!0);for(var o in t)if(Object.prototype.hasOwnProperty.call(t,o)&&Ae(t[o]))return!0;return!1}function Oe(e){var t=[],n=e.data,r=e;return r.data=Te(n,t),r.attachments=t.length,{packet:r,buffers:t}}function Te(t,n){if(!t)return t;if(Ee(t)){var r={_placeholder:!0,num:n.length};return n.push(t),r}if(Array.isArray(t)){for(var i=new Array(t.length),o=0;o<t.length;o++)i[o]=Te(t[o],n);return i}if("object"===e(t)&&!(t instanceof Date)){var s={};for(var a in t)Object.prototype.hasOwnProperty.call(t,a)&&(s[a]=Te(t[a],n));return s}return t}function Re(e,t){return e.data=Ce(e.data,t),delete e.attachments,e}function Ce(t,n){if(!t)return t;if(t&&!0===t._placeholder){if("number"==typeof t.num&&t.num>=0&&t.num<n.length)return n[t.num];throw new Error("illegal attachments")}if(Array.isArray(t))for(var r=0;r<t.length;r++)t[r]=Ce(t[r],n);else if("object"===e(t))for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(t[i]=Ce(t[i],n));return t}var Be,Se=["connect","connect_error","disconnect","disconnecting","newListener","removeListener"];!function(e){e[e.CONNECT=0]="CONNECT",e[e.DISCONNECT=1]="DISCONNECT",e[e.EVENT=2]="EVENT",e[e.ACK=3]="ACK",e[e.CONNECT_ERROR=4]="CONNECT_ERROR",e[e.BINARY_EVENT=5]="BINARY_EVENT",e[e.BINARY_ACK=6]="BINARY_ACK"}(Be||(Be={}));var Ne=function(){function e(n){t(this,e),this.replacer=n}return r(e,[{key:"encode",value:function(e){return e.type!==Be.EVENT&&e.type!==Be.ACK||!Ae(e)?[this.encodeAsString(e)]:this.encodeAsBinary({type:e.type===Be.EVENT?Be.BINARY_EVENT:Be.BINARY_ACK,nsp:e.nsp,data:e.data,id:e.id})}},{key:"encodeAsString",value:function(e){var t=""+e.type;return e.type!==Be.BINARY_EVENT&&e.type!==Be.BINARY_ACK||(t+=e.attachments+"-"),e.nsp&&"/"!==e.nsp&&(t+=e.nsp+","),null!=e.id&&(t+=e.id),null!=e.data&&(t+=JSON.stringify(e.data,this.replacer)),t}},{key:"encodeAsBinary",value:function(e){var t=Oe(e),n=this.encodeAsString(t.packet),r=t.buffers;return r.unshift(n),r}}]),e}();function Le(e){return"[object Object]"===Object.prototype.toString.call(e)}var xe=function(e){o(i,e);var n=l(i);function i(e){var r;return t(this,i),(r=n.call(this)).reviver=e,r}return r(i,[{key:"add",value:function(e){var t;if("string"==typeof e){if(this.reconstructor)throw new Error("got plaintext data when reconstructing a packet");var n=(t=this.decodeString(e)).type===Be.BINARY_EVENT;n||t.type===Be.BINARY_ACK?(t.type=n?Be.EVENT:Be.ACK,this.reconstructor=new Pe(t),0===t.attachments&&p(s(i.prototype),"emitReserved",this).call(this,"decoded",t)):p(s(i.prototype),"emitReserved",this).call(this,"decoded",t)}else{if(!Ee(e)&&!e.base64)throw new Error("Unknown type: "+e);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");(t=this.reconstructor.takeBinaryData(e))&&(this.reconstructor=null,p(s(i.prototype),"emitReserved",this).call(this,"decoded",t))}}},{key:"decodeString",value:function(e){var t=0,n={type:Number(e.charAt(0))};if(void 0===Be[n.type])throw new Error("unknown packet type "+n.type);if(n.type===Be.BINARY_EVENT||n.type===Be.BINARY_ACK){for(var r=t+1;"-"!==e.charAt(++t)&&t!=e.length;);var o=e.substring(r,t);if(o!=Number(o)||"-"!==e.charAt(t))throw new Error("Illegal attachments");n.attachments=Number(o)}if("/"===e.charAt(t+1)){for(var s=t+1;++t;){if(","===e.charAt(t))break;if(t===e.length)break}n.nsp=e.substring(s,t)}else n.nsp="/";var a=e.charAt(t+1);if(""!==a&&Number(a)==a){for(var c=t+1;++t;){var u=e.charAt(t);if(null==u||Number(u)!=u){--t;break}if(t===e.length)break}n.id=Number(e.substring(c,t+1))}if(e.charAt(++t)){var h=this.tryParse(e.substr(t));if(!i.isPayloadValid(n.type,h))throw new Error("invalid payload");n.data=h}return n}},{key:"tryParse",value:function(e){try{return JSON.parse(e,this.reviver)}catch(e){return!1}}},{key:"destroy",value:function(){this.reconstructor&&(this.reconstructor.finishedReconstruction(),this.reconstructor=null)}}],[{key:"isPayloadValid",value:function(e,t){switch(e){case Be.CONNECT:return Le(t);case Be.DISCONNECT:return void 0===t;case Be.CONNECT_ERROR:return"string"==typeof t||Le(t);case Be.EVENT:case Be.BINARY_EVENT:return Array.isArray(t)&&("number"==typeof t[0]||"string"==typeof t[0]&&-1===Se.indexOf(t[0]));case Be.ACK:case Be.BINARY_ACK:return Array.isArray(t)}}}]),i}(U),Pe=function(){function e(n){t(this,e),this.packet=n,this.buffers=[],this.reconPack=n}return r(e,[{key:"takeBinaryData",value:function(e){if(this.buffers.push(e),this.buffers.length===this.reconPack.attachments){var t=Re(this.reconPack,this.buffers);return this.finishedReconstruction(),t}return null}},{key:"finishedReconstruction",value:function(){this.reconPack=null,this.buffers=[]}}]),e}(),je=Object.freeze({__proto__:null,protocol:5,get PacketType(){return Be},Encoder:Ne,Decoder:xe});function qe(e,t,n){return e.on(t,n),function(){e.off(t,n)}}var De=Object.freeze({connect:1,connect_error:1,disconnect:1,disconnecting:1,newListener:1,removeListener:1}),Ue=function(e){o(a,e);var n=l(a);function a(e,r,o){var s;return t(this,a),(s=n.call(this)).connected=!1,s.recovered=!1,s.receiveBuffer=[],s.sendBuffer=[],s._queue=[],s._queueSeq=0,s.ids=0,s.acks={},s.flags={},s.io=e,s.nsp=r,o&&o.auth&&(s.auth=o.auth),s._opts=i({},o),s.io._autoConnect&&s.open(),s}return r(a,[{key:"disconnected",get:function(){return!this.connected}},{key:"subEvents",value:function(){if(!this.subs){var e=this.io;this.subs=[qe(e,"open",this.onopen.bind(this)),qe(e,"packet",this.onpacket.bind(this)),qe(e,"error",this.onerror.bind(this)),qe(e,"close",this.onclose.bind(this))]}}},{key:"active",get:function(){return!!this.subs}},{key:"connect",value:function(){return this.connected||(this.subEvents(),this.io._reconnecting||this.io.open(),"open"===this.io._readyState&&this.onopen()),this}},{key:"open",value:function(){return this.connect()}},{key:"send",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return t.unshift("message"),this.emit.apply(this,t),this}},{key:"emit",value:function(e){if(De.hasOwnProperty(e))throw new Error('"'+e.toString()+'" is a reserved event name');for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];if(n.unshift(e),this._opts.retries&&!this.flags.fromQueue&&!this.flags.volatile)return this._addToQueue(n),this;var i={type:Be.EVENT,data:n,options:{}};if(i.options.compress=!1!==this.flags.compress,"function"==typeof n[n.length-1]){var o=this.ids++,s=n.pop();this._registerAckCallback(o,s),i.id=o}var a=this.io.engine&&this.io.engine.transport&&this.io.engine.transport.writable;return this.flags.volatile&&(!a||!this.connected)||(this.connected?(this.notifyOutgoingListeners(i),this.packet(i)):this.sendBuffer.push(i)),this.flags={},this}},{key:"_registerAckCallback",value:function(e,t){var n,r=this,i=null!==(n=this.flags.timeout)&&void 0!==n?n:this._opts.ackTimeout;if(void 0!==i){var o=this.io.setTimeoutFn((function(){delete r.acks[e];for(var n=0;n<r.sendBuffer.length;n++)r.sendBuffer[n].id===e&&r.sendBuffer.splice(n,1);t.call(r,new Error("operation has timed out"))}),i),s=function(){r.io.clearTimeoutFn(o);for(var e=arguments.length,n=new Array(e),i=0;i<e;i++)n[i]=arguments[i];t.apply(r,n)};s.withError=!0,this.acks[e]=s}else this.acks[e]=t}},{key:"emitWithAck",value:function(e){for(var t=this,n=arguments.length,r=new Array(n>1?n-1:0),i=1;i<n;i++)r[i-1]=arguments[i];return new Promise((function(n,i){var o=function(e,t){return e?i(e):n(t)};o.withError=!0,r.push(o),t.emit.apply(t,[e].concat(r))}))}},{key:"_addToQueue",value:function(e){var t,n=this;"function"==typeof e[e.length-1]&&(t=e.pop());var r={id:this._queueSeq++,tryCount:0,pending:!1,args:e,flags:i({fromQueue:!0},this.flags)};e.push((function(e){if(r===n._queue[0]){if(null!==e)r.tryCount>n._opts.retries&&(n._queue.shift(),t&&t(e));else if(n._queue.shift(),t){for(var i=arguments.length,o=new Array(i>1?i-1:0),s=1;s<i;s++)o[s-1]=arguments[s];t.apply(void 0,[null].concat(o))}return r.pending=!1,n._drainQueue()}})),this._queue.push(r),this._drainQueue()}},{key:"_drainQueue",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this._queue.length){var t=this._queue[0];t.pending&&!e||(t.pending=!0,t.tryCount++,this.flags=t.flags,this.emit.apply(this,t.args))}}},{key:"packet",value:function(e){e.nsp=this.nsp,this.io._packet(e)}},{key:"onopen",value:function(){var e=this;"function"==typeof this.auth?this.auth((function(t){e._sendConnectPacket(t)})):this._sendConnectPacket(this.auth)}},{key:"_sendConnectPacket",value:function(e){this.packet({type:Be.CONNECT,data:this._pid?i({pid:this._pid,offset:this._lastOffset},e):e})}},{key:"onerror",value:function(e){this.connected||this.emitReserved("connect_error",e)}},{key:"onclose",value:function(e,t){this.connected=!1,delete this.id,this.emitReserved("disconnect",e,t),this._clearAcks()}},{key:"_clearAcks",value:function(){var e=this;Object.keys(this.acks).forEach((function(t){if(!e.sendBuffer.some((function(e){return String(e.id)===t}))){var n=e.acks[t];delete e.acks[t],n.withError&&n.call(e,new Error("socket has been disconnected"))}}))}},{key:"onpacket",value:function(e){if(e.nsp===this.nsp)switch(e.type){case Be.CONNECT:e.data&&e.data.sid?this.onconnect(e.data.sid,e.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Be.EVENT:case Be.BINARY_EVENT:this.onevent(e);break;case Be.ACK:case Be.BINARY_ACK:this.onack(e);break;case Be.DISCONNECT:this.ondisconnect();break;case Be.CONNECT_ERROR:this.destroy();var t=new Error(e.data.message);t.data=e.data.data,this.emitReserved("connect_error",t)}}},{key:"onevent",value:function(e){var t=e.data||[];null!=e.id&&t.push(this.ack(e.id)),this.connected?this.emitEvent(t):this.receiveBuffer.push(Object.freeze(t))}},{key:"emitEvent",value:function(e){if(this._anyListeners&&this._anyListeners.length){var t,n=y(this._anyListeners.slice());try{for(n.s();!(t=n.n()).done;){t.value.apply(this,e)}}catch(e){n.e(e)}finally{n.f()}}p(s(a.prototype),"emit",this).apply(this,e),this._pid&&e.length&&"string"==typeof e[e.length-1]&&(this._lastOffset=e[e.length-1])}},{key:"ack",value:function(e){var t=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,i=new Array(r),o=0;o<r;o++)i[o]=arguments[o];t.packet({type:Be.ACK,id:e,data:i})}}}},{key:"onack",value:function(e){var t=this.acks[e.id];"function"==typeof t&&(delete this.acks[e.id],t.withError&&e.data.unshift(null),t.apply(this,e.data))}},{key:"onconnect",value:function(e,t){this.id=e,this.recovered=t&&this._pid===t,this._pid=t,this.connected=!0,this.emitBuffered(),this.emitReserved("connect"),this._drainQueue(!0)}},{key:"emitBuffered",value:function(){var e=this;this.receiveBuffer.forEach((function(t){return e.emitEvent(t)})),this.receiveBuffer=[],this.sendBuffer.forEach((function(t){e.notifyOutgoingListeners(t),e.packet(t)})),this.sendBuffer=[]}},{key:"ondisconnect",value:function(){this.destroy(),this.onclose("io server disconnect")}},{key:"destroy",value:function(){this.subs&&(this.subs.forEach((function(e){return e()})),this.subs=void 0),this.io._destroy(this)}},{key:"disconnect",value:function(){return this.connected&&this.packet({type:Be.DISCONNECT}),this.destroy(),this.connected&&this.onclose("io client disconnect"),this}},{key:"close",value:function(){return this.disconnect()}},{key:"compress",value:function(e){return this.flags.compress=e,this}},{key:"volatile",get:function(){return this.flags.volatile=!0,this}},{key:"timeout",value:function(e){return this.flags.timeout=e,this}},{key:"onAny",value:function(e){return this._anyListeners=this._anyListeners||[],this._anyListeners.push(e),this}},{key:"prependAny",value:function(e){return this._anyListeners=this._anyListeners||[],this._anyListeners.unshift(e),this}},{key:"offAny",value:function(e){if(!this._anyListeners)return this;if(e){for(var t=this._anyListeners,n=0;n<t.length;n++)if(e===t[n])return t.splice(n,1),this}else this._anyListeners=[];return this}},{key:"listenersAny",value:function(){return this._anyListeners||[]}},{key:"onAnyOutgoing",value:function(e){return this._anyOutgoingListeners=this._anyOutgoingListeners||[],this._anyOutgoingListeners.push(e),this}},{key:"prependAnyOutgoing",value:function(e){return this._anyOutgoingListeners=this._anyOutgoingListeners||[],this._anyOutgoingListeners.unshift(e),this}},{key:"offAnyOutgoing",value:function(e){if(!this._anyOutgoingListeners)return this;if(e){for(var t=this._anyOutgoingListeners,n=0;n<t.length;n++)if(e===t[n])return t.splice(n,1),this}else this._anyOutgoingListeners=[];return this}},{key:"listenersAnyOutgoing",value:function(){return this._anyOutgoingListeners||[]}},{key:"notifyOutgoingListeners",value:function(e){if(this._anyOutgoingListeners&&this._anyOutgoingListeners.length){var t,n=y(this._anyOutgoingListeners.slice());try{for(n.s();!(t=n.n()).done;){t.value.apply(this,e.data)}}catch(e){n.e(e)}finally{n.f()}}}}]),a}(U);function Ie(e){e=e||{},this.ms=e.min||100,this.max=e.max||1e4,this.factor=e.factor||2,this.jitter=e.jitter>0&&e.jitter<=1?e.jitter:0,this.attempts=0}Ie.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=0==(1&Math.floor(10*t))?e-n:e+n}return 0|Math.min(e,this.max)},Ie.prototype.reset=function(){this.attempts=0},Ie.prototype.setMin=function(e){this.ms=e},Ie.prototype.setMax=function(e){this.max=e},Ie.prototype.setJitter=function(e){this.jitter=e};var Fe=function(n){o(s,n);var i=l(s);function s(n,r){var o,a;t(this,s),(o=i.call(this)).nsps={},o.subs=[],n&&"object"===e(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",o.opts=r,H(f(o),r),o.reconnection(!1!==r.reconnection),o.reconnectionAttempts(r.reconnectionAttempts||1/0),o.reconnectionDelay(r.reconnectionDelay||1e3),o.reconnectionDelayMax(r.reconnectionDelayMax||5e3),o.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),o.backoff=new Ie({min:o.reconnectionDelay(),max:o.reconnectionDelayMax(),jitter:o.randomizationFactor()}),o.timeout(null==r.timeout?2e4:r.timeout),o._readyState="closed",o.uri=n;var c=r.parser||je;return o.encoder=new c.Encoder,o.decoder=new c.Decoder,o._autoConnect=!1!==r.autoConnect,o._autoConnect&&o.open(),o}return r(s,[{key:"reconnection",value:function(e){return arguments.length?(this._reconnection=!!e,this):this._reconnection}},{key:"reconnectionAttempts",value:function(e){return void 0===e?this._reconnectionAttempts:(this._reconnectionAttempts=e,this)}},{key:"reconnectionDelay",value:function(e){var t;return void 0===e?this._reconnectionDelay:(this._reconnectionDelay=e,null===(t=this.backoff)||void 0===t||t.setMin(e),this)}},{key:"randomizationFactor",value:function(e){var t;return void 0===e?this._randomizationFactor:(this._randomizationFactor=e,null===(t=this.backoff)||void 0===t||t.setJitter(e),this)}},{key:"reconnectionDelayMax",value:function(e){var t;return void 0===e?this._reconnectionDelayMax:(this._reconnectionDelayMax=e,null===(t=this.backoff)||void 0===t||t.setMax(e),this)}},{key:"timeout",value:function(e){return arguments.length?(this._timeout=e,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(e){var t=this;if(~this._readyState.indexOf("open"))return this;this.engine=new ge(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var i=qe(n,"open",(function(){r.onopen(),e&&e()})),o=function(n){t.cleanup(),t._readyState="closed",t.emitReserved("error",n),e?e(n):t.maybeReconnectOnOpen()},s=qe(n,"error",o);if(!1!==this._timeout){var a=this._timeout,c=this.setTimeoutFn((function(){i(),o(new Error("timeout")),n.close()}),a);this.opts.autoUnref&&c.unref(),this.subs.push((function(){t.clearTimeoutFn(c)}))}return this.subs.push(i),this.subs.push(s),this}},{key:"connect",value:function(e){return this.open(e)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var e=this.engine;this.subs.push(qe(e,"ping",this.onping.bind(this)),qe(e,"data",this.ondata.bind(this)),qe(e,"error",this.onerror.bind(this)),qe(e,"close",this.onclose.bind(this)),qe(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(e){try{this.decoder.add(e)}catch(e){this.onclose("parse error",e)}}},{key:"ondecoded",value:function(e){var t=this;ce((function(){t.emitReserved("packet",e)}),this.setTimeoutFn)}},{key:"onerror",value:function(e){this.emitReserved("error",e)}},{key:"socket",value:function(e,t){var n=this.nsps[e];return n?this._autoConnect&&!n.active&&n.connect():(n=new Ue(this,e,t),this.nsps[e]=n),n}},{key:"_destroy",value:function(e){for(var t=0,n=Object.keys(this.nsps);t<n.length;t++){var r=n[t];if(this.nsps[r].active)return}this._close()}},{key:"_packet",value:function(e){for(var t=this.encoder.encode(e),n=0;n<t.length;n++)this.engine.write(t[n],e.options)}},{key:"cleanup",value:function(){this.subs.forEach((function(e){return e()})),this.subs.length=0,this.decoder.destroy()}},{key:"_close",value:function(){this.skipReconnect=!0,this._reconnecting=!1,this.onclose("forced close"),this.engine&&this.engine.close()}},{key:"disconnect",value:function(){return this._close()}},{key:"onclose",value:function(e,t){this.cleanup(),this.backoff.reset(),this._readyState="closed",this.emitReserved("close",e,t),this._reconnection&&!this.skipReconnect&&this.reconnect()}},{key:"reconnect",value:function(){var e=this;if(this._reconnecting||this.skipReconnect)return this;var t=this;if(this.backoff.attempts>=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){t.skipReconnect||(e.emitReserved("reconnect_attempt",t.backoff.attempts),t.skipReconnect||t.open((function(n){n?(t._reconnecting=!1,t.reconnect(),e.emitReserved("reconnect_error",n)):t.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){e.clearTimeoutFn(r)}))}}},{key:"onreconnect",value:function(){var e=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",e)}}]),s}(U),Me={};function Ve(t,n){"object"===e(t)&&(n=t,t=void 0);var r,i=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=e;n=n||"undefined"!=typeof location&&location,null==e&&(e=n.protocol+"//"+n.host),"string"==typeof e&&("/"===e.charAt(0)&&(e="/"===e.charAt(1)?n.protocol+e:n.host+e),/^(https?|wss?):\/\//.test(e)||(e=void 0!==n?n.protocol+"//"+e:"https://"+e),r=ve(e)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var i=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+t,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),o=i.source,s=i.id,a=i.path,c=Me[s]&&a in Me[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||c?r=new Fe(o,n):(Me[s]||(Me[s]=new Fe(o,n)),r=Me[s]),i.query&&!n.query&&(n.query=i.queryKey),r.socket(i.path,n)}return i(Ve,{Manager:Fe,Socket:Ue,io:Ve,connect:Ve}),Ve}));
+//# sourceMappingURL=socket.io.min.js.map
-- 
GitLab