Quellcode durchsuchen

Protobuf serial protocol (#101)

- Firmware
  - Refactor all code to use a log interface rather than `Serial` directly
  - Moved platformio.ini to root, so you can load the entire repo in VS Code and still use platformio
  - Reduced graphic buffer bit depth to 8 bits (short on RAM :( )
  - Implemented threadsafe log interface in interface_task (using a queue for posting log message)
  - Created serial protocol interface and 2 implementations - plaintext (default) and protobuf (selected by sending a NULL byte)
  - Protobuf protocol is roughly the same architecture as Splitflap's:
    - PacketSerial (cobs) framing with NULL delimiters
    - CRC32 checksums for packets
    - nanopb generated code for encoding/decoding (generated firmware code is checked in, since it should change less frequently and this reduces burden to build the project from scratch)
- Software
  - Typescript example host-side code:
    - smartknobjs-proto
      - Autogenerated types/encoding/decoding protobuf code (using protobufjs)
    - smartknobjs
      - Helper library for interfacing the with smartknob via serial/protobuf. Implements basic outgoing queue (with retries and ACK checking) and message callback for responding to messages from the SmartKnob
    - example
      - Basic demo CLI app that uses smartknobjs to connect to the smartknob, send a haptic config, and print state changes and log messages to the console
Scott Bezek vor 3 Jahren
Ursprung
Commit
ae28d523e0
52 geänderte Dateien mit 6300 neuen und 339 gelöschten Zeilen
  1. 1 1
      .github/workflows/pio.yml
  2. 3 0
      .gitmodules
  3. 12 7
      .vscode/extensions.json
  4. 12 0
      .vscode/settings.json
  5. 5 0
      firmware/.vscode/settings.json
  6. 19 71
      firmware/src/display_task.cpp
  7. 6 4
      firmware/src/display_task.h
  8. 143 130
      firmware/src/interface_task.cpp
  9. 15 3
      firmware/src/interface_task.h
  10. 0 19
      firmware/src/knob_data.h
  11. 3 2
      firmware/src/logger.h
  12. 25 37
      firmware/src/main.cpp
  13. 74 56
      firmware/src/motor_task.cpp
  14. 13 5
      firmware/src/motor_task.h
  15. 11 1
      firmware/src/mt6701_sensor.cpp
  16. 10 0
      firmware/src/mt6701_sensor.h
  17. 30 0
      firmware/src/proto_gen/smartknob.pb.c
  18. 187 0
      firmware/src/proto_gen/smartknob.pb.h
  19. 20 0
      firmware/src/proto_helpers.h
  20. 21 0
      firmware/src/serial/crc32.cpp
  21. 11 0
      firmware/src/serial/crc32.h
  22. 28 0
      firmware/src/serial/serial_protocol.h
  23. 44 0
      firmware/src/serial/serial_protocol_plaintext.cpp
  24. 26 0
      firmware/src/serial/serial_protocol_plaintext.h
  25. 152 0
      firmware/src/serial/serial_protocol_protobuf.cpp
  26. 41 0
      firmware/src/serial/serial_protocol_protobuf.h
  27. 64 0
      firmware/src/serial/uart_stream.cpp
  28. 47 0
      firmware/src/serial/uart_stream.h
  29. 7 1
      firmware/src/tlv_sensor.cpp
  30. 4 0
      firmware/src/tlv_sensor.h
  31. 13 2
      platformio.ini
  32. 34 0
      proto/generate_protobuf.py
  33. 55 0
      proto/smartknob.proto
  34. 2 0
      software/js/.gitignore
  35. 1 0
      software/js/.npmrc
  36. 21 0
      software/js/README.md
  37. 4315 0
      software/js/package-lock.json
  38. 24 0
      software/js/package.json
  39. 19 0
      software/js/packages/example/.eslintrc
  40. 10 0
      software/js/packages/example/.prettierrc
  41. 28 0
      software/js/packages/example/package.json
  42. 60 0
      software/js/packages/example/src/index.ts
  43. 105 0
      software/js/packages/example/tsconfig.json
  44. 201 0
      software/js/packages/smartknobjs-proto/package-lock.json
  45. 15 0
      software/js/packages/smartknobjs-proto/package.json
  46. 19 0
      software/js/packages/smartknobjs/.eslintrc
  47. 10 0
      software/js/packages/smartknobjs/.prettierrc
  48. 29 0
      software/js/packages/smartknobjs/package.json
  49. 195 0
      software/js/packages/smartknobjs/src/index.ts
  50. 4 0
      software/js/packages/smartknobjs/src/typings/cobs.d.ts
  51. 105 0
      software/js/packages/smartknobjs/tsconfig.json
  52. 1 0
      thirdparty/nanopb

+ 1 - 1
.github/workflows/pio.yml

@@ -39,5 +39,5 @@ jobs:
       # Run regardless of other build step failures, as long as setup steps completed
       if: always() && steps.pio_install.outcome == 'success'
       run: |
-        pio run -d ./firmware \
+        pio run \
           -e view \

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "thirdparty/nanopb"]
+	path = thirdparty/nanopb
+	url = git@github.com:nanopb/nanopb.git

+ 12 - 7
.vscode/extensions.json

@@ -1,7 +1,12 @@
-{
-    // See http://go.microsoft.com/fwlink/?LinkId=827846
-    // for the documentation about the extensions.json format
-    "recommendations": [
-        "platformio.platformio-ide"
-    ]
-}
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "dbaeumer.vscode-eslint",
+        "platformio.platformio-ide",
+        "rvest.vs-code-prettier-eslint"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}

+ 12 - 0
.vscode/settings.json

@@ -0,0 +1,12 @@
+{
+    "eslint.workingDirectories": [
+        "./software/js/packages/example",
+        "./software/js/packages/smartknobjs",
+        "./software/js"
+    ],
+    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
+    "editor.formatOnPaste": false,
+    "editor.formatOnType": false,
+    "editor.formatOnSave": true,
+    "editor.formatOnSaveMode": "file",
+}

+ 5 - 0
firmware/.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+    "files.associations": {
+        "functional": "cpp"
+    }
+}

+ 19 - 71
firmware/src/display_task.cpp

@@ -6,8 +6,8 @@
 
 static const uint8_t LEDC_CHANNEL_LCD_BACKLIGHT = 0;
 
-DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 4048, 1, task_core} {
-  knob_state_queue_ = xQueueCreate(1, sizeof(KnobState));
+DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 2048, 1, task_core} {
+  knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState));
   assert(knob_state_queue_ != NULL);
 
   mutex_ = xSemaphoreCreateMutex();
@@ -19,63 +19,6 @@ DisplayTask::~DisplayTask() {
   vSemaphoreDelete(mutex_);
 }
 
-static void HSV_to_RGB(float h, float s, float v, uint8_t *r, uint8_t *g, uint8_t *b)
-{
-  int i;
-  float f,p,q,t;
-  
-  h = fmax(0.0, fmin(360.0, h));
-  s = fmax(0.0, fmin(100.0, s));
-  v = fmax(0.0, fmin(100.0, v));
-  
-  s /= 100;
-  v /= 100;
-  
-  if(s == 0) {
-    // Achromatic (grey)
-    *r = *g = *b = round(v*255);
-    return;
-  }
-
-  h /= 60; // sector 0 to 5
-  i = floor(h);
-  f = h - i; // factorial part of h
-  p = v * (1 - s);
-  q = v * (1 - s * f);
-  t = v * (1 - s * (1 - f));
-  switch(i) {
-    case 0:
-      *r = round(255*v);
-      *g = round(255*t);
-      *b = round(255*p);
-      break;
-    case 1:
-      *r = round(255*q);
-      *g = round(255*v);
-      *b = round(255*p);
-      break;
-    case 2:
-      *r = round(255*p);
-      *g = round(255*v);
-      *b = round(255*t);
-      break;
-    case 3:
-      *r = round(255*p);
-      *g = round(255*q);
-      *b = round(255*v);
-      break;
-    case 4:
-      *r = round(255*t);
-      *g = round(255*p);
-      *b = round(255*v);
-      break;
-    default: // case 5:
-      *r = round(255*v);
-      *g = round(255*p);
-      *b = round(255*q);
-    }
-}
-
 void DisplayTask::run() {
     tft_.begin();
     tft_.invertDisplay(1);
@@ -86,28 +29,23 @@ void DisplayTask::run() {
     ledcAttachPin(PIN_LCD_BACKLIGHT, LEDC_CHANNEL_LCD_BACKLIGHT);
     ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, UINT16_MAX);
 
-    spr_.setColorDepth(16);
+    spr_.setColorDepth(8);
 
     if (spr_.createSprite(TFT_WIDTH, TFT_HEIGHT) == nullptr) {
-      Serial.println("ERROR: sprite allocation failed!");
+      log("ERROR: sprite allocation failed!");
       tft_.fillScreen(TFT_RED);
     } else {
-      Serial.println("Sprite created!");
+      log("Sprite created!");
       tft_.fillScreen(TFT_PURPLE);
     }
     spr_.setTextColor(0xFFFF, TFT_BLACK);
     
-    KnobState state;
+    PB_SmartKnobState state;
 
     const int RADIUS = TFT_WIDTH / 2;
     const uint16_t FILL_COLOR = spr_.color565(90, 18, 151);
     const uint16_t DOT_COLOR = spr_.color565(80, 100, 200);
 
-    int32_t pointer_center_x = TFT_WIDTH / 2;
-    int32_t pointer_center_y = TFT_HEIGHT / 2;
-    int32_t pointer_length_short = 10;
-    int32_t pointer_length_long = TFT_WIDTH / 2 - 5;
-
     spr_.setTextDatum(CC_DATUM);
     spr_.setTextColor(TFT_WHITE);
     while(1) {
@@ -125,15 +63,15 @@ void DisplayTask::run() {
         spr_.drawString(String() + state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2 - VALUE_OFFSET, 1);
         spr_.setFreeFont(&DESCRIPTION_FONT);
         int32_t line_y = TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET;
-        char* start = state.config.descriptor;
-        char* end = start + strlen(state.config.descriptor);
+        char* start = state.config.text;
+        char* end = start + strlen(state.config.text);
         while (start < end) {
           char* newline = strchr(start, '\n');
           if (newline == nullptr) {
             newline = end;
           }
           
-          char buf[sizeof(state.config.descriptor)] = {};
+          char buf[sizeof(state.config.text)] = {};
           strncat(buf, start, min(sizeof(buf) - 1, (size_t)(newline - start)));
           spr_.drawString(String(buf), TFT_WIDTH / 2, line_y, 1);
           start = newline + 1;
@@ -202,4 +140,14 @@ void DisplayTask::setBrightness(uint16_t brightness) {
   brightness_ = brightness;
 }
 
+void DisplayTask::setLogger(Logger* logger) {
+    logger_ = logger;
+}
+
+void DisplayTask::log(const char* msg) {
+    if (logger_ != nullptr) {
+        logger_->log(msg);
+    }
+}
+
 #endif

+ 6 - 4
firmware/src/display_task.h

@@ -5,7 +5,8 @@
 #include <Arduino.h>
 #include <TFT_eSPI.h>
 
-#include "knob_data.h"
+#include "logger.h"
+#include "proto_gen/smartknob.pb.h"
 #include "task.h"
 
 class DisplayTask : public Task<DisplayTask> {
@@ -18,6 +19,7 @@ class DisplayTask : public Task<DisplayTask> {
         QueueHandle_t getKnobStateQueue();
 
         void setBrightness(uint16_t brightness);
+        void setLogger(Logger* logger);
 
     protected:
         void run();
@@ -30,11 +32,11 @@ class DisplayTask : public Task<DisplayTask> {
 
         QueueHandle_t knob_state_queue_;
 
-        KnobState state_;
-
+        PB_SmartKnobState state_;
         SemaphoreHandle_t mutex_;
-
         uint16_t brightness_;
+        Logger* logger_;
+        void log(const char* msg);
 };
 
 #else

+ 143 - 130
firmware/src/interface_task.cpp

@@ -1,5 +1,3 @@
-#include <AceButton.h>
-
 #if SK_LEDS
 #include <FastLED.h>
 #endif
@@ -15,8 +13,6 @@
 #include "interface_task.h"
 #include "util.h"
 
-using namespace ace_button;
-
 #define COUNT_OF(A) (sizeof(A) / sizeof(A[0]))
 
 #if SK_LEDS
@@ -31,14 +27,14 @@ HX711 scale;
 Adafruit_VEML7700 veml = Adafruit_VEML7700();
 #endif
 
-static KnobConfig configs[] = {
+static PB_SmartKnobConfig configs[] = {
     // int32_t num_positions;
     // int32_t position;
     // float position_width_radians;
     // float detent_strength_unit;
     // float endstop_strength_unit;
     // float snap_point;
-    // char descriptor[50];
+    // char text[51];
 
     {
         0,
@@ -123,32 +119,26 @@ static KnobConfig configs[] = {
     },
 };
 
-InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task) : Task("Interface", 4048, 1, task_core), motor_task_(motor_task), display_task_(display_task) {
+InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task) : 
+        Task("Interface", 3000, 1, task_core),
+        stream_(),
+        motor_task_(motor_task),
+        display_task_(display_task),
+        plaintext_protocol_(stream_, motor_task_),
+        proto_protocol_(stream_, motor_task_) {
     #if SK_DISPLAY
         assert(display_task != nullptr);
     #endif
-}
 
-InterfaceTask::~InterfaceTask() {}
+    log_queue_ = xQueueCreate(10, sizeof(std::string *));
+    assert(log_queue_ != NULL);
+
+    knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState));
+    assert(knob_state_queue_ != NULL);
+}
 
 void InterfaceTask::run() {
-    #if PIN_BUTTON_NEXT >= 34
-        pinMode(PIN_BUTTON_NEXT, INPUT);
-    #else
-        pinMode(PIN_BUTTON_NEXT, INPUT_PULLUP);
-    #endif
-    AceButton button_next((uint8_t) PIN_BUTTON_NEXT);
-    button_next.getButtonConfig()->setIEventHandler(this);
-
-    #if PIN_BUTTON_PREV > -1
-        #if PIN_BUTTON_PREV >= 34
-            pinMode(PIN_BUTTON_PREV, INPUT);
-        #else
-            pinMode(PIN_BUTTON_PREV, INPUT_PULLUP);
-        #endif
-        AceButton button_prev((uint8_t) PIN_BUTTON_PREV);
-        button_prev.getButtonConfig()->setIEventHandler(this);
-    #endif
+    stream_.begin();
     
     #if SK_LEDS
         FastLED.addLeds<SK6812, PIN_LED_DATA, GRB>(leds, NUM_LEDS);
@@ -167,121 +157,64 @@ void InterfaceTask::run() {
             veml.setGain(VEML7700_GAIN_2);
             veml.setIntegrationTime(VEML7700_IT_400MS);
         } else {
-            Serial.println("ALS sensor not found!");
+            log("ALS sensor not found!");
         }
     #endif
 
     motor_task_.setConfig(configs[0]);
+    motor_task_.addListener(knob_state_queue_);
+
+
+    // Start in legacy protocol mode
+    plaintext_protocol_.init([this] () {
+        changeConfig(true);
+    });
+    SerialProtocol* current_protocol = &plaintext_protocol_;
+
+    ProtocolChangeCallback protocol_change_callback = [this, &current_protocol] (uint8_t protocol) {
+        switch (protocol) {
+            case SERIAL_PROTOCOL_LEGACY:
+                current_protocol = &plaintext_protocol_;
+                break;
+            case SERIAL_PROTOCOL_PROTO:
+                current_protocol = &proto_protocol_;
+                break;
+            default:
+                log("Unknown protocol requested");
+                break;
+        }
+    };
 
-    // How far button is pressed, in range [0, 1]
-    float press_value_unit = 0;
+    plaintext_protocol_.setProtocolChangeCallback(protocol_change_callback);
+    proto_protocol_.setProtocolChangeCallback(protocol_change_callback);
 
     // Interface loop:
     while (1) {
-        button_next.check();
-        #if PIN_BUTTON_PREV > -1
-            button_prev.check();
-        #endif
-        if (Serial.available()) {
-            int v = Serial.read();
-            if (v == ' ') {
-                changeConfig(true);
-            }
+        PB_SmartKnobState state;
+        if (xQueueReceive(knob_state_queue_, &state, 0) == pdTRUE) {
+            current_protocol->handleState(state);
         }
 
-        #if SK_ALS
-            const float LUX_ALPHA = 0.005;
-            static float lux_avg;
-            float lux = veml.readLux();
-            lux_avg = lux * LUX_ALPHA + lux_avg * (1 - LUX_ALPHA);
-            static uint32_t last_als;
-            if (millis() - last_als > 1000) {
-                Serial.print("millilux: "); Serial.println(lux*1000);
-                last_als = millis();
-            }
-        #endif
+        current_protocol->loop();
 
-        #if SK_STRAIN
-            // TODO: calibrate and track (long term moving average) zero point (lower); allow calibration of set point offset
-            const int32_t lower = 950000;
-            const int32_t upper = 1800000;
-            if (scale.wait_ready_timeout(100)) {
-                int32_t reading = scale.read();
-
-                // Ignore readings that are way out of expected bounds
-                if (reading >= lower - (upper - lower) && reading < upper + (upper - lower)*2) {
-                    static uint32_t last_reading_display;
-                    if (millis() - last_reading_display > 1000) {
-                        Serial.print("HX711 reading: ");
-                        Serial.println(reading);
-                        last_reading_display = millis();
-                    }
-                    long value = CLAMP(reading, lower, upper);
-                    press_value_unit = 1. * (value - lower) / (upper - lower);
-
-                    static bool pressed;
-                    if (!pressed && press_value_unit > 0.75) {
-                        motor_task_.playHaptic(true);
-                        pressed = true;
-                        changeConfig(true);
-                    } else if (pressed && press_value_unit < 0.25) {
-                        motor_task_.playHaptic(false);
-                        pressed = false;
-                    }
-                }
-            } else {
-                Serial.println("HX711 not found.");
-
-                #if SK_LEDS
-                    for (uint8_t i = 0; i < NUM_LEDS; i++) {
-                        leds[i] = CRGB::Red;
-                    }
-                    FastLED.show();
-                #endif
-            }
-        #endif
-
-        uint16_t brightness = UINT16_MAX;
-        // TODO: brightness scale factor should be configurable (depends on reflectivity of surface)
-        #if SK_ALS
-            brightness = (uint16_t)CLAMP(lux_avg * 13000, (float)1280, (float)UINT16_MAX);
-        #endif
-
-        #if SK_DISPLAY
-            display_task_->setBrightness(brightness); // TODO: apply gamma correction
-        #endif
-
-        #if SK_LEDS
-            for (uint8_t i = 0; i < NUM_LEDS; i++) {
-                leds[i].setHSV(200 * press_value_unit, 255, brightness >> 8);
-
-                // Gamma adjustment
-                leds[i].r = dim8_video(leds[i].r);
-                leds[i].g = dim8_video(leds[i].g);
-                leds[i].b = dim8_video(leds[i].b);
-            }
-            FastLED.show();
-        #endif
+        std::string* log_string;
+        while (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) {
+            current_protocol->log(log_string->c_str());
+            delete log_string;
+        }
 
-        delay(10);
+        updateHardware();
+
+        delay(1);
     }
 }
 
-void InterfaceTask::handleEvent(AceButton* button, uint8_t event_type, uint8_t button_state) {
-    switch (event_type) {
-        case AceButton::kEventPressed:
-            if (button->getPin() == PIN_BUTTON_NEXT) {
-                changeConfig(true);
-            }
-            #if PIN_BUTTON_PREV > -1
-                if (button->getPin() == PIN_BUTTON_PREV) {
-                    changeConfig(false);
-                }
-            #endif
-            break;
-        case AceButton::kEventReleased:
-            break;
-    }
+void InterfaceTask::log(const char* msg) {
+    // Allocate a string for the duration it's in the queue; it is free'd by the queue consumer
+    std::string* msg_str = new std::string(msg);
+
+    // Put string in queue (or drop if full to avoid blocking)
+    xQueueSendToBack(log_queue_, &msg_str, 0);
 }
 
 void InterfaceTask::changeConfig(bool next) {
@@ -295,9 +228,89 @@ void InterfaceTask::changeConfig(bool next) {
         }
     }
     
-    Serial.print("Changing config to ");
-    Serial.print(current_config_);
-    Serial.print(" -- ");
-    Serial.println(configs[current_config_].descriptor);
+    char buf_[256];
+    snprintf(buf_, sizeof(buf_), "Changing config to %d -- %s", current_config_, configs[current_config_].text);
+    log(buf_);
     motor_task_.setConfig(configs[current_config_]);
 }
+
+void InterfaceTask::updateHardware() {
+    // How far button is pressed, in range [0, 1]
+    float press_value_unit = 0;
+
+    #if SK_ALS
+        const float LUX_ALPHA = 0.005;
+        static float lux_avg;
+        float lux = veml.readLux();
+        lux_avg = lux * LUX_ALPHA + lux_avg * (1 - LUX_ALPHA);
+        static uint32_t last_als;
+        if (millis() - last_als > 1000) {
+            snprintf(buf_, sizeof(buf_), "millilux: %.2f", lux*1000);
+            log(buf_);
+            last_als = millis();
+        }
+    #endif
+
+    #if SK_STRAIN
+        if (scale.wait_ready_timeout(100)) {
+            int32_t reading = scale.read();
+
+            static uint32_t last_reading_display;
+            if (millis() - last_reading_display > 1000) {
+                snprintf(buf_, sizeof(buf_), "HX711 reading: %d", reading);
+                log(buf_);
+                last_reading_display = millis();
+            }
+
+            // TODO: calibrate and track (long term moving average) zero point (lower); allow calibration of set point offset
+            const int32_t lower = 950000;
+            const int32_t upper = 1800000;
+            // Ignore readings that are way out of expected bounds
+            if (reading >= lower - (upper - lower) && reading < upper + (upper - lower)*2) {
+                long value = CLAMP(reading, lower, upper);
+                press_value_unit = 1. * (value - lower) / (upper - lower);
+
+                static bool pressed;
+                if (!pressed && press_value_unit > 0.75) {
+                    motor_task_.playHaptic(true);
+                    pressed = true;
+                    changeConfig(true);
+                } else if (pressed && press_value_unit < 0.25) {
+                    motor_task_.playHaptic(false);
+                    pressed = false;
+                }
+            }
+        } else {
+            log("HX711 not found.");
+
+            #if SK_LEDS
+                for (uint8_t i = 0; i < NUM_LEDS; i++) {
+                    leds[i] = CRGB::Red;
+                }
+                FastLED.show();
+            #endif
+        }
+    #endif
+
+    uint16_t brightness = UINT16_MAX;
+    // TODO: brightness scale factor should be configurable (depends on reflectivity of surface)
+    #if SK_ALS
+        brightness = (uint16_t)CLAMP(lux_avg * 13000, (float)1280, (float)UINT16_MAX);
+    #endif
+
+    #if SK_DISPLAY
+        display_task_->setBrightness(brightness); // TODO: apply gamma correction
+    #endif
+
+    #if SK_LEDS
+        for (uint8_t i = 0; i < NUM_LEDS; i++) {
+            leds[i].setHSV(200 * press_value_unit, 255, brightness >> 8);
+
+            // Gamma adjustment
+            leds[i].r = dim8_video(leds[i].r);
+            leds[i].g = dim8_video(leds[i].g);
+            leds[i].b = dim8_video(leds[i].b);
+        }
+        FastLED.show();
+    #endif
+}

+ 15 - 3
firmware/src/interface_task.h

@@ -4,26 +4,38 @@
 #include <Arduino.h>
 
 #include "display_task.h"
+#include "logger.h"
 #include "motor_task.h"
+#include "serial/serial_protocol_plaintext.h"
+#include "serial/serial_protocol_protobuf.h"
+#include "serial/uart_stream.h"
 #include "task.h"
 
-class InterfaceTask : public Task<InterfaceTask>, public ace_button::IEventHandler {
+class InterfaceTask : public Task<InterfaceTask>, public Logger {
     friend class Task<InterfaceTask>; // Allow base Task to invoke protected run()
 
     public:
         InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task);
-        ~InterfaceTask();
+        virtual ~InterfaceTask() {};
 
-        void handleEvent(ace_button::AceButton* button, uint8_t event_type, uint8_t button_state) override;
+        void log(const char* msg) override;
 
     protected:
         void run();
 
     private:
+        UartStream stream_;
         MotorTask& motor_task_;
         DisplayTask* display_task_;
+        char buf_[64];
 
         int current_config_ = 0;
 
+        QueueHandle_t log_queue_;
+        QueueHandle_t knob_state_queue_;
+        SerialProtocolPlaintext plaintext_protocol_;
+        SerialProtocolProtobuf proto_protocol_;
+
         void changeConfig(bool next);
+        void updateHardware();
 };

+ 0 - 19
firmware/src/knob_data.h

@@ -1,19 +0,0 @@
-#pragma once
-
-#include <Arduino.h>
-
-struct KnobConfig {
-    int32_t num_positions;
-    int32_t position;
-    float position_width_radians;
-    float detent_strength_unit;
-    float endstop_strength_unit;
-    float snap_point;
-    char descriptor[50];
-};
-
-struct KnobState {
-    int32_t current_position;
-    float sub_position_unit;
-    KnobConfig config;
-};

+ 3 - 2
firmware/src/logger.h

@@ -1,7 +1,8 @@
+#pragma once
 
 class Logger {
     public:
         Logger() {};
         virtual ~Logger() {};
-    
-};
+        virtual void log(const char* msg) = 0;
+};

+ 25 - 37
firmware/src/main.cpp

@@ -1,66 +1,54 @@
 #include <Arduino.h>
-#include <SimpleFOC.h>
 
 #include "display_task.h"
 #include "interface_task.h"
 #include "motor_task.h"
 
 #if SK_DISPLAY
-static DisplayTask display_task = DisplayTask(0);
+static DisplayTask display_task(0);
 static DisplayTask* display_task_p = &display_task;
 #else
 static DisplayTask* display_task_p = nullptr;
 #endif
-static MotorTask motor_task = MotorTask(1);
+static MotorTask motor_task(1);
 
 
-InterfaceTask interface_task = InterfaceTask(0, motor_task, display_task_p);
-
-static QueueHandle_t knob_state_debug_queue;
+InterfaceTask interface_task(0, motor_task, display_task_p);
 
 void setup() {
-  Serial.begin(115200);
-
-  motor_task.begin();
-  interface_task.begin();
-
   #if SK_DISPLAY
+  display_task.setLogger(&interface_task);
   display_task.begin();
 
   // Connect display to motor_task's knob state feed
   motor_task.addListener(display_task.getKnobStateQueue());
   #endif
 
-  // Create a queue and register it with motor_task to print knob state to serial (see loop() below)
-  knob_state_debug_queue = xQueueCreate(1, sizeof(KnobState));
-  assert(knob_state_debug_queue != NULL);
-
-  motor_task.addListener(knob_state_debug_queue);
+  motor_task.setLogger(&interface_task);
+  motor_task.begin();
+  interface_task.begin();
 
   // Free up the Arduino loop task
   vTaskDelete(NULL);
 }
 
-
-static KnobState state = {};
-uint32_t last_debug;
-
 void loop() {
-  // Print any new state, at most 5 times per second
-  if (millis() - last_debug > 200 && xQueueReceive(knob_state_debug_queue, &state, portMAX_DELAY) == pdTRUE) {
-    Serial.println(state.current_position);
-    last_debug = millis();
-  }
-
-  static uint32_t last_stack_debug;
-  if (millis() - last_stack_debug > 1000) {
-    Serial.println("Stack high water:");
-    Serial.printf("main: %d\n", uxTaskGetStackHighWaterMark(NULL));
-    #if SK_DISPLAY
-      Serial.printf("display: %d\n", uxTaskGetStackHighWaterMark(display_task.getHandle()));
-    #endif
-    Serial.printf("motor: %d\n", uxTaskGetStackHighWaterMark(motor_task.getHandle()));
-    Serial.printf("interface: %d\n", uxTaskGetStackHighWaterMark(interface_task.getHandle()));
-    last_stack_debug = millis();
-  }
+  // char buf[50];
+  // static uint32_t last_stack_debug;
+  // if (millis() - last_stack_debug > 1000) {
+  //   interface_task.log("Stack high water:");
+  //   snprintf(buf, sizeof(buf), "  main: %d", uxTaskGetStackHighWaterMark(NULL));
+  //   interface_task.log(buf);
+  //   #if SK_DISPLAY
+  //     snprintf(buf, sizeof(buf), "  display: %d", uxTaskGetStackHighWaterMark(display_task.getHandle()));
+  //     interface_task.log(buf);
+  //   #endif
+  //   snprintf(buf, sizeof(buf), "  motor: %d", uxTaskGetStackHighWaterMark(motor_task.getHandle()));
+  //   interface_task.log(buf);
+  //   snprintf(buf, sizeof(buf), "  interface: %d", uxTaskGetStackHighWaterMark(interface_task.getHandle()));
+  //   interface_task.log(buf);
+  //   snprintf(buf, sizeof(buf), "Heap -- free: %d, largest: %d", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
+  //   interface_task.log(buf);
+  //   last_stack_debug = millis();
+  // }
 }

+ 74 - 56
firmware/src/motor_task.cpp

@@ -29,7 +29,7 @@ static const float IDLE_CORRECTION_MAX_ANGLE_RAD = 5 * PI / 180;
 static const float IDLE_CORRECTION_RATE_ALPHA = 0.0005;
 
 
-MotorTask::MotorTask(const uint8_t task_core) : Task("Motor", 2048, 1, task_core) {
+MotorTask::MotorTask(const uint8_t task_core) : Task("Motor", 2500, 1, task_core) {
     queue_ = xQueueCreate(5, sizeof(Command));
     assert(queue_ != NULL);
 }
@@ -43,16 +43,13 @@ MotorTask::~MotorTask() {}
     MT6701Sensor encoder = MT6701Sensor();
 #endif
 
-Commander command = Commander(Serial);
-
-
 void MotorTask::run() {
 
     driver.voltage_power_supply = 5;
     driver.init();
 
     #if SENSOR_TLV
-    encoder.init(Wire, false);
+    encoder.init(&Wire, false);
     #endif
 
     #if SENSOR_MT6701
@@ -82,29 +79,12 @@ void MotorTask::run() {
     motor.pole_pairs = MOTOR_POLE_PAIRS;
     motor.initFOC(ZERO_ELECTRICAL_OFFSET, FOC_DIRECTION);
 
-    bool calibrate = false;
-
-    Serial.println("Press Y to run calibration");
-    uint32_t t = millis();
-    while (millis() - t < 3000) {
-        if (Serial.read() == 'Y') {
-            calibrate = true;
-            break;
-        }
-        delay(10);
-    }
-    if (calibrate) {
-        this->calibrate();
-    }
-
-    Serial.println(motor.zero_electric_angle);
-
     motor.monitor_downsample = 0; // disable monitor at first - optional
 
     // disableCore0WDT();
 
     float current_detent_center = motor.shaft_angle;
-    KnobConfig config = {
+    PB_SmartKnobConfig config = {
         .num_positions = 2,
         .position = 0,
         .position_width_radians = 60 * _PI / 180,
@@ -115,6 +95,8 @@ void MotorTask::run() {
     uint32_t last_idle_start = 0;
     uint32_t last_publish = 0;
 
+    PB_SmartKnobConfig latest_config = config;
+
     while (1) {
         motor.loopFOC();
 
@@ -122,10 +104,14 @@ void MotorTask::run() {
         Command command;
         if (xQueueReceive(queue_, &command, 0) == pdTRUE) {
             switch (command.command_type) {
+                case CommandType::CALIBRATE:
+                    calibrate();
+                    break;
                 case CommandType::CONFIG: {
                     // Change haptic input mode
                     config = command.data.config;
-                    Serial.println("Got new config");
+                    latest_config = config;
+                    log("Got new config");
                     current_detent_center = motor.shaft_angle;
                     #if SK_INVERT_ROTATION
                         current_detent_center = -motor.shaft_angle;
@@ -222,10 +208,11 @@ void MotorTask::run() {
         }
 
         // Publish current status to other registered tasks periodically
-        if (millis() - last_publish > 10) {
+        if (millis() - last_publish > 5) {
             publish({
                 .current_position = config.position,
                 .sub_position_unit = -angle_to_detent_center / config.position_width_radians,
+                .has_config = true,
                 .config = config,
             });
             last_publish = millis();
@@ -237,7 +224,7 @@ void MotorTask::run() {
     }
 }
 
-void MotorTask::setConfig(const KnobConfig& config) {
+void MotorTask::setConfig(const PB_SmartKnobConfig& config) {
     Command command = {
         .command_type = CommandType::CONFIG,
         .data = {
@@ -260,12 +247,22 @@ void MotorTask::playHaptic(bool press) {
     xQueueSend(queue_, &command, portMAX_DELAY);
 }
 
+void MotorTask::runCalibration() {
+    Command command = {
+        .command_type = CommandType::CALIBRATE,
+        .data = {
+            .unused = 0,
+        }
+    };
+    xQueueSend(queue_, &command, portMAX_DELAY);
+}
+
 
 void MotorTask::addListener(QueueHandle_t queue) {
     listeners_.push_back(queue);
 }
 
-void MotorTask::publish(const KnobState& state) {
+void MotorTask::publish(const PB_SmartKnobState& state) {
     for (auto listener : listeners_) {
         xQueueOverwrite(listener, &state);
     }
@@ -277,7 +274,7 @@ void MotorTask::calibrate() {
     // So this value is based on experimentation.
     // TODO: dig into SimpleFOC calibration and find/fix the issue
 
-    Serial.println("\n\n\nStarting calibration, please do not touch to motor until complete!");
+    log("\n\n\nStarting calibration, please DO NOT TOUCH MOTOR until complete!");
 
     motor.controller = MotionControlType::angle_openloop;
     motor.pole_pairs = 1;
@@ -309,16 +306,16 @@ void MotorTask::calibrate() {
     motor.voltage_limit = 0;
     motor.move(a);
 
-    Serial.println();
+    log("");
 
     // TODO: check for no motor movement!
 
-    Serial.print("Sensor measures positive for positive motor rotation: ");
+    log("Sensor measures positive for positive motor rotation:");
     if (end_sensor > start_sensor) {
-        Serial.println("YES, Direction=CW");
+        log("YES, Direction=CW");
         motor.initFOC(0, Direction::CW);
     } else {
-        Serial.println("NO, Direction=CCW");
+        log("NO, Direction=CCW");
         motor.initFOC(0, Direction::CCW);
     }
 
@@ -326,22 +323,23 @@ void MotorTask::calibrate() {
     // #### Determine pole-pairs
     // Rotate 20 electrical revolutions and measure mechanical angle traveled, to calculate pole-pairs
     uint8_t electrical_revolutions = 20;
-    Serial.printf("Going to measure %d electrical revolutions...\n", electrical_revolutions);
+    snprintf(buf_, sizeof(buf_), "Going to measure %d electrical revolutions...", electrical_revolutions);
+    log(buf_);
     motor.voltage_limit = 5;
     motor.move(a);
-    Serial.println("Going to electrical zero...");
+    log("Going to electrical zero...");
     float destination = a + _2PI;
     for (; a < destination; a += 0.03) {
         encoder.update();
         motor.move(a);
         delay(1);
     }
-    Serial.println("pause..."); // Let momentum settle...
+    log("pause..."); // Let momentum settle...
     for (uint16_t i = 0; i < 1000; i++) {
         encoder.update();
         delay(1);
     }
-    Serial.println("Measuring...");
+    log("Measuring...");
 
     start_sensor = motor.sensor_direction * encoder.getAngle();
     destination = a + electrical_revolutions * _2PI;
@@ -360,16 +358,17 @@ void MotorTask::calibrate() {
     motor.move(a);
 
     if (fabsf(motor.shaft_angle - motor.target) > 1 * PI / 180) {
-        Serial.println("ERROR: motor did not reach target!");
+        log("ERROR: motor did not reach target!");
         while(1) {}
     }
 
     float electrical_per_mechanical = electrical_revolutions * _2PI / (end_sensor - start_sensor);
-    Serial.print("Electrical angle / mechanical angle (i.e. pole pairs) = ");
-    Serial.println(electrical_per_mechanical);
+    snprintf(buf_, sizeof(buf_), "Electrical angle / mechanical angle (i.e. pole pairs) = %.2f", electrical_per_mechanical);
+    log(buf_);
 
     int measured_pole_pairs = (int)round(electrical_per_mechanical);
-    Serial.printf("Pole pairs set to %d\n", measured_pole_pairs);
+    snprintf(buf_, sizeof(buf_), "Pole pairs set to %d", measured_pole_pairs);
+    log(buf_);
 
     delay(1000);
 
@@ -396,11 +395,8 @@ void MotorTask::calibrate() {
         offset_x += cosf(offset_angle);
         offset_y += sinf(offset_angle);
 
-        Serial.print(degrees(real_electrical_angle));
-        Serial.print(", ");
-        Serial.print(degrees(measured_electrical_angle));
-        Serial.print(", ");
-        Serial.println(degrees(_normalizeAngle(offset_angle)));
+        snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
+        log(buf_);
     }
     for (; a > destination2; a -= 0.4) {
         motor.move(a);
@@ -416,11 +412,8 @@ void MotorTask::calibrate() {
         offset_x += cosf(offset_angle);
         offset_y += sinf(offset_angle);
 
-        Serial.print(degrees(real_electrical_angle));
-        Serial.print(", ");
-        Serial.print(degrees(measured_electrical_angle));
-        Serial.print(", ");
-        Serial.println(degrees(_normalizeAngle(offset_angle)));
+        snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
+        log(buf_);
     }
     motor.voltage_limit = 0;
     motor.move(a);
@@ -435,14 +428,39 @@ void MotorTask::calibrate() {
     motor.voltage_limit = 5;
     motor.controller = MotionControlType::torque;
 
-    Serial.print("\n\nRESULTS:\n  Update these constants at the top of " __FILE__ "\n  ZERO_ELECTRICAL_OFFSET: ");
-    Serial.println(motor.zero_electric_angle);
-    Serial.print("  FOC_DIRECTION: ");
+    log("\n\nRESULTS:\n  Update these constants at the top of " __FILE__);
+    snprintf(buf_, sizeof(buf_), "  ZERO_ELECTRICAL_OFFSET: %.2f", motor.zero_electric_angle);
+    log(buf_);
     if (motor.sensor_direction == Direction::CW) {
-        Serial.println("Direction::CW");
+        log("  FOC_DIRECTION: Direction::CW");
     } else {
-        Serial.println("Direction::CCW");
+        log("  FOC_DIRECTION: Direction::CCW");
     }
-    Serial.printf("  MOTOR_POLE_PAIRS: %d\n", motor.pole_pairs);
+    snprintf(buf_, sizeof(buf_), "  MOTOR_POLE_PAIRS: %d", motor.pole_pairs);
+    log(buf_);
     delay(2000);
 }
+
+void MotorTask::checkSensorError() {
+#if SENSOR_TLV
+    if (encoder.getAndClearError()) {
+        log("LOCKED!");
+    }
+#elif SENSOR_MT6701
+    MT6701Error error = encoder.getAndClearError();
+    if (error.error) {
+        snprintf(buf_, sizeof(buf_), "CRC error. Received %d; calculated %d", error.received_crc, error.calculated_crc);
+        log(buf_);
+    }
+#endif
+}
+
+void MotorTask::setLogger(Logger* logger) {
+    logger_ = logger;
+}
+
+void MotorTask::log(const char* msg) {
+    if (logger_ != nullptr) {
+        logger_->log(msg);
+    }
+}

+ 13 - 5
firmware/src/motor_task.h

@@ -4,11 +4,13 @@
 #include <SimpleFOC.h>
 #include <vector>
 
-#include "knob_data.h"
+#include "logger.h"
+#include "proto_gen/smartknob.pb.h"
 #include "task.h"
 
 
 enum class CommandType {
+    CALIBRATE,
     CONFIG,
     HAPTIC,
 };
@@ -20,7 +22,8 @@ struct HapticData {
 struct Command {
     CommandType command_type;
     union CommandData {
-        KnobConfig config;
+        uint8_t unused;
+        PB_SmartKnobConfig config;
         HapticData haptic;
     };
     CommandData data;
@@ -33,23 +36,28 @@ class MotorTask : public Task<MotorTask> {
         MotorTask(const uint8_t task_core);
         ~MotorTask();
 
-        void setConfig(const KnobConfig& config);
+        void setConfig(const PB_SmartKnobConfig& config);
         void playHaptic(bool press);
+        void runCalibration();
 
         void addListener(QueueHandle_t queue);
+        void setLogger(Logger* logger);
 
     protected:
         void run();
 
     private:
         QueueHandle_t queue_;
-
+        Logger* logger_;
         std::vector<QueueHandle_t> listeners_;
+        char buf_[72];
 
         // BLDC motor & driver instance
         BLDCMotor motor = BLDCMotor(1);
         BLDCDriver6PWM driver = BLDCDriver6PWM(PIN_UH, PIN_UL, PIN_VH, PIN_VL, PIN_WH, PIN_WL);
 
-        void publish(const KnobState& state);
+        void publish(const PB_SmartKnobState& state);
         void calibrate();
+        void checkSensorError();
+        void log(const char* msg);
 };

+ 11 - 1
firmware/src/mt6701_sensor.cpp

@@ -105,7 +105,11 @@ float MT6701Sensor::getSensorAngle() {
         x_ = new_x * ALPHA + x_ * (1-ALPHA);
         y_ = new_y * ALPHA + y_ * (1-ALPHA);
       } else {
-        Serial.printf("Bad CRC. expected %d, actual %d\n", calculated_crc, received_crc);
+        error_ = {
+          .error = true,
+          .received_crc = received_crc,
+          .calculated_crc = calculated_crc,
+        };
       }
 
       last_update_ = now;
@@ -117,4 +121,10 @@ float MT6701Sensor::getSensorAngle() {
     return rad;
 }
 
+MT6701Error MT6701Sensor::getAndClearError() {
+  MT6701Error out = error_;
+  error_ = {};
+  return out;
+}
+
 #endif

+ 10 - 0
firmware/src/mt6701_sensor.h

@@ -3,6 +3,12 @@
 #include <SimpleFOC.h>
 #include "driver/spi_master.h"
 
+struct MT6701Error {
+    bool error;
+    uint8_t received_crc;
+    uint8_t calculated_crc;
+};
+
 class MT6701Sensor : public Sensor {
     public:
         MT6701Sensor();
@@ -16,6 +22,8 @@ class MT6701Sensor : public Sensor {
         //    Calling this method directly does not update the base-class internal fields.
         //    Use update() when calling from outside code.
         float getSensorAngle();
+
+        MT6701Error getAndClearError();
     private:
 
         spi_device_handle_t spi_device_;
@@ -24,4 +32,6 @@ class MT6701Sensor : public Sensor {
         float x_;
         float y_;
         uint32_t last_update_;
+
+        MT6701Error error_ = {};
 };

+ 30 - 0
firmware/src/proto_gen/smartknob.pb.c

@@ -0,0 +1,30 @@
+/* Automatically generated nanopb constant definitions */
+/* Generated by nanopb-0.4.7-dev */
+
+#include "smartknob.pb.h"
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+PB_BIND(PB_FromSmartKnob, PB_FromSmartKnob, 2)
+
+
+PB_BIND(PB_Ack, PB_Ack, AUTO)
+
+
+PB_BIND(PB_Log, PB_Log, 2)
+
+
+PB_BIND(PB_SmartKnobState, PB_SmartKnobState, AUTO)
+
+
+PB_BIND(PB_ToSmartknob, PB_ToSmartknob, AUTO)
+
+
+PB_BIND(PB_SmartKnobConfig, PB_SmartKnobConfig, AUTO)
+
+
+PB_BIND(PB_RequestState, PB_RequestState, AUTO)
+
+
+

+ 187 - 0
firmware/src/proto_gen/smartknob.pb.h

@@ -0,0 +1,187 @@
+/* Automatically generated nanopb header */
+/* Generated by nanopb-0.4.7-dev */
+
+#ifndef PB_PB_SMARTKNOB_PB_H_INCLUDED
+#define PB_PB_SMARTKNOB_PB_H_INCLUDED
+#include <pb.h>
+
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+/* Struct definitions */
+typedef struct _PB_RequestState {
+    char dummy_field;
+} PB_RequestState;
+
+typedef struct _PB_Ack {
+    uint32_t nonce;
+} PB_Ack;
+
+typedef struct _PB_Log {
+    char msg[256];
+} PB_Log;
+
+typedef struct _PB_SmartKnobConfig {
+    int32_t num_positions;
+    int32_t position;
+    float position_width_radians;
+    float detent_strength_unit;
+    float endstop_strength_unit;
+    float snap_point;
+    char text[51];
+} PB_SmartKnobConfig;
+
+typedef struct _PB_SmartKnobState {
+    int32_t current_position;
+    float sub_position_unit;
+    bool has_config;
+    PB_SmartKnobConfig config;
+} PB_SmartKnobState;
+
+/* Message TO the Smartknob from the USB host */
+typedef struct _PB_ToSmartknob {
+    uint32_t nonce;
+    pb_size_t which_payload;
+    union {
+        PB_RequestState request_state;
+        PB_SmartKnobConfig smartknob_config;
+    } payload;
+} PB_ToSmartknob;
+
+/* Message FROM the SmartKnob to USB host */
+typedef struct _PB_FromSmartKnob {
+    pb_size_t which_payload;
+    union {
+        PB_Ack ack;
+        PB_Log log;
+        PB_SmartKnobState smartknob_state;
+    } payload;
+} PB_FromSmartKnob;
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Initializer values for message structs */
+#define PB_FromSmartKnob_init_default            {0, {PB_Ack_init_default}}
+#define PB_Ack_init_default                      {0}
+#define PB_Log_init_default                      {""}
+#define PB_SmartKnobState_init_default           {0, 0, false, PB_SmartKnobConfig_init_default}
+#define PB_ToSmartknob_init_default              {0, 0, {PB_RequestState_init_default}}
+#define PB_SmartKnobConfig_init_default          {0, 0, 0, 0, 0, 0, ""}
+#define PB_RequestState_init_default             {0}
+#define PB_FromSmartKnob_init_zero               {0, {PB_Ack_init_zero}}
+#define PB_Ack_init_zero                         {0}
+#define PB_Log_init_zero                         {""}
+#define PB_SmartKnobState_init_zero              {0, 0, false, PB_SmartKnobConfig_init_zero}
+#define PB_ToSmartknob_init_zero                 {0, 0, {PB_RequestState_init_zero}}
+#define PB_SmartKnobConfig_init_zero             {0, 0, 0, 0, 0, 0, ""}
+#define PB_RequestState_init_zero                {0}
+
+/* Field tags (for use in manual encoding/decoding) */
+#define PB_Ack_nonce_tag                         1
+#define PB_Log_msg_tag                           1
+#define PB_SmartKnobConfig_num_positions_tag     1
+#define PB_SmartKnobConfig_position_tag          2
+#define PB_SmartKnobConfig_position_width_radians_tag 3
+#define PB_SmartKnobConfig_detent_strength_unit_tag 4
+#define PB_SmartKnobConfig_endstop_strength_unit_tag 5
+#define PB_SmartKnobConfig_snap_point_tag        6
+#define PB_SmartKnobConfig_text_tag              7
+#define PB_SmartKnobState_current_position_tag   1
+#define PB_SmartKnobState_sub_position_unit_tag  2
+#define PB_SmartKnobState_config_tag             3
+#define PB_ToSmartknob_nonce_tag                 1
+#define PB_ToSmartknob_request_state_tag         2
+#define PB_ToSmartknob_smartknob_config_tag      3
+#define PB_FromSmartKnob_ack_tag                 1
+#define PB_FromSmartKnob_log_tag                 2
+#define PB_FromSmartKnob_smartknob_state_tag     3
+
+/* Struct field encoding specification for nanopb */
+#define PB_FromSmartKnob_FIELDLIST(X, a) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,ack,payload.ack),   1) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,log,payload.log),   2) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,smartknob_state,payload.smartknob_state),   3)
+#define PB_FromSmartKnob_CALLBACK NULL
+#define PB_FromSmartKnob_DEFAULT NULL
+#define PB_FromSmartKnob_payload_ack_MSGTYPE PB_Ack
+#define PB_FromSmartKnob_payload_log_MSGTYPE PB_Log
+#define PB_FromSmartKnob_payload_smartknob_state_MSGTYPE PB_SmartKnobState
+
+#define PB_Ack_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, UINT32,   nonce,             1)
+#define PB_Ack_CALLBACK NULL
+#define PB_Ack_DEFAULT NULL
+
+#define PB_Log_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, STRING,   msg,               1)
+#define PB_Log_CALLBACK NULL
+#define PB_Log_DEFAULT NULL
+
+#define PB_SmartKnobState_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, INT32,    current_position,   1) \
+X(a, STATIC,   SINGULAR, FLOAT,    sub_position_unit,   2) \
+X(a, STATIC,   OPTIONAL, MESSAGE,  config,            3)
+#define PB_SmartKnobState_CALLBACK NULL
+#define PB_SmartKnobState_DEFAULT NULL
+#define PB_SmartKnobState_config_MSGTYPE PB_SmartKnobConfig
+
+#define PB_ToSmartknob_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, UINT32,   nonce,             1) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,request_state,payload.request_state),   2) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,smartknob_config,payload.smartknob_config),   3)
+#define PB_ToSmartknob_CALLBACK NULL
+#define PB_ToSmartknob_DEFAULT NULL
+#define PB_ToSmartknob_payload_request_state_MSGTYPE PB_RequestState
+#define PB_ToSmartknob_payload_smartknob_config_MSGTYPE PB_SmartKnobConfig
+
+#define PB_SmartKnobConfig_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, INT32,    num_positions,     1) \
+X(a, STATIC,   SINGULAR, INT32,    position,          2) \
+X(a, STATIC,   SINGULAR, FLOAT,    position_width_radians,   3) \
+X(a, STATIC,   SINGULAR, FLOAT,    detent_strength_unit,   4) \
+X(a, STATIC,   SINGULAR, FLOAT,    endstop_strength_unit,   5) \
+X(a, STATIC,   SINGULAR, FLOAT,    snap_point,        6) \
+X(a, STATIC,   SINGULAR, STRING,   text,              7)
+#define PB_SmartKnobConfig_CALLBACK NULL
+#define PB_SmartKnobConfig_DEFAULT NULL
+
+#define PB_RequestState_FIELDLIST(X, a) \
+
+#define PB_RequestState_CALLBACK NULL
+#define PB_RequestState_DEFAULT NULL
+
+extern const pb_msgdesc_t PB_FromSmartKnob_msg;
+extern const pb_msgdesc_t PB_Ack_msg;
+extern const pb_msgdesc_t PB_Log_msg;
+extern const pb_msgdesc_t PB_SmartKnobState_msg;
+extern const pb_msgdesc_t PB_ToSmartknob_msg;
+extern const pb_msgdesc_t PB_SmartKnobConfig_msg;
+extern const pb_msgdesc_t PB_RequestState_msg;
+
+/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
+#define PB_FromSmartKnob_fields &PB_FromSmartKnob_msg
+#define PB_Ack_fields &PB_Ack_msg
+#define PB_Log_fields &PB_Log_msg
+#define PB_SmartKnobState_fields &PB_SmartKnobState_msg
+#define PB_ToSmartknob_fields &PB_ToSmartknob_msg
+#define PB_SmartKnobConfig_fields &PB_SmartKnobConfig_msg
+#define PB_RequestState_fields &PB_RequestState_msg
+
+/* Maximum encoded size of messages (where known) */
+#define PB_Ack_size                              6
+#define PB_FromSmartKnob_size                    261
+#define PB_Log_size                              258
+#define PB_RequestState_size                     0
+#define PB_SmartKnobConfig_size                  94
+#define PB_SmartKnobState_size                   112
+#define PB_ToSmartknob_size                      102
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif

+ 20 - 0
firmware/src/proto_helpers.h

@@ -0,0 +1,20 @@
+#pragma once
+
+#include "proto_gen/smartknob.pb.h"
+
+bool config_eq(PB_SmartKnobConfig& first, PB_SmartKnobConfig& second) {
+    return first.detent_strength_unit == second.detent_strength_unit
+        && first.endstop_strength_unit == second.endstop_strength_unit
+        && first.num_positions == second.num_positions
+        && first.position == second.position
+        && first.position_width_radians == second.position_width_radians
+        && first.snap_point == second.snap_point
+        && strcmp(first.text, second.text) == 0;
+}
+
+bool state_eq(PB_SmartKnobState& first, PB_SmartKnobState& second) {
+    return first.has_config == second.has_config
+        && (!first.has_config || config_eq(first.config, second.config))
+        && first.current_position == second.current_position
+        && first.sub_position_unit == second.sub_position_unit;
+}

+ 21 - 0
firmware/src/serial/crc32.cpp

@@ -0,0 +1,21 @@
+/* Simple public domain implementation of the standard CRC32 checksum.
+ * Outputs the checksum for each file given as a command line argument.
+ * Invalid file names and files that cause errors are silently skipped.
+ * The program reads from stdin if it is called with no arguments. */
+
+#include "crc32.h"
+
+static uint32_t crc32_for_byte(uint32_t r) {
+  for(int j = 0; j < 8; ++j)
+    r = (r & 1? 0: (uint32_t)0xEDB88320L) ^ r >> 1;
+  return r ^ (uint32_t)0xFF000000L;
+}
+
+void crc32(const void *data, size_t n_bytes, uint32_t* crc) {
+  static uint32_t table[0x100];
+  if(!*table)
+    for(size_t i = 0; i < 0x100; ++i)
+      table[i] = crc32_for_byte(i);
+  for(size_t i = 0; i < n_bytes; ++i)
+    *crc = table[(uint8_t)*crc ^ ((uint8_t*)data)[i]] ^ *crc >> 8;
+}

+ 11 - 0
firmware/src/serial/crc32.h

@@ -0,0 +1,11 @@
+/* Simple public domain implementation of the standard CRC32 checksum.
+ * Outputs the checksum for each file given as a command line argument.
+ * Invalid file names and files that cause errors are silently skipped.
+ * The program reads from stdin if it is called with no arguments. */
+#pragma once
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+void crc32(const void *data, size_t n_bytes, uint32_t* crc);

+ 28 - 0
firmware/src/serial/serial_protocol.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include <functional>
+
+#include "../logger.h"
+#include "../proto_gen/smartknob.pb.h"
+
+#define SERIAL_PROTOCOL_LEGACY 0
+#define SERIAL_PROTOCOL_PROTO 1
+
+typedef std::function<void(uint8_t)> ProtocolChangeCallback;
+
+class SerialProtocol : public Logger {
+    public:
+        SerialProtocol() : Logger() {}
+        virtual ~SerialProtocol(){}
+
+        virtual void loop() = 0;
+
+        virtual void handleState(const PB_SmartKnobState& state) = 0;
+
+        virtual void setProtocolChangeCallback(ProtocolChangeCallback cb) {
+            protocol_change_callback_ = cb;
+        }
+    
+    protected:
+        ProtocolChangeCallback protocol_change_callback_;
+};

+ 44 - 0
firmware/src/serial/serial_protocol_plaintext.cpp

@@ -0,0 +1,44 @@
+#include "../proto_gen/smartknob.pb.h"
+
+#include "serial_protocol_plaintext.h"
+
+void SerialProtocolPlaintext::handleState(const PB_SmartKnobState& state) {
+    bool substantial_change = (latest_state_.current_position != state.current_position)
+        || (latest_state_.config.detent_strength_unit != state.config.detent_strength_unit)
+        || (latest_state_.config.endstop_strength_unit != state.config.endstop_strength_unit)
+        || (latest_state_.config.num_positions != state.config.num_positions);
+    latest_state_ = state;
+
+    if (substantial_change) {
+        stream_.printf("STATE: %d/%d  (detent strength: %0.2f, width: %0.0f deg, endstop strength: %0.2f)\n", state.current_position, state.config.num_positions - 1, state.config.detent_strength_unit, degrees(state.config.position_width_radians), state.config.endstop_strength_unit);
+    }
+}
+
+void SerialProtocolPlaintext::log(const char* msg) {
+    stream_.print("LOG: ");
+    stream_.println(msg);
+}
+
+void SerialProtocolPlaintext::loop() {
+    while (stream_.available() > 0) {
+        int b = stream_.read();
+        if (b == 0) {
+            if (protocol_change_callback_) {
+                protocol_change_callback_(SERIAL_PROTOCOL_PROTO);
+            }
+            break;
+        }
+        if (b == ' ') {
+            if (demo_config_change_callback_) {
+                demo_config_change_callback_();
+            }
+        } else if (b == 'C') {
+            motor_task_.runCalibration();
+        }
+    }
+}
+
+void SerialProtocolPlaintext::init(DemoConfigChangeCallback cb) {
+    demo_config_change_callback_ = cb;
+    stream_.println("SmartKnob starting!\n\nSerial mode: plaintext\nPress 'C' at any time to calibrate.\nPress <Space> to change haptic modes.");
+}

+ 26 - 0
firmware/src/serial/serial_protocol_plaintext.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include "../proto_gen/smartknob.pb.h"
+
+#include "motor_task.h"
+#include "serial_protocol.h"
+#include "uart_stream.h"
+
+typedef std::function<void(void)> DemoConfigChangeCallback;
+
+class SerialProtocolPlaintext : public SerialProtocol {
+    public:
+        SerialProtocolPlaintext(Stream& stream, MotorTask& motor_task) : SerialProtocol(), stream_(stream), motor_task_(motor_task) {}
+        ~SerialProtocolPlaintext(){}
+        void log(const char* msg) override;
+        void loop() override;
+        void handleState(const PB_SmartKnobState& state) override;
+
+        void init(DemoConfigChangeCallback cb);
+    
+    private:
+        Stream& stream_;
+        MotorTask& motor_task_;
+        PB_SmartKnobState latest_state_ = {};
+        DemoConfigChangeCallback demo_config_change_callback_;
+};

+ 152 - 0
firmware/src/serial/serial_protocol_protobuf.cpp

@@ -0,0 +1,152 @@
+#include <PacketSerial.h>
+
+#include "../proto_gen/smartknob.pb.h"
+
+#include "../proto_helpers.h"
+
+#include "crc32.h"
+#include "pb_encode.h"
+#include "pb_decode.h"
+#include "serial_protocol_protobuf.h"
+
+static SerialProtocolProtobuf* singleton_for_packet_serial = 0;
+
+static const uint16_t MIN_STATE_INTERVAL_MILLIS = 5;
+static const uint16_t PERIODIC_STATE_INTERVAL_MILLIS = 5000;
+
+SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task) :
+        SerialProtocol(),
+        stream_(stream),
+        motor_task_(motor_task),
+        packet_serial_() {
+    packet_serial_.setStream(&stream);
+
+    // Note: not threadsafe or instance safe!! but PacketSerial requires a legacy function pointer, so we can't
+    // use a member, std::function, or lambda with captures
+    assert(singleton_for_packet_serial == 0);
+    singleton_for_packet_serial = this;
+
+    packet_serial_.setPacketHandler([](const uint8_t* buffer, size_t size) {
+        singleton_for_packet_serial->handlePacket(buffer, size);
+    });
+}
+
+void SerialProtocolProtobuf::handleState(const PB_SmartKnobState& state) {
+    latest_state_ = state;
+}
+
+void SerialProtocolProtobuf::ack(uint32_t nonce) {
+    pb_tx_buffer_ = {};
+    pb_tx_buffer_.which_payload = PB_FromSmartKnob_ack_tag;
+    pb_tx_buffer_.payload.ack.nonce = nonce;
+    sendPbTxBuffer();
+}
+
+void SerialProtocolProtobuf::log(const char* msg) {
+    pb_tx_buffer_ = {};
+    pb_tx_buffer_.which_payload = PB_FromSmartKnob_log_tag;
+
+    strlcpy(pb_tx_buffer_.payload.log.msg, msg, sizeof(pb_tx_buffer_.payload.log.msg));
+
+    sendPbTxBuffer();
+}
+
+void SerialProtocolProtobuf::loop() {
+    do {
+        packet_serial_.update();
+    } while (stream_.available());
+
+    // Rate limit state change transmissions
+    bool state_changed = !state_eq(latest_state_, last_sent_state_) && millis() - last_sent_state_millis_ >= MIN_STATE_INTERVAL_MILLIS;
+
+    // Send state periodically or when forced, regardless of rate limit for state changes
+    bool force_send_state = state_requested_ || millis() - last_sent_state_millis_ > PERIODIC_STATE_INTERVAL_MILLIS;
+    if (state_changed || force_send_state) {
+        state_requested_ = false;
+        pb_tx_buffer_ = {};
+        pb_tx_buffer_.which_payload = PB_FromSmartKnob_smartknob_state_tag;
+        pb_tx_buffer_.payload.smartknob_state = latest_state_;
+
+        sendPbTxBuffer();
+
+        last_sent_state_ = latest_state_;
+        last_sent_state_millis_ = millis();
+    }
+}
+
+void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
+    if (size <= 4) {
+        // Too small, ignore bad packet
+        log("Small packet");
+        return;
+    }
+
+    // Compute and append little-endian CRC32
+    uint32_t expected_crc = 0;
+    crc32(buffer, size - 4, &expected_crc);
+
+    uint32_t provided_crc = buffer[size - 4]
+                         | (buffer[size - 3] << 8)
+                         | (buffer[size - 2] << 16)
+                         | (buffer[size - 1] << 24);
+
+    if (expected_crc != provided_crc) {
+        char buf[200];
+        snprintf(buf, sizeof(buf), "Bad CRC (%u byte packet). Expected %08x but got %08x.", size - 4, expected_crc, provided_crc);
+        log(buf);
+        return;
+    }
+
+    pb_istream_t stream = pb_istream_from_buffer(buffer, size - 4);
+    if (!pb_decode(&stream, PB_ToSmartknob_fields, &pb_rx_buffer_)) {
+        char buf[200];
+        snprintf(buf, sizeof(buf), "Decoding failed: %s", PB_GET_ERROR(&stream));
+        log(buf);
+        return;
+    }
+
+    // Always ACK immediately
+    ack(pb_rx_buffer_.nonce);
+    if (pb_rx_buffer_.nonce == last_nonce_) {
+        // Ignore any extraneous retries
+        char buf[200];
+        snprintf(buf, sizeof(buf), "Already handled nonce %u", pb_rx_buffer_.nonce);
+        log(buf);
+        return;
+    }
+    last_nonce_ = pb_rx_buffer_.nonce;
+    
+    switch (pb_rx_buffer_.which_payload) {
+        case PB_ToSmartknob_smartknob_config_tag: {
+            motor_task_.setConfig(pb_rx_buffer_.payload.smartknob_config);
+            break;
+        }
+        default: {
+            char buf[200];
+            snprintf(buf, sizeof(buf), "Unknown payload type: %d", pb_rx_buffer_.which_payload);
+            log(buf);
+            return;
+        }
+    }
+}
+
+void SerialProtocolProtobuf::sendPbTxBuffer() {
+    // Encode protobuf message to byte buffer
+    pb_ostream_t stream = pb_ostream_from_buffer(tx_buffer_, sizeof(tx_buffer_));
+    if (!pb_encode(&stream, PB_FromSmartKnob_fields, &pb_tx_buffer_)) {
+        stream_.println(stream.errmsg);
+        stream_.flush();
+        assert(false);
+    }
+
+    // Compute and append little-endian CRC32
+    uint32_t crc = 0;
+    crc32(tx_buffer_, stream.bytes_written, &crc);
+    tx_buffer_[stream.bytes_written + 0] = (crc >> 0)  & 0xFF;
+    tx_buffer_[stream.bytes_written + 1] = (crc >> 8)  & 0xFF;
+    tx_buffer_[stream.bytes_written + 2] = (crc >> 16) & 0xFF;
+    tx_buffer_[stream.bytes_written + 3] = (crc >> 24) & 0xFF;
+
+    // Encode and send proto+CRC as a COBS packet
+    packet_serial_.send(tx_buffer_, stream.bytes_written + 4);
+}

+ 41 - 0
firmware/src/serial/serial_protocol_protobuf.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include <PacketSerial.h>
+
+#include "../proto_gen/smartknob.pb.h"
+
+#include "motor_task.h"
+#include "serial_protocol.h"
+#include "uart_stream.h"
+
+class SerialProtocolProtobuf : public SerialProtocol {
+    public:
+        SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task);
+        ~SerialProtocolProtobuf(){}
+        void log(const char* msg) override;
+        void loop() override;
+        void handleState(const PB_SmartKnobState& state) override;
+    
+    private:
+        Stream& stream_;
+        MotorTask& motor_task_;
+        
+        PB_FromSmartKnob pb_tx_buffer_;
+        PB_ToSmartknob pb_rx_buffer_;
+
+        uint8_t tx_buffer_[PB_FromSmartKnob_size + 4]; // Max message size + CRC32
+
+        PacketSerial_<COBS, 0, (PB_ToSmartknob_size + 4) * 2 + 10> packet_serial_;
+
+        uint32_t last_nonce_;
+
+        PB_SmartKnobState latest_state_ = {};
+        PB_SmartKnobState last_sent_state_ = {};
+        uint32_t last_sent_state_millis_ = 0;
+
+        bool state_requested_;
+
+        void sendPbTxBuffer();
+        void handlePacket(const uint8_t* buffer, size_t size);
+        void ack(uint32_t nonce);
+};

+ 64 - 0
firmware/src/serial/uart_stream.cpp

@@ -0,0 +1,64 @@
+/*
+   Copyright 2021 Scott Bezek and the splitflap contributors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+#include <driver/uart.h>
+
+#include "config.h"
+#include "uart_stream.h"
+
+UartStream::UartStream() : Stream() {
+}
+
+void UartStream::begin() {
+    uart_config_t conf;
+    conf.baud_rate           = MONITOR_SPEED;
+    conf.data_bits           = UART_DATA_8_BITS;
+    conf.parity              = UART_PARITY_DISABLE;
+    conf.stop_bits           = UART_STOP_BITS_1;
+    conf.flow_ctrl           = UART_HW_FLOWCTRL_DISABLE;
+    conf.rx_flow_ctrl_thresh = 0;
+    conf.use_ref_tick        = false;
+    assert(uart_param_config(uart_port_, &conf) == ESP_OK);
+    assert(uart_driver_install(uart_port_, 32000, 32000, 0, NULL, 0) == ESP_OK);
+}
+
+int UartStream::peek() {
+    return -1;
+}
+
+int UartStream::available() {
+    size_t size = 0;
+    assert(uart_get_buffered_data_len(uart_port_, &size) == ESP_OK);
+    return size;
+}
+
+int UartStream::read() {
+    uint8_t b;
+    int res = uart_read_bytes(uart_port_, &b, 1, 0);
+    return res != 1 ? -1 : b;
+}
+
+void UartStream::flush() {
+
+}
+
+size_t UartStream::write(uint8_t b) {
+    return uart_write_bytes(uart_port_, (char*)&b, 1);
+}
+
+size_t UartStream::write(const uint8_t *buffer, size_t size) {
+    return uart_write_bytes(uart_port_, (const char*)buffer, size);
+}

+ 47 - 0
firmware/src/serial/uart_stream.h

@@ -0,0 +1,47 @@
+/*
+   Copyright 2021 Scott Bezek and the splitflap contributors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+#pragma once
+
+#include <Arduino.h>
+
+#include <driver/uart.h>
+
+/**
+ * Implementation of an Arduino Stream for UART serial communications using the esp uart driver
+ * directly, rather than the Arduino HAL which has a small fixed underlying rx FIFO size and
+ * potentially other issues that cause dropped bytes at high speeds/bursts.
+ * 
+ * This is not a full or optimized implementation; just the minimal necessary for this project.
+ */
+class UartStream : public Stream {
+    public:
+        UartStream();
+
+        void begin();
+
+        // Stream methods
+        int available() override;
+        int read() override;
+        int peek() override;
+        void flush() override;
+
+        // Print methods
+        size_t write(uint8_t b) override;
+        size_t write(const uint8_t *buffer, size_t size) override;
+
+    private:
+        const uart_port_t uart_port_ = UART_NUM_0;
+};

+ 7 - 1
firmware/src/tlv_sensor.cpp

@@ -35,7 +35,7 @@ float TlvSensor::getSensorAngle() {
         }
       }
       if (all_same) {
-        Serial.println("LOCKED!");
+        error_ = true;
         init(wire_, invert_);
         // Force unique frame counts to avoid reset loop
         for (uint8_t i = 1; i < sizeof(frame_counts_); i++) {
@@ -49,3 +49,9 @@ float TlvSensor::getSensorAngle() {
     }
     return rad;
 }
+
+bool TlvSensor::getAndClearError() {
+  bool error = error_;
+  error_ = false;
+  return error;
+}

+ 4 - 0
firmware/src/tlv_sensor.h

@@ -16,6 +16,8 @@ class TlvSensor : public Sensor {
         //    Calling this method directly does not update the base-class internal fields.
         //    Use update() when calling from outside code.
         float getSensorAngle();
+
+        bool getAndClearError();
     private:
         Tlv493d tlv_ = Tlv493d();
         float x_;
@@ -24,6 +26,8 @@ class TlvSensor : public Sensor {
         TwoWire* wire_;
         bool invert_;
 
+        bool error_ = false;
+
         uint8_t frame_counts_[3] = {};
         uint8_t cur_frame_count_index_ = 0;
 };

+ 13 - 2
firmware/platformio.ini → platformio.ini

@@ -10,11 +10,16 @@
 
 [platformio]
 default_envs = view
+src_dir = firmware/src
+lib_dir = firmware/lib
+include_dir = firmware/include
+test_dir = firmware/test
+data_dir = firmware/data
 
 [base_config]
 platform = espressif32@3.4
 framework = arduino
-monitor_speed = 115200
+monitor_speed = 921600
 monitor_flags = 
 	--eol=CRLF
 	--echo
@@ -23,9 +28,9 @@ lib_deps =
     askuric/Simple FOC @ 2.2.0
     infineon/TLV493D-Magnetic-Sensor @ 1.0.3
     bxparks/AceButton @ 1.9.1
-
 build_flags =
   -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
+  -DMONITOR_SPEED=921600
 
 [env:view]
 extends = base_config
@@ -36,6 +41,12 @@ lib_deps =
   fastled/FastLED @ 3.5.0
   bogde/HX711 @ 0.7.5
   adafruit/Adafruit VEML7700 Library @ 1.1.1
+  bakercp/PacketSerial @ 1.4.0
+  nanopb/Nanopb @ 0.4.6   ; Ideally this would reference the nanopb submodule, but that would require
+                          ; everyone to check out submodules to just compile, so we use the library
+                          ; registry for the runtime. The submodule is available for manually updating
+                          ; the pre-compiled (checked in) .pb.h/c files when proto files change, but is
+                          ; otherwise not used during application firmware compilation.
 
 build_flags =
   ${base_config.build_flags}

+ 34 - 0
proto/generate_protobuf.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+
+import os
+import shutil
+import subprocess
+import sys
+
+def run():
+    SCRIPT_PATH = Path(__file__).absolute().parent
+    REPO_ROOT = SCRIPT_PATH.parent
+
+    proto_path = REPO_ROOT / 'proto'
+
+    nanopb_path = REPO_ROOT / 'thirdparty' / 'nanopb'
+
+    # Make sure nanopb submodule is available
+    if not os.path.isdir(nanopb_path):
+        print(f'Nanopb checkout not found! Make sure you have inited/updated the submodule located at {nanopb_path}', file=sys.stderr)
+        exit(1)
+
+    nanopb_generator_path = nanopb_path / 'generator' / 'nanopb_generator.py'
+    c_generated_output_path = REPO_ROOT / 'firmware' / 'src' / 'proto_gen'
+
+    proto_files = [f for f in os.listdir(proto_path) if f.endswith('.proto')]
+    assert len(proto_files) > 0, 'No proto files found!'
+
+    # Generate C files via nanopb
+    subprocess.check_call(['python3', nanopb_generator_path, '-D', c_generated_output_path] + proto_files, cwd=proto_path)
+
+
+if __name__ == '__main__':
+    run()

+ 55 - 0
proto/smartknob.proto

@@ -0,0 +1,55 @@
+syntax = "proto3";
+
+import "nanopb.proto";
+
+package PB;
+
+/*
+ * Message FROM the SmartKnob to USB host
+ */
+message FromSmartKnob {
+    oneof payload {
+        Ack ack = 1;
+        Log log = 2;
+        SmartKnobState smartknob_state = 3;
+    }
+}
+
+message Ack {
+    uint32 nonce = 1;
+}
+
+message Log {
+    string msg = 1 [(nanopb).max_length = 255];
+}
+
+message SmartKnobState {
+    int32 current_position = 1;
+    float sub_position_unit = 2;
+    SmartKnobConfig config = 3;
+}
+
+/*
+ * Message TO the Smartknob from the USB host
+ */
+message ToSmartknob {
+    uint32 nonce = 1;
+
+    oneof payload {
+        RequestState request_state = 2;
+        SmartKnobConfig smartknob_config = 3;
+    }
+}
+
+message SmartKnobConfig {
+    int32 num_positions = 1;
+    int32 position = 2;
+    float position_width_radians = 3;
+    float detent_strength_unit = 4;
+    float endstop_strength_unit = 5;
+    float snap_point = 6;
+    string text = 7 [(nanopb).max_length = 50];
+}
+
+message RequestState {}
+

+ 2 - 0
software/js/.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+dist

+ 1 - 0
software/js/.npmrc

@@ -0,0 +1 @@
+engine-strict=true

+ 21 - 0
software/js/README.md

@@ -0,0 +1,21 @@
+# Typescript SmartKnob protobuf interface library
+
+### Requirements (nvm is recommended)
+
+- node >= 18.11.0
+- npm >= 8.19.2
+
+### Setup
+
+```
+npm ci
+npm run build
+```
+
+### Example
+
+Connect the SmartKnob via USB, then run the example:
+
+```
+npm run example
+```

+ 4315 - 0
software/js/package-lock.json

@@ -0,0 +1,4315 @@
+{
+    "name": "root",
+    "version": "0.1.0",
+    "lockfileVersion": 2,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "root",
+            "version": "0.1.0",
+            "license": "Apache-2.0",
+            "workspaces": [
+                "packages/smartknobjs-proto",
+                "packages/smartknobjs",
+                "packages/example"
+            ],
+            "devDependencies": {
+                "eslint-config-prettier": "^8.5.0"
+            }
+        },
+        "node_modules/@cspotcode/source-map-support": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@eslint/eslintrc": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
+            "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
+            "dev": true,
+            "dependencies": {
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.4.0",
+                "globals": "^13.15.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/@eslint/eslintrc/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true
+        },
+        "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/@humanwhocodes/config-array": {
+            "version": "0.10.7",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
+            "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
+            "dev": true,
+            "dependencies": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.4"
+            },
+            "engines": {
+                "node": ">=10.10.0"
+            }
+        },
+        "node_modules/@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "dev": true,
+            "engines": {
+                "node": ">=12.22"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/nzakas"
+            }
+        },
+        "node_modules/@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+            "dev": true
+        },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+            "dev": true,
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+            "dev": true
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
+        "node_modules/@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "dev": true,
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@protobufjs/aspromise": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+            "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+        },
+        "node_modules/@protobufjs/base64": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+            "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+        },
+        "node_modules/@protobufjs/codegen": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+            "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+        },
+        "node_modules/@protobufjs/eventemitter": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+            "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+        },
+        "node_modules/@protobufjs/fetch": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+            "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+            "dependencies": {
+                "@protobufjs/aspromise": "^1.1.1",
+                "@protobufjs/inquire": "^1.1.0"
+            }
+        },
+        "node_modules/@protobufjs/float": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+            "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+        },
+        "node_modules/@protobufjs/inquire": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+            "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+        },
+        "node_modules/@protobufjs/path": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+            "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+        },
+        "node_modules/@protobufjs/pool": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+            "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+        },
+        "node_modules/@protobufjs/utf8": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+            "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+        },
+        "node_modules/@serialport/binding-abstract": {
+            "version": "9.2.3",
+            "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.2.3.tgz",
+            "integrity": "sha512-cQs9tbIlG3P0IrOWyVirqlhWuJ7Ms2Zh9m2108z6Y5UW/iVj6wEOiW8EmK9QX9jmJXYllE7wgGgvVozP5oCj3w==",
+            "dependencies": {
+                "debug": "^4.3.2"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/binding-mock": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-9.2.4.tgz",
+            "integrity": "sha512-dpEhACCs44oQhh6ajJfJdvQdK38Vq0N4W6iD/gdplglDCK7qXRQCMUjJIeKdS/HSEiWkC3bwumUhUufdsOyT4g==",
+            "dependencies": {
+                "@serialport/binding-abstract": "9.2.3",
+                "debug": "^4.3.2"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/bindings": {
+            "version": "9.2.8",
+            "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-9.2.8.tgz",
+            "integrity": "sha512-hSLxTe0tADZ3LMMGwvEJWOC/TaFQTyPeFalUCsJ1lSQ0k6bPF04JwrtB/C81GetmDBTNRY0GlD0SNtKCc7Dr5g==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "@serialport/binding-abstract": "9.2.3",
+                "@serialport/parser-readline": "9.2.4",
+                "bindings": "^1.5.0",
+                "debug": "^4.3.2",
+                "nan": "^2.15.0",
+                "prebuild-install": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-byte-length": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.2.4.tgz",
+            "integrity": "sha512-sQD/iw4ZMU3xW9PLi0/GlvU6Y623jGeWecbMkO7izUo/6P7gtfv1c9ikd5h0kwL8AoAOpQA1lxdHIKox+umBUg==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-cctalk": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-9.2.4.tgz",
+            "integrity": "sha512-T4TU5vQMwmo9AB3gQLFDWbfJXlW5jd9guEsB/nqKjFHTv0FXPdZ7DQ2TpSp8RnHWxU3GX6kYTaDO20BKzc8GPQ==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-delimiter": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-9.2.4.tgz",
+            "integrity": "sha512-4nvTAoYAgkxFiXrkI+3CA49Yd43CODjeszh89EK+I9c8wOZ+etZduRCzINYPiy26g7zO+GRAb9FoPCsY+sYcbQ==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-inter-byte-timeout": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-9.2.4.tgz",
+            "integrity": "sha512-SOAdvr0oBQIOCXX198hiTlxs4JTKg9j5piapw5tNq52fwDOWdbYrFneT/wN04UTMKaDrJuEvXq6T4rv4j7nJ5A==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-readline": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz",
+            "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==",
+            "dependencies": {
+                "@serialport/parser-delimiter": "9.2.4"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-ready": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-9.2.4.tgz",
+            "integrity": "sha512-Pyi94Itjl6qAURwIZr/gmZpMAyTmKXThm6vL5DoAWGQjcRHWB0gwv2TY2v7N+mQLJYUKU3cMnvnATXxHm7xjxw==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/parser-regex": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-9.2.4.tgz",
+            "integrity": "sha512-sI/cVvPOYz+Dbv4ZdnW16qAwvXiFf/1pGASQdbveRTlgJDdz7sRNlCBifzfTN2xljwvCTZYqiudKvDdC1TepRQ==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@serialport/stream": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-9.2.4.tgz",
+            "integrity": "sha512-bLye8Ub4vUFQGmkh8qEqehr7SE7EJs2yDs0h9jzuL5oKi+F34CFmWkEErO8GAOQ8YNn7p6b3GxUgs+0BrHHDZQ==",
+            "dependencies": {
+                "debug": "^4.3.2"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/@tsconfig/node10": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node12": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node14": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node16": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
+            "dev": true
+        },
+        "node_modules/@types/json-schema": {
+            "version": "7.0.11",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+            "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+            "dev": true
+        },
+        "node_modules/@types/long": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+            "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
+        },
+        "node_modules/@types/node": {
+            "version": "18.11.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
+            "integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
+        },
+        "node_modules/@types/semver": {
+            "version": "7.3.12",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz",
+            "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==",
+            "dev": true
+        },
+        "node_modules/@types/serialport": {
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/@types/serialport/-/serialport-8.0.2.tgz",
+            "integrity": "sha512-z4b1I8/vdZE3upgCcAL9VAWlVVFUVn5uo3faAHavkVfK/Hb1LUxKwp9YCtA5AZqEUCWoSWl20SRTOvAI/5fQWQ==",
+            "dev": true,
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@typescript-eslint/eslint-plugin": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.1.tgz",
+            "integrity": "sha512-FsWboKkWdytGiXT5O1/R9j37YgcjO8MKHSUmWnIEjVaz0krHkplPnYi7mwdb+5+cs0toFNQb0HIrN7zONdIEWg==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/type-utils": "5.40.1",
+                "@typescript-eslint/utils": "5.40.1",
+                "debug": "^4.3.4",
+                "ignore": "^5.2.0",
+                "regexpp": "^3.2.0",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "@typescript-eslint/parser": "^5.0.0",
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/eslint-plugin/node_modules/tsutils": {
+            "version": "3.21.0",
+            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+            "dev": true,
+            "dependencies": {
+                "tslib": "^1.8.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            },
+            "peerDependencies": {
+                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+            }
+        },
+        "node_modules/@typescript-eslint/parser": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.1.tgz",
+            "integrity": "sha512-IK6x55va5w4YvXd4b3VrXQPldV9vQTxi5ov+g4pMANsXPTXOcfjx08CRR1Dfrcc51syPtXHF5bgLlMHYFrvQtg==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "debug": "^4.3.4"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/scope-manager": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.1.tgz",
+            "integrity": "sha512-jkn4xsJiUQucI16OLCXrLRXDZ3afKhOIqXs4R3O+M00hdQLKR58WuyXPZZjhKLFCEP2g+TXdBRtLQ33UfAdRUg==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/visitor-keys": "5.40.1"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/type-utils": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.1.tgz",
+            "integrity": "sha512-DLAs+AHQOe6n5LRraXiv27IYPhleF0ldEmx6yBqBgBLaNRKTkffhV1RPsjoJBhVup2zHxfaRtan8/YRBgYhU9Q==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "@typescript-eslint/utils": "5.40.1",
+                "debug": "^4.3.4",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "*"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/type-utils/node_modules/tsutils": {
+            "version": "3.21.0",
+            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+            "dev": true,
+            "dependencies": {
+                "tslib": "^1.8.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            },
+            "peerDependencies": {
+                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+            }
+        },
+        "node_modules/@typescript-eslint/types": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.1.tgz",
+            "integrity": "sha512-Icg9kiuVJSwdzSQvtdGspOlWNjVDnF3qVIKXdJ103o36yRprdl3Ge5cABQx+csx960nuMF21v8qvO31v9t3OHw==",
+            "dev": true,
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.1.tgz",
+            "integrity": "sha512-5QTP/nW5+60jBcEPfXy/EZL01qrl9GZtbgDZtDPlfW5zj/zjNrdI2B5zMUHmOsfvOr2cWqwVdWjobCiHcedmQA==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/visitor-keys": "5.40.1",
+                "debug": "^4.3.4",
+                "globby": "^11.1.0",
+                "is-glob": "^4.0.3",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/tsutils": {
+            "version": "3.21.0",
+            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+            "dev": true,
+            "dependencies": {
+                "tslib": "^1.8.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            },
+            "peerDependencies": {
+                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+            }
+        },
+        "node_modules/@typescript-eslint/utils": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.1.tgz",
+            "integrity": "sha512-a2TAVScoX9fjryNrW6BZRnreDUszxqm9eQ9Esv8n5nXApMW0zeANUYlwh/DED04SC/ifuBvXgZpIK5xeJHQ3aw==",
+            "dev": true,
+            "dependencies": {
+                "@types/json-schema": "^7.0.9",
+                "@types/semver": "^7.3.12",
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "eslint-scope": "^5.1.1",
+                "eslint-utils": "^3.0.0",
+                "semver": "^7.3.7"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            }
+        },
+        "node_modules/@typescript-eslint/visitor-keys": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.1.tgz",
+            "integrity": "sha512-A2DGmeZ+FMja0geX5rww+DpvILpwo1OsiQs0M+joPWJYsiEFBLsH0y1oFymPNul6Z5okSmHpP4ivkc2N0Cgfkw==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.40.1",
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/acorn": {
+            "version": "8.8.0",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
+            "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+            "dev": true,
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/acorn-jsx": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+            "dev": true,
+            "peerDependencies": {
+                "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            }
+        },
+        "node_modules/acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/ajv": {
+            "version": "6.12.6",
+            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+            "dev": true,
+            "dependencies": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/epoberezkin"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+            "dev": true
+        },
+        "node_modules/array-union": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+            "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
+        "node_modules/base64-js": {
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+            "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/bindings": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+            "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+            "dependencies": {
+                "file-uri-to-path": "1.0.0"
+            }
+        },
+        "node_modules/bl": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+            "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+            "dependencies": {
+                "buffer": "^5.5.0",
+                "inherits": "^2.0.4",
+                "readable-stream": "^3.4.0"
+            }
+        },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/buffer": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+            "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "base64-js": "^1.3.1",
+                "ieee754": "^1.1.13"
+            }
+        },
+        "node_modules/callsites": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/chownr": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+            "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+        },
+        "node_modules/cobs": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/cobs/-/cobs-0.2.1.tgz",
+            "integrity": "sha512-rf2gUd1MS8AoQQG1xpqlXk3wuoIWAwx//GxloBOf8sj1hN15KEjmP/OkC6lyKTx/J0CrULKM3xzlfj9gq3auAQ=="
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+            "dev": true
+        },
+        "node_modules/crc-32": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+            "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+            "bin": {
+                "crc32": "bin/crc32.njs"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+            "dev": true
+        },
+        "node_modules/cross-spawn": {
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+            "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+            "dev": true,
+            "dependencies": {
+                "path-key": "^3.1.0",
+                "shebang-command": "^2.0.0",
+                "which": "^2.0.1"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/debug": {
+            "version": "4.3.4",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+            "dependencies": {
+                "ms": "2.1.2"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/decompress-response": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+            "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+            "dependencies": {
+                "mimic-response": "^3.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/deep-extend": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+            "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
+        "node_modules/deep-is": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+            "dev": true
+        },
+        "node_modules/detect-libc": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+            "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.3.1"
+            }
+        },
+        "node_modules/dir-glob": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+            "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "dev": true,
+            "dependencies": {
+                "path-type": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/doctrine": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+            "dev": true,
+            "dependencies": {
+                "esutils": "^2.0.2"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/end-of-stream": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+            "dependencies": {
+                "once": "^1.4.0"
+            }
+        },
+        "node_modules/eslint": {
+            "version": "8.25.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
+            "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
+            "dev": true,
+            "dependencies": {
+                "@eslint/eslintrc": "^1.3.3",
+                "@humanwhocodes/config-array": "^0.10.5",
+                "@humanwhocodes/module-importer": "^1.0.1",
+                "ajv": "^6.10.0",
+                "chalk": "^4.0.0",
+                "cross-spawn": "^7.0.2",
+                "debug": "^4.3.2",
+                "doctrine": "^3.0.0",
+                "escape-string-regexp": "^4.0.0",
+                "eslint-scope": "^7.1.1",
+                "eslint-utils": "^3.0.0",
+                "eslint-visitor-keys": "^3.3.0",
+                "espree": "^9.4.0",
+                "esquery": "^1.4.0",
+                "esutils": "^2.0.2",
+                "fast-deep-equal": "^3.1.3",
+                "file-entry-cache": "^6.0.1",
+                "find-up": "^5.0.0",
+                "glob-parent": "^6.0.1",
+                "globals": "^13.15.0",
+                "globby": "^11.1.0",
+                "grapheme-splitter": "^1.0.4",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.0.0",
+                "imurmurhash": "^0.1.4",
+                "is-glob": "^4.0.0",
+                "js-sdsl": "^4.1.4",
+                "js-yaml": "^4.1.0",
+                "json-stable-stringify-without-jsonify": "^1.0.1",
+                "levn": "^0.4.1",
+                "lodash.merge": "^4.6.2",
+                "minimatch": "^3.1.2",
+                "natural-compare": "^1.4.0",
+                "optionator": "^0.9.1",
+                "regexpp": "^3.2.0",
+                "strip-ansi": "^6.0.1",
+                "strip-json-comments": "^3.1.0",
+                "text-table": "^0.2.0"
+            },
+            "bin": {
+                "eslint": "bin/eslint.js"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint-config-prettier": {
+            "version": "8.5.0",
+            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
+            "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
+            "dev": true,
+            "bin": {
+                "eslint-config-prettier": "bin/cli.js"
+            },
+            "peerDependencies": {
+                "eslint": ">=7.0.0"
+            }
+        },
+        "node_modules/eslint-scope": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+            "dev": true,
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^4.1.1"
+            },
+            "engines": {
+                "node": ">=8.0.0"
+            }
+        },
+        "node_modules/eslint-utils": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+            "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+            "dev": true,
+            "dependencies": {
+                "eslint-visitor-keys": "^2.0.0"
+            },
+            "engines": {
+                "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mysticatea"
+            },
+            "peerDependencies": {
+                "eslint": ">=5"
+            }
+        },
+        "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+            "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/eslint-visitor-keys": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+            "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+            "dev": true,
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/eslint/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/eslint/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true
+        },
+        "node_modules/eslint/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/eslint/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/eslint/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/eslint/node_modules/escape-string-regexp": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+            "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint/node_modules/eslint-scope": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+            "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+            "dev": true,
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/eslint/node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/eslint/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/eslint/node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/eslint/node_modules/strip-json-comments": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dev": true,
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/espree": {
+            "version": "9.4.0",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
+            "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+            "dev": true,
+            "dependencies": {
+                "acorn": "^8.8.0",
+                "acorn-jsx": "^5.3.2",
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/esquery": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+            "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+            "dev": true,
+            "dependencies": {
+                "estraverse": "^5.1.0"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/esquery/node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/esrecurse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+            "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+            "dev": true,
+            "dependencies": {
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/esrecurse/node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/estraverse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/esutils": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/example": {
+            "resolved": "packages/example",
+            "link": true
+        },
+        "node_modules/expand-template": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+            "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "dev": true
+        },
+        "node_modules/fast-glob": {
+            "version": "3.2.12",
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+            "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.4"
+            },
+            "engines": {
+                "node": ">=8.6.0"
+            }
+        },
+        "node_modules/fast-glob/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fast-json-stable-stringify": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+            "dev": true
+        },
+        "node_modules/fast-levenshtein": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+            "dev": true
+        },
+        "node_modules/fastq": {
+            "version": "1.13.0",
+            "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+            "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+            "dev": true,
+            "dependencies": {
+                "reusify": "^1.0.4"
+            }
+        },
+        "node_modules/file-entry-cache": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+            "dev": true,
+            "dependencies": {
+                "flat-cache": "^3.0.4"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/file-uri-to-path": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+            "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+        },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/find-up": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+            "dev": true,
+            "dependencies": {
+                "locate-path": "^6.0.0",
+                "path-exists": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/flat-cache": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+            "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+            "dev": true,
+            "dependencies": {
+                "flatted": "^3.1.0",
+                "rimraf": "^3.0.2"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/flatted": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+            "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+            "dev": true
+        },
+        "node_modules/fs-constants": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+        },
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+            "dev": true
+        },
+        "node_modules/github-from-package": {
+            "version": "0.0.0",
+            "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+            "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+        },
+        "node_modules/glob": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "dev": true,
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dev": true,
+            "dependencies": {
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
+        "node_modules/globals": {
+            "version": "13.17.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
+            "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
+            "dev": true,
+            "dependencies": {
+                "type-fest": "^0.20.2"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/globby": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+            "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "dev": true,
+            "dependencies": {
+                "array-union": "^2.1.0",
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.9",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/grapheme-splitter": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+            "dev": true
+        },
+        "node_modules/ieee754": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+            "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/ignore": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+            "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+            "dev": true,
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/import-fresh": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+            "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+            "dev": true,
+            "dependencies": {
+                "parent-module": "^1.0.0",
+                "resolve-from": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.8.19"
+            }
+        },
+        "node_modules/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "dev": true,
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+        },
+        "node_modules/ini": {
+            "version": "1.3.8",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/isexe": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+            "dev": true
+        },
+        "node_modules/js-sdsl": {
+            "version": "4.1.5",
+            "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
+            "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
+            "dev": true
+        },
+        "node_modules/json-schema-traverse": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+            "dev": true
+        },
+        "node_modules/json-stable-stringify-without-jsonify": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+            "dev": true
+        },
+        "node_modules/levn": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+            "dev": true,
+            "dependencies": {
+                "prelude-ls": "^1.2.1",
+                "type-check": "~0.4.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+            "dev": true,
+            "dependencies": {
+                "p-locate": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/lodash.merge": {
+            "version": "4.6.2",
+            "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+            "dev": true
+        },
+        "node_modules/long": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+            "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true
+        },
+        "node_modules/merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/micromatch": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+            "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+            "dev": true,
+            "dependencies": {
+                "braces": "^3.0.2",
+                "picomatch": "^2.3.1"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
+        "node_modules/mimic-response": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+            "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+            "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/mkdirp-classic": {
+            "version": "0.5.3",
+            "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+            "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+        },
+        "node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node_modules/nan": {
+            "version": "2.17.0",
+            "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
+            "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ=="
+        },
+        "node_modules/napi-build-utils": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+            "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+        },
+        "node_modules/natural-compare": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+            "dev": true
+        },
+        "node_modules/node-abi": {
+            "version": "3.26.0",
+            "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.26.0.tgz",
+            "integrity": "sha512-jRVtMFTChbi2i/jqo/i2iP9634KMe+7K1v35mIdj3Mn59i5q27ZYhn+sW6npISM/PQg7HrP2kwtRBMmh5Uvzdg==",
+            "dependencies": {
+                "semver": "^7.3.5"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/optionator": {
+            "version": "0.9.1",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+            "dev": true,
+            "dependencies": {
+                "deep-is": "^0.1.3",
+                "fast-levenshtein": "^2.0.6",
+                "levn": "^0.4.1",
+                "prelude-ls": "^1.2.1",
+                "type-check": "^0.4.0",
+                "word-wrap": "^1.2.3"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+            "dev": true,
+            "dependencies": {
+                "yocto-queue": "^0.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+            "dev": true,
+            "dependencies": {
+                "p-limit": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/parent-module": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+            "dev": true,
+            "dependencies": {
+                "callsites": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/path-exists": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/path-key": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-type": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+            "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/prebuild-install": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+            "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+            "dependencies": {
+                "detect-libc": "^2.0.0",
+                "expand-template": "^2.0.3",
+                "github-from-package": "0.0.0",
+                "minimist": "^1.2.3",
+                "mkdirp-classic": "^0.5.3",
+                "napi-build-utils": "^1.0.1",
+                "node-abi": "^3.3.0",
+                "pump": "^3.0.0",
+                "rc": "^1.2.7",
+                "simple-get": "^4.0.0",
+                "tar-fs": "^2.0.0",
+                "tunnel-agent": "^0.6.0"
+            },
+            "bin": {
+                "prebuild-install": "bin.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/prelude-ls": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/prettier": {
+            "version": "2.7.1",
+            "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
+            "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
+            "dev": true,
+            "bin": {
+                "prettier": "bin-prettier.js"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            },
+            "funding": {
+                "url": "https://github.com/prettier/prettier?sponsor=1"
+            }
+        },
+        "node_modules/protobufjs": {
+            "version": "6.11.3",
+            "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+            "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "@protobufjs/aspromise": "^1.1.2",
+                "@protobufjs/base64": "^1.1.2",
+                "@protobufjs/codegen": "^2.0.4",
+                "@protobufjs/eventemitter": "^1.1.0",
+                "@protobufjs/fetch": "^1.1.0",
+                "@protobufjs/float": "^1.0.2",
+                "@protobufjs/inquire": "^1.1.0",
+                "@protobufjs/path": "^1.1.2",
+                "@protobufjs/pool": "^1.1.0",
+                "@protobufjs/utf8": "^1.1.0",
+                "@types/long": "^4.0.1",
+                "@types/node": ">=13.7.0",
+                "long": "^4.0.0"
+            },
+            "bin": {
+                "pbjs": "bin/pbjs",
+                "pbts": "bin/pbts"
+            }
+        },
+        "node_modules/pump": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+            "dependencies": {
+                "end-of-stream": "^1.1.0",
+                "once": "^1.3.1"
+            }
+        },
+        "node_modules/punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/rc": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+            "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+            "dependencies": {
+                "deep-extend": "^0.6.0",
+                "ini": "~1.3.0",
+                "minimist": "^1.2.0",
+                "strip-json-comments": "~2.0.1"
+            },
+            "bin": {
+                "rc": "cli.js"
+            }
+        },
+        "node_modules/readable-stream": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/regexpp": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+            "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mysticatea"
+            }
+        },
+        "node_modules/resolve-from": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+            "dev": true,
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/reusify": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+            "dev": true,
+            "engines": {
+                "iojs": ">=1.0.0",
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dev": true,
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "queue-microtask": "^1.2.2"
+            }
+        },
+        "node_modules/safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/semver": {
+            "version": "7.3.8",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+            "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/serialport": {
+            "version": "9.2.8",
+            "resolved": "https://registry.npmjs.org/serialport/-/serialport-9.2.8.tgz",
+            "integrity": "sha512-FsWpMQgSJxi93JgWl5xM1f9/Z8IjRJuaUEoHqLf8FPBLw7gMhInuHOBhI2onQufWIYPGTz3H3oGcu1nCaK1EfA==",
+            "dependencies": {
+                "@serialport/binding-mock": "9.2.4",
+                "@serialport/bindings": "9.2.8",
+                "@serialport/parser-byte-length": "9.2.4",
+                "@serialport/parser-cctalk": "9.2.4",
+                "@serialport/parser-delimiter": "9.2.4",
+                "@serialport/parser-inter-byte-timeout": "9.2.4",
+                "@serialport/parser-readline": "9.2.4",
+                "@serialport/parser-ready": "9.2.4",
+                "@serialport/parser-regex": "9.2.4",
+                "@serialport/stream": "9.2.4",
+                "debug": "^4.3.2"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/serialport/donate"
+            }
+        },
+        "node_modules/shebang-command": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+            "dev": true,
+            "dependencies": {
+                "shebang-regex": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/shebang-regex": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/simple-concat": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+            "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/simple-get": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+            "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "decompress-response": "^6.0.0",
+                "once": "^1.3.1",
+                "simple-concat": "^1.0.0"
+            }
+        },
+        "node_modules/slash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+            "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/smartknobjs": {
+            "resolved": "packages/smartknobjs",
+            "link": true
+        },
+        "node_modules/smartknobjs-proto": {
+            "resolved": "packages/smartknobjs-proto",
+            "link": true
+        },
+        "node_modules/string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "dependencies": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-json-comments": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+            "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/tar-fs": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+            "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+            "dependencies": {
+                "chownr": "^1.1.1",
+                "mkdirp-classic": "^0.5.2",
+                "pump": "^3.0.0",
+                "tar-stream": "^2.1.4"
+            }
+        },
+        "node_modules/tar-stream": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+            "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+            "dependencies": {
+                "bl": "^4.0.3",
+                "end-of-stream": "^1.4.1",
+                "fs-constants": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.1.1"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/text-table": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/ts-node": {
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+            "dev": true,
+            "dependencies": {
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1",
+                "yn": "3.1.1"
+            },
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js",
+                "ts-script": "dist/bin-script-deprecated.js"
+            },
+            "peerDependencies": {
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=2.7"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/tslib": {
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+            "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+            "dev": true
+        },
+        "node_modules/tunnel-agent": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+            "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+            "dependencies": {
+                "safe-buffer": "^5.0.1"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/type-check": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+            "dev": true,
+            "dependencies": {
+                "prelude-ls": "^1.2.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "4.8.4",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+            "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+            "dev": true,
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=4.2.0"
+            }
+        },
+        "node_modules/uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dev": true,
+            "dependencies": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+        },
+        "node_modules/v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+            "dev": true
+        },
+        "node_modules/which": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+            "dev": true,
+            "dependencies": {
+                "isexe": "^2.0.0"
+            },
+            "bin": {
+                "node-which": "bin/node-which"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/word-wrap": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "packages/example": {
+            "version": "0.1.0",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "serialport": "^9.2.4",
+                "smartknobjs": "^0.1.0"
+            },
+            "devDependencies": {
+                "@types/serialport": "^8.0.2",
+                "@typescript-eslint/eslint-plugin": "^5.40.1",
+                "@typescript-eslint/parser": "^5.40.1",
+                "eslint": "^8.25.0",
+                "prettier": "^2.4.1",
+                "ts-node": "^10.2.1",
+                "typescript": "^4.8.4"
+            }
+        },
+        "packages/smartknobjs": {
+            "version": "0.1.0",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "cobs": "^0.2.1",
+                "crc-32": "^1.2.0",
+                "serialport": "^9.2.4",
+                "smartknobjs-proto": "^0.1.0"
+            },
+            "devDependencies": {
+                "@types/serialport": "^8.0.2",
+                "@typescript-eslint/eslint-plugin": "^5.40.1",
+                "@typescript-eslint/parser": "^5.40.1",
+                "eslint": "^8.25.0",
+                "prettier": "^2.4.1",
+                "ts-node": "^10.2.1",
+                "typescript": "^4.8.4"
+            }
+        },
+        "packages/smartknobjs-proto": {
+            "version": "0.1.0",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "protobufjs": "^6.11.2"
+            }
+        },
+        "smartknobjs": {
+            "version": "0.1.0",
+            "extraneous": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "cobs": "^0.2.1",
+                "crc-32": "^1.2.0",
+                "serialport": "^9.2.4",
+                "smartknobjs-proto": "^0.1.0"
+            },
+            "devDependencies": {
+                "@types/serialport": "^8.0.2",
+                "@typescript-eslint/eslint-plugin": "^5.40.1",
+                "@typescript-eslint/parser": "^5.40.1",
+                "eslint": "^8.25.0",
+                "prettier": "^2.4.1",
+                "ts-node": "^10.2.1",
+                "typescript": "^4.8.4"
+            }
+        },
+        "smartknobjs-proto": {
+            "version": "0.1.0",
+            "extraneous": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "protobufjs": "^6.11.2"
+            }
+        }
+    },
+    "dependencies": {
+        "@cspotcode/source-map-support": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "dev": true,
+            "requires": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            }
+        },
+        "@eslint/eslintrc": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
+            "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
+            "dev": true,
+            "requires": {
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.4.0",
+                "globals": "^13.15.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
+            },
+            "dependencies": {
+                "argparse": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+                    "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+                    "dev": true
+                },
+                "js-yaml": {
+                    "version": "4.1.0",
+                    "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+                    "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+                    "dev": true,
+                    "requires": {
+                        "argparse": "^2.0.1"
+                    }
+                },
+                "strip-json-comments": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+                    "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+                    "dev": true
+                }
+            }
+        },
+        "@humanwhocodes/config-array": {
+            "version": "0.10.7",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
+            "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
+            "dev": true,
+            "requires": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.4"
+            }
+        },
+        "@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "dev": true
+        },
+        "@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+            "dev": true
+        },
+        "@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+            "dev": true
+        },
+        "@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+            "dev": true
+        },
+        "@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dev": true,
+            "requires": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
+        "@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dev": true,
+            "requires": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            }
+        },
+        "@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "dev": true
+        },
+        "@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "dev": true,
+            "requires": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
+            }
+        },
+        "@protobufjs/aspromise": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+            "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+        },
+        "@protobufjs/base64": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+            "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+        },
+        "@protobufjs/codegen": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+            "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+        },
+        "@protobufjs/eventemitter": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+            "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+        },
+        "@protobufjs/fetch": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+            "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+            "requires": {
+                "@protobufjs/aspromise": "^1.1.1",
+                "@protobufjs/inquire": "^1.1.0"
+            }
+        },
+        "@protobufjs/float": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+            "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+        },
+        "@protobufjs/inquire": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+            "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+        },
+        "@protobufjs/path": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+            "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+        },
+        "@protobufjs/pool": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+            "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+        },
+        "@protobufjs/utf8": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+            "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+        },
+        "@serialport/binding-abstract": {
+            "version": "9.2.3",
+            "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.2.3.tgz",
+            "integrity": "sha512-cQs9tbIlG3P0IrOWyVirqlhWuJ7Ms2Zh9m2108z6Y5UW/iVj6wEOiW8EmK9QX9jmJXYllE7wgGgvVozP5oCj3w==",
+            "requires": {
+                "debug": "^4.3.2"
+            }
+        },
+        "@serialport/binding-mock": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-9.2.4.tgz",
+            "integrity": "sha512-dpEhACCs44oQhh6ajJfJdvQdK38Vq0N4W6iD/gdplglDCK7qXRQCMUjJIeKdS/HSEiWkC3bwumUhUufdsOyT4g==",
+            "requires": {
+                "@serialport/binding-abstract": "9.2.3",
+                "debug": "^4.3.2"
+            }
+        },
+        "@serialport/bindings": {
+            "version": "9.2.8",
+            "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-9.2.8.tgz",
+            "integrity": "sha512-hSLxTe0tADZ3LMMGwvEJWOC/TaFQTyPeFalUCsJ1lSQ0k6bPF04JwrtB/C81GetmDBTNRY0GlD0SNtKCc7Dr5g==",
+            "requires": {
+                "@serialport/binding-abstract": "9.2.3",
+                "@serialport/parser-readline": "9.2.4",
+                "bindings": "^1.5.0",
+                "debug": "^4.3.2",
+                "nan": "^2.15.0",
+                "prebuild-install": "^7.0.0"
+            }
+        },
+        "@serialport/parser-byte-length": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.2.4.tgz",
+            "integrity": "sha512-sQD/iw4ZMU3xW9PLi0/GlvU6Y623jGeWecbMkO7izUo/6P7gtfv1c9ikd5h0kwL8AoAOpQA1lxdHIKox+umBUg=="
+        },
+        "@serialport/parser-cctalk": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-9.2.4.tgz",
+            "integrity": "sha512-T4TU5vQMwmo9AB3gQLFDWbfJXlW5jd9guEsB/nqKjFHTv0FXPdZ7DQ2TpSp8RnHWxU3GX6kYTaDO20BKzc8GPQ=="
+        },
+        "@serialport/parser-delimiter": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-9.2.4.tgz",
+            "integrity": "sha512-4nvTAoYAgkxFiXrkI+3CA49Yd43CODjeszh89EK+I9c8wOZ+etZduRCzINYPiy26g7zO+GRAb9FoPCsY+sYcbQ=="
+        },
+        "@serialport/parser-inter-byte-timeout": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-9.2.4.tgz",
+            "integrity": "sha512-SOAdvr0oBQIOCXX198hiTlxs4JTKg9j5piapw5tNq52fwDOWdbYrFneT/wN04UTMKaDrJuEvXq6T4rv4j7nJ5A=="
+        },
+        "@serialport/parser-readline": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz",
+            "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==",
+            "requires": {
+                "@serialport/parser-delimiter": "9.2.4"
+            }
+        },
+        "@serialport/parser-ready": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-9.2.4.tgz",
+            "integrity": "sha512-Pyi94Itjl6qAURwIZr/gmZpMAyTmKXThm6vL5DoAWGQjcRHWB0gwv2TY2v7N+mQLJYUKU3cMnvnATXxHm7xjxw=="
+        },
+        "@serialport/parser-regex": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-9.2.4.tgz",
+            "integrity": "sha512-sI/cVvPOYz+Dbv4ZdnW16qAwvXiFf/1pGASQdbveRTlgJDdz7sRNlCBifzfTN2xljwvCTZYqiudKvDdC1TepRQ=="
+        },
+        "@serialport/stream": {
+            "version": "9.2.4",
+            "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-9.2.4.tgz",
+            "integrity": "sha512-bLye8Ub4vUFQGmkh8qEqehr7SE7EJs2yDs0h9jzuL5oKi+F34CFmWkEErO8GAOQ8YNn7p6b3GxUgs+0BrHHDZQ==",
+            "requires": {
+                "debug": "^4.3.2"
+            }
+        },
+        "@tsconfig/node10": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+            "dev": true
+        },
+        "@tsconfig/node12": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+            "dev": true
+        },
+        "@tsconfig/node14": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+            "dev": true
+        },
+        "@tsconfig/node16": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
+            "dev": true
+        },
+        "@types/json-schema": {
+            "version": "7.0.11",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+            "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+            "dev": true
+        },
+        "@types/long": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+            "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
+        },
+        "@types/node": {
+            "version": "18.11.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
+            "integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
+        },
+        "@types/semver": {
+            "version": "7.3.12",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz",
+            "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==",
+            "dev": true
+        },
+        "@types/serialport": {
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/@types/serialport/-/serialport-8.0.2.tgz",
+            "integrity": "sha512-z4b1I8/vdZE3upgCcAL9VAWlVVFUVn5uo3faAHavkVfK/Hb1LUxKwp9YCtA5AZqEUCWoSWl20SRTOvAI/5fQWQ==",
+            "dev": true,
+            "requires": {
+                "@types/node": "*"
+            }
+        },
+        "@typescript-eslint/eslint-plugin": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.1.tgz",
+            "integrity": "sha512-FsWboKkWdytGiXT5O1/R9j37YgcjO8MKHSUmWnIEjVaz0krHkplPnYi7mwdb+5+cs0toFNQb0HIrN7zONdIEWg==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/type-utils": "5.40.1",
+                "@typescript-eslint/utils": "5.40.1",
+                "debug": "^4.3.4",
+                "ignore": "^5.2.0",
+                "regexpp": "^3.2.0",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "dependencies": {
+                "tsutils": {
+                    "version": "3.21.0",
+                    "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+                    "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+                    "dev": true,
+                    "requires": {
+                        "tslib": "^1.8.1"
+                    }
+                }
+            }
+        },
+        "@typescript-eslint/parser": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.1.tgz",
+            "integrity": "sha512-IK6x55va5w4YvXd4b3VrXQPldV9vQTxi5ov+g4pMANsXPTXOcfjx08CRR1Dfrcc51syPtXHF5bgLlMHYFrvQtg==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "debug": "^4.3.4"
+            }
+        },
+        "@typescript-eslint/scope-manager": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.1.tgz",
+            "integrity": "sha512-jkn4xsJiUQucI16OLCXrLRXDZ3afKhOIqXs4R3O+M00hdQLKR58WuyXPZZjhKLFCEP2g+TXdBRtLQ33UfAdRUg==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/visitor-keys": "5.40.1"
+            }
+        },
+        "@typescript-eslint/type-utils": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.1.tgz",
+            "integrity": "sha512-DLAs+AHQOe6n5LRraXiv27IYPhleF0ldEmx6yBqBgBLaNRKTkffhV1RPsjoJBhVup2zHxfaRtan8/YRBgYhU9Q==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "@typescript-eslint/utils": "5.40.1",
+                "debug": "^4.3.4",
+                "tsutils": "^3.21.0"
+            },
+            "dependencies": {
+                "tsutils": {
+                    "version": "3.21.0",
+                    "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+                    "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+                    "dev": true,
+                    "requires": {
+                        "tslib": "^1.8.1"
+                    }
+                }
+            }
+        },
+        "@typescript-eslint/types": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.1.tgz",
+            "integrity": "sha512-Icg9kiuVJSwdzSQvtdGspOlWNjVDnF3qVIKXdJ103o36yRprdl3Ge5cABQx+csx960nuMF21v8qvO31v9t3OHw==",
+            "dev": true
+        },
+        "@typescript-eslint/typescript-estree": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.1.tgz",
+            "integrity": "sha512-5QTP/nW5+60jBcEPfXy/EZL01qrl9GZtbgDZtDPlfW5zj/zjNrdI2B5zMUHmOsfvOr2cWqwVdWjobCiHcedmQA==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/visitor-keys": "5.40.1",
+                "debug": "^4.3.4",
+                "globby": "^11.1.0",
+                "is-glob": "^4.0.3",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "dependencies": {
+                "tsutils": {
+                    "version": "3.21.0",
+                    "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+                    "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+                    "dev": true,
+                    "requires": {
+                        "tslib": "^1.8.1"
+                    }
+                }
+            }
+        },
+        "@typescript-eslint/utils": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.1.tgz",
+            "integrity": "sha512-a2TAVScoX9fjryNrW6BZRnreDUszxqm9eQ9Esv8n5nXApMW0zeANUYlwh/DED04SC/ifuBvXgZpIK5xeJHQ3aw==",
+            "dev": true,
+            "requires": {
+                "@types/json-schema": "^7.0.9",
+                "@types/semver": "^7.3.12",
+                "@typescript-eslint/scope-manager": "5.40.1",
+                "@typescript-eslint/types": "5.40.1",
+                "@typescript-eslint/typescript-estree": "5.40.1",
+                "eslint-scope": "^5.1.1",
+                "eslint-utils": "^3.0.0",
+                "semver": "^7.3.7"
+            }
+        },
+        "@typescript-eslint/visitor-keys": {
+            "version": "5.40.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.1.tgz",
+            "integrity": "sha512-A2DGmeZ+FMja0geX5rww+DpvILpwo1OsiQs0M+joPWJYsiEFBLsH0y1oFymPNul6Z5okSmHpP4ivkc2N0Cgfkw==",
+            "dev": true,
+            "requires": {
+                "@typescript-eslint/types": "5.40.1",
+                "eslint-visitor-keys": "^3.3.0"
+            }
+        },
+        "acorn": {
+            "version": "8.8.0",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
+            "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+            "dev": true
+        },
+        "acorn-jsx": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+            "dev": true,
+            "requires": {}
+        },
+        "acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "dev": true
+        },
+        "ajv": {
+            "version": "6.12.6",
+            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+            "dev": true,
+            "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+            }
+        },
+        "ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true
+        },
+        "arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+            "dev": true
+        },
+        "array-union": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+            "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+            "dev": true
+        },
+        "balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
+        "base64-js": {
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+            "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+        },
+        "bindings": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+            "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+            "requires": {
+                "file-uri-to-path": "1.0.0"
+            }
+        },
+        "bl": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+            "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+            "requires": {
+                "buffer": "^5.5.0",
+                "inherits": "^2.0.4",
+                "readable-stream": "^3.4.0"
+            }
+        },
+        "brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "requires": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "requires": {
+                "fill-range": "^7.0.1"
+            }
+        },
+        "buffer": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+            "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+            "requires": {
+                "base64-js": "^1.3.1",
+                "ieee754": "^1.1.13"
+            }
+        },
+        "callsites": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+            "dev": true
+        },
+        "chownr": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+            "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+        },
+        "cobs": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/cobs/-/cobs-0.2.1.tgz",
+            "integrity": "sha512-rf2gUd1MS8AoQQG1xpqlXk3wuoIWAwx//GxloBOf8sj1hN15KEjmP/OkC6lyKTx/J0CrULKM3xzlfj9gq3auAQ=="
+        },
+        "concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+            "dev": true
+        },
+        "crc-32": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+            "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
+        },
+        "create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+            "dev": true
+        },
+        "cross-spawn": {
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+            "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+            "dev": true,
+            "requires": {
+                "path-key": "^3.1.0",
+                "shebang-command": "^2.0.0",
+                "which": "^2.0.1"
+            }
+        },
+        "debug": {
+            "version": "4.3.4",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+            "requires": {
+                "ms": "2.1.2"
+            }
+        },
+        "decompress-response": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+            "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+            "requires": {
+                "mimic-response": "^3.1.0"
+            }
+        },
+        "deep-extend": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+            "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+        },
+        "deep-is": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+            "dev": true
+        },
+        "detect-libc": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+            "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="
+        },
+        "diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+            "dev": true
+        },
+        "dir-glob": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+            "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "dev": true,
+            "requires": {
+                "path-type": "^4.0.0"
+            }
+        },
+        "doctrine": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+            "dev": true,
+            "requires": {
+                "esutils": "^2.0.2"
+            }
+        },
+        "end-of-stream": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+            "requires": {
+                "once": "^1.4.0"
+            }
+        },
+        "eslint": {
+            "version": "8.25.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
+            "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
+            "dev": true,
+            "requires": {
+                "@eslint/eslintrc": "^1.3.3",
+                "@humanwhocodes/config-array": "^0.10.5",
+                "@humanwhocodes/module-importer": "^1.0.1",
+                "ajv": "^6.10.0",
+                "chalk": "^4.0.0",
+                "cross-spawn": "^7.0.2",
+                "debug": "^4.3.2",
+                "doctrine": "^3.0.0",
+                "escape-string-regexp": "^4.0.0",
+                "eslint-scope": "^7.1.1",
+                "eslint-utils": "^3.0.0",
+                "eslint-visitor-keys": "^3.3.0",
+                "espree": "^9.4.0",
+                "esquery": "^1.4.0",
+                "esutils": "^2.0.2",
+                "fast-deep-equal": "^3.1.3",
+                "file-entry-cache": "^6.0.1",
+                "find-up": "^5.0.0",
+                "glob-parent": "^6.0.1",
+                "globals": "^13.15.0",
+                "globby": "^11.1.0",
+                "grapheme-splitter": "^1.0.4",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.0.0",
+                "imurmurhash": "^0.1.4",
+                "is-glob": "^4.0.0",
+                "js-sdsl": "^4.1.4",
+                "js-yaml": "^4.1.0",
+                "json-stable-stringify-without-jsonify": "^1.0.1",
+                "levn": "^0.4.1",
+                "lodash.merge": "^4.6.2",
+                "minimatch": "^3.1.2",
+                "natural-compare": "^1.4.0",
+                "optionator": "^0.9.1",
+                "regexpp": "^3.2.0",
+                "strip-ansi": "^6.0.1",
+                "strip-json-comments": "^3.1.0",
+                "text-table": "^0.2.0"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "4.3.0",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^2.0.1"
+                    }
+                },
+                "argparse": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+                    "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+                    "dev": true
+                },
+                "chalk": {
+                    "version": "4.1.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^4.1.0",
+                        "supports-color": "^7.1.0"
+                    }
+                },
+                "color-convert": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "~1.1.4"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.4",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+                    "dev": true
+                },
+                "escape-string-regexp": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+                    "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+                    "dev": true
+                },
+                "eslint-scope": {
+                    "version": "7.1.1",
+                    "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+                    "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+                    "dev": true,
+                    "requires": {
+                        "esrecurse": "^4.3.0",
+                        "estraverse": "^5.2.0"
+                    }
+                },
+                "estraverse": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+                    "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+                    "dev": true
+                },
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+                    "dev": true
+                },
+                "js-yaml": {
+                    "version": "4.1.0",
+                    "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+                    "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+                    "dev": true,
+                    "requires": {
+                        "argparse": "^2.0.1"
+                    }
+                },
+                "strip-json-comments": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+                    "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+                    "dev": true
+                },
+                "supports-color": {
+                    "version": "7.2.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+                    "dev": true,
+                    "requires": {
+                        "has-flag": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "eslint-config-prettier": {
+            "version": "8.5.0",
+            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
+            "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
+            "dev": true,
+            "requires": {}
+        },
+        "eslint-scope": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+            "dev": true,
+            "requires": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^4.1.1"
+            }
+        },
+        "eslint-utils": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+            "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+            "dev": true,
+            "requires": {
+                "eslint-visitor-keys": "^2.0.0"
+            },
+            "dependencies": {
+                "eslint-visitor-keys": {
+                    "version": "2.1.0",
+                    "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+                    "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+                    "dev": true
+                }
+            }
+        },
+        "eslint-visitor-keys": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+            "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+            "dev": true
+        },
+        "espree": {
+            "version": "9.4.0",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
+            "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+            "dev": true,
+            "requires": {
+                "acorn": "^8.8.0",
+                "acorn-jsx": "^5.3.2",
+                "eslint-visitor-keys": "^3.3.0"
+            }
+        },
+        "esquery": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+            "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+            "dev": true,
+            "requires": {
+                "estraverse": "^5.1.0"
+            },
+            "dependencies": {
+                "estraverse": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+                    "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+                    "dev": true
+                }
+            }
+        },
+        "esrecurse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+            "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+            "dev": true,
+            "requires": {
+                "estraverse": "^5.2.0"
+            },
+            "dependencies": {
+                "estraverse": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+                    "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+                    "dev": true
+                }
+            }
+        },
+        "estraverse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+            "dev": true
+        },
+        "esutils": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+            "dev": true
+        },
+        "example": {
+            "version": "file:packages/example",
+            "requires": {
+                "@types/serialport": "^8.0.2",
+                "@typescript-eslint/eslint-plugin": "^5.40.1",
+                "@typescript-eslint/parser": "^5.40.1",
+                "eslint": "^8.25.0",
+                "prettier": "^2.4.1",
+                "serialport": "^9.2.4",
+                "smartknobjs": "^0.1.0",
+                "ts-node": "^10.2.1",
+                "typescript": "^4.8.4"
+            }
+        },
+        "expand-template": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+            "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
+        },
+        "fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "dev": true
+        },
+        "fast-glob": {
+            "version": "3.2.12",
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+            "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+            "dev": true,
+            "requires": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.4"
+            },
+            "dependencies": {
+                "glob-parent": {
+                    "version": "5.1.2",
+                    "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+                    "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+                    "dev": true,
+                    "requires": {
+                        "is-glob": "^4.0.1"
+                    }
+                }
+            }
+        },
+        "fast-json-stable-stringify": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+            "dev": true
+        },
+        "fast-levenshtein": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+            "dev": true
+        },
+        "fastq": {
+            "version": "1.13.0",
+            "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+            "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+            "dev": true,
+            "requires": {
+                "reusify": "^1.0.4"
+            }
+        },
+        "file-entry-cache": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+            "dev": true,
+            "requires": {
+                "flat-cache": "^3.0.4"
+            }
+        },
+        "file-uri-to-path": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+            "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+        },
+        "fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "requires": {
+                "to-regex-range": "^5.0.1"
+            }
+        },
+        "find-up": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+            "dev": true,
+            "requires": {
+                "locate-path": "^6.0.0",
+                "path-exists": "^4.0.0"
+            }
+        },
+        "flat-cache": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+            "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+            "dev": true,
+            "requires": {
+                "flatted": "^3.1.0",
+                "rimraf": "^3.0.2"
+            }
+        },
+        "flatted": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+            "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+            "dev": true
+        },
+        "fs-constants": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+        },
+        "fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+            "dev": true
+        },
+        "github-from-package": {
+            "version": "0.0.0",
+            "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+            "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+        },
+        "glob": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "dev": true,
+            "requires": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            }
+        },
+        "glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dev": true,
+            "requires": {
+                "is-glob": "^4.0.3"
+            }
+        },
+        "globals": {
+            "version": "13.17.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
+            "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
+            "dev": true,
+            "requires": {
+                "type-fest": "^0.20.2"
+            }
+        },
+        "globby": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+            "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "dev": true,
+            "requires": {
+                "array-union": "^2.1.0",
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.9",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^3.0.0"
+            }
+        },
+        "grapheme-splitter": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+            "dev": true
+        },
+        "ieee754": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+            "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+        },
+        "ignore": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+            "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+            "dev": true
+        },
+        "import-fresh": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+            "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+            "dev": true,
+            "requires": {
+                "parent-module": "^1.0.0",
+                "resolve-from": "^4.0.0"
+            }
+        },
+        "imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+            "dev": true
+        },
+        "inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "dev": true,
+            "requires": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+        },
+        "ini": {
+            "version": "1.3.8",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+        },
+        "is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+            "dev": true
+        },
+        "is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "requires": {
+                "is-extglob": "^2.1.1"
+            }
+        },
+        "is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true
+        },
+        "isexe": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+            "dev": true
+        },
+        "js-sdsl": {
+            "version": "4.1.5",
+            "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
+            "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
+            "dev": true
+        },
+        "json-schema-traverse": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+            "dev": true
+        },
+        "json-stable-stringify-without-jsonify": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+            "dev": true
+        },
+        "levn": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+            "dev": true,
+            "requires": {
+                "prelude-ls": "^1.2.1",
+                "type-check": "~0.4.0"
+            }
+        },
+        "locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+            "dev": true,
+            "requires": {
+                "p-locate": "^5.0.0"
+            }
+        },
+        "lodash.merge": {
+            "version": "4.6.2",
+            "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+            "dev": true
+        },
+        "long": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+            "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+        },
+        "lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "requires": {
+                "yallist": "^4.0.0"
+            }
+        },
+        "make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true
+        },
+        "merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "dev": true
+        },
+        "micromatch": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+            "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+            "dev": true,
+            "requires": {
+                "braces": "^3.0.2",
+                "picomatch": "^2.3.1"
+            }
+        },
+        "mimic-response": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+            "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+        },
+        "minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dev": true,
+            "requires": {
+                "brace-expansion": "^1.1.7"
+            }
+        },
+        "minimist": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+            "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
+        },
+        "mkdirp-classic": {
+            "version": "0.5.3",
+            "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+            "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+        },
+        "ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "nan": {
+            "version": "2.17.0",
+            "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
+            "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ=="
+        },
+        "napi-build-utils": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+            "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+        },
+        "natural-compare": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+            "dev": true
+        },
+        "node-abi": {
+            "version": "3.26.0",
+            "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.26.0.tgz",
+            "integrity": "sha512-jRVtMFTChbi2i/jqo/i2iP9634KMe+7K1v35mIdj3Mn59i5q27ZYhn+sW6npISM/PQg7HrP2kwtRBMmh5Uvzdg==",
+            "requires": {
+                "semver": "^7.3.5"
+            }
+        },
+        "once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "requires": {
+                "wrappy": "1"
+            }
+        },
+        "optionator": {
+            "version": "0.9.1",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+            "dev": true,
+            "requires": {
+                "deep-is": "^0.1.3",
+                "fast-levenshtein": "^2.0.6",
+                "levn": "^0.4.1",
+                "prelude-ls": "^1.2.1",
+                "type-check": "^0.4.0",
+                "word-wrap": "^1.2.3"
+            }
+        },
+        "p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+            "dev": true,
+            "requires": {
+                "yocto-queue": "^0.1.0"
+            }
+        },
+        "p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+            "dev": true,
+            "requires": {
+                "p-limit": "^3.0.2"
+            }
+        },
+        "parent-module": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+            "dev": true,
+            "requires": {
+                "callsites": "^3.0.0"
+            }
+        },
+        "path-exists": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+            "dev": true
+        },
+        "path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "dev": true
+        },
+        "path-key": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+            "dev": true
+        },
+        "path-type": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+            "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+            "dev": true
+        },
+        "picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true
+        },
+        "prebuild-install": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+            "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+            "requires": {
+                "detect-libc": "^2.0.0",
+                "expand-template": "^2.0.3",
+                "github-from-package": "0.0.0",
+                "minimist": "^1.2.3",
+                "mkdirp-classic": "^0.5.3",
+                "napi-build-utils": "^1.0.1",
+                "node-abi": "^3.3.0",
+                "pump": "^3.0.0",
+                "rc": "^1.2.7",
+                "simple-get": "^4.0.0",
+                "tar-fs": "^2.0.0",
+                "tunnel-agent": "^0.6.0"
+            }
+        },
+        "prelude-ls": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+            "dev": true
+        },
+        "prettier": {
+            "version": "2.7.1",
+            "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
+            "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
+            "dev": true
+        },
+        "protobufjs": {
+            "version": "6.11.3",
+            "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+            "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+            "requires": {
+                "@protobufjs/aspromise": "^1.1.2",
+                "@protobufjs/base64": "^1.1.2",
+                "@protobufjs/codegen": "^2.0.4",
+                "@protobufjs/eventemitter": "^1.1.0",
+                "@protobufjs/fetch": "^1.1.0",
+                "@protobufjs/float": "^1.0.2",
+                "@protobufjs/inquire": "^1.1.0",
+                "@protobufjs/path": "^1.1.2",
+                "@protobufjs/pool": "^1.1.0",
+                "@protobufjs/utf8": "^1.1.0",
+                "@types/long": "^4.0.1",
+                "@types/node": ">=13.7.0",
+                "long": "^4.0.0"
+            }
+        },
+        "pump": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+            "requires": {
+                "end-of-stream": "^1.1.0",
+                "once": "^1.3.1"
+            }
+        },
+        "punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+            "dev": true
+        },
+        "queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true
+        },
+        "rc": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+            "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+            "requires": {
+                "deep-extend": "^0.6.0",
+                "ini": "~1.3.0",
+                "minimist": "^1.2.0",
+                "strip-json-comments": "~2.0.1"
+            }
+        },
+        "readable-stream": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+            "requires": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            }
+        },
+        "regexpp": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+            "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+            "dev": true
+        },
+        "resolve-from": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+            "dev": true
+        },
+        "reusify": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+            "dev": true
+        },
+        "rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dev": true,
+            "requires": {
+                "glob": "^7.1.3"
+            }
+        },
+        "run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "dev": true,
+            "requires": {
+                "queue-microtask": "^1.2.2"
+            }
+        },
+        "safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+        },
+        "semver": {
+            "version": "7.3.8",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+            "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+            "requires": {
+                "lru-cache": "^6.0.0"
+            }
+        },
+        "serialport": {
+            "version": "9.2.8",
+            "resolved": "https://registry.npmjs.org/serialport/-/serialport-9.2.8.tgz",
+            "integrity": "sha512-FsWpMQgSJxi93JgWl5xM1f9/Z8IjRJuaUEoHqLf8FPBLw7gMhInuHOBhI2onQufWIYPGTz3H3oGcu1nCaK1EfA==",
+            "requires": {
+                "@serialport/binding-mock": "9.2.4",
+                "@serialport/bindings": "9.2.8",
+                "@serialport/parser-byte-length": "9.2.4",
+                "@serialport/parser-cctalk": "9.2.4",
+                "@serialport/parser-delimiter": "9.2.4",
+                "@serialport/parser-inter-byte-timeout": "9.2.4",
+                "@serialport/parser-readline": "9.2.4",
+                "@serialport/parser-ready": "9.2.4",
+                "@serialport/parser-regex": "9.2.4",
+                "@serialport/stream": "9.2.4",
+                "debug": "^4.3.2"
+            }
+        },
+        "shebang-command": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+            "dev": true,
+            "requires": {
+                "shebang-regex": "^3.0.0"
+            }
+        },
+        "shebang-regex": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+            "dev": true
+        },
+        "simple-concat": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+            "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
+        },
+        "simple-get": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+            "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+            "requires": {
+                "decompress-response": "^6.0.0",
+                "once": "^1.3.1",
+                "simple-concat": "^1.0.0"
+            }
+        },
+        "slash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+            "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "dev": true
+        },
+        "smartknobjs": {
+            "version": "file:packages/smartknobjs",
+            "requires": {
+                "@types/serialport": "^8.0.2",
+                "@typescript-eslint/eslint-plugin": "^5.40.1",
+                "@typescript-eslint/parser": "^5.40.1",
+                "cobs": "^0.2.1",
+                "crc-32": "^1.2.0",
+                "eslint": "^8.25.0",
+                "prettier": "^2.4.1",
+                "serialport": "^9.2.4",
+                "smartknobjs-proto": "^0.1.0",
+                "ts-node": "^10.2.1",
+                "typescript": "^4.8.4"
+            }
+        },
+        "smartknobjs-proto": {
+            "version": "file:packages/smartknobjs-proto",
+            "requires": {
+                "protobufjs": "^6.11.2"
+            }
+        },
+        "string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "requires": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "requires": {
+                "ansi-regex": "^5.0.1"
+            }
+        },
+        "strip-json-comments": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+            "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
+        },
+        "tar-fs": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+            "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+            "requires": {
+                "chownr": "^1.1.1",
+                "mkdirp-classic": "^0.5.2",
+                "pump": "^3.0.0",
+                "tar-stream": "^2.1.4"
+            }
+        },
+        "tar-stream": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+            "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+            "requires": {
+                "bl": "^4.0.3",
+                "end-of-stream": "^1.4.1",
+                "fs-constants": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.1.1"
+            }
+        },
+        "text-table": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true
+        },
+        "to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "requires": {
+                "is-number": "^7.0.0"
+            }
+        },
+        "ts-node": {
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+            "dev": true,
+            "requires": {
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1",
+                "yn": "3.1.1"
+            }
+        },
+        "tslib": {
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+            "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+            "dev": true
+        },
+        "tunnel-agent": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+            "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+            "requires": {
+                "safe-buffer": "^5.0.1"
+            }
+        },
+        "type-check": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+            "dev": true,
+            "requires": {
+                "prelude-ls": "^1.2.1"
+            }
+        },
+        "type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+            "dev": true
+        },
+        "typescript": {
+            "version": "4.8.4",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+            "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+            "dev": true
+        },
+        "uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dev": true,
+            "requires": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+        },
+        "v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+            "dev": true
+        },
+        "which": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+            "dev": true,
+            "requires": {
+                "isexe": "^2.0.0"
+            }
+        },
+        "word-wrap": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+            "dev": true
+        },
+        "wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+        },
+        "yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+            "dev": true
+        },
+        "yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+            "dev": true
+        }
+    }
+}

+ 24 - 0
software/js/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "root",
+  "version": "0.1.0",
+  "description": "",
+  "main": "index.js",
+  "engines": {
+    "npm": ">=8.19.2",
+    "node": ">=18.11.0"
+  },
+  "scripts": {
+    "build": "npm run build --workspaces --if-present",
+    "example": "npm run -w example main"
+  },
+  "author": "",
+  "license": "Apache-2.0",
+  "workspaces": [
+    "packages/smartknobjs-proto",
+    "packages/smartknobjs",
+    "packages/example"
+  ],
+  "devDependencies": {
+    "eslint-config-prettier": "^8.5.0"
+  }
+}

+ 19 - 0
software/js/packages/example/.eslintrc

@@ -0,0 +1,19 @@
+// .eslintrc
+{
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "ecmaVersion": 12,
+        "sourceType": "module"
+    },
+    "plugins": ["@typescript-eslint"],
+    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
+
+    "rules": {
+        "@typescript-eslint/no-unused-vars": "error",
+        "@typescript-eslint/consistent-type-definitions": ["error", "type"]
+    },
+
+    "env": {
+        "node": true
+    }
+}

+ 10 - 0
software/js/packages/example/.prettierrc

@@ -0,0 +1,10 @@
+{
+    "printWidth": 120,
+    "tabWidth": 4,
+    "useTabs": false,
+    "semi": false,
+    "singleQuote": true,
+    "trailingComma": "all",
+    "bracketSpacing": false,
+    "arrowParens": "always"
+}

+ 28 - 0
software/js/packages/example/package.json

@@ -0,0 +1,28 @@
+{
+    "name": "example",
+    "version": "0.1.0",
+    "description": "SmartKnob Interface Library",
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "scripts": {
+        "build": "tsc",
+        "format": "prettier --write \"**/*.+(js|ts|json)\"",
+        "lint": "eslint --ext .js,.ts .",
+        "main": "ts-node src/index.ts"
+    },
+    "author": "",
+    "license": "Apache-2.0",
+    "dependencies": {
+        "serialport": "^9.2.4",
+        "smartknobjs": "^0.1.0"
+    },
+    "devDependencies": {
+        "@types/serialport": "^8.0.2",
+        "@typescript-eslint/eslint-plugin": "^5.40.1",
+        "@typescript-eslint/parser": "^5.40.1",
+        "eslint": "^8.25.0",
+        "prettier": "^2.4.1",
+        "ts-node": "^10.2.1",
+        "typescript": "^4.8.4"
+    }
+}

+ 60 - 0
software/js/packages/example/src/index.ts

@@ -0,0 +1,60 @@
+import SerialPort = require('serialport')
+import {SmartKnob} from 'smartknobjs'
+import {PB} from 'smartknobjs-proto'
+
+const main = async () => {
+    const ports = await SerialPort.list()
+
+    const matchingPorts = ports.filter((portInfo) => {
+        // Implement a check for your device's vendor+product+serial
+        // (this is more robust than the alternative of just hardcoding a "path" like "/dev/ttyUSB0")
+        return (
+            portInfo.vendorId === '1a86' && portInfo.productId === '7523'
+            // && portInfo.serialNumber === 'DEADBEEF'
+        )
+    })
+
+    if (matchingPorts.length < 1) {
+        console.error(`No smartknob usb serial port found! ${JSON.stringify(ports, undefined, 4)}`)
+        return
+    } else if (matchingPorts.length > 1) {
+        console.error(`Multiple smartknob usb serial ports found: ${JSON.stringify(matchingPorts, undefined, 4)}`)
+        return
+    }
+
+    const portInfo = matchingPorts[0]
+
+    let lastLoggedState: PB.ISmartKnobState | undefined
+    const smartknob = new SmartKnob(portInfo.path, (message: PB.FromSmartKnob) => {
+        if (message.payload === 'log' && message.log) {
+            console.log('LOG', message.log.msg)
+        } else if (message.payload === 'smartknobState' && message.smartknobState) {
+            if (
+                message.smartknobState.currentPosition !== lastLoggedState?.currentPosition ||
+                Math.abs((message.smartknobState.subPositionUnit ?? 0) - (lastLoggedState?.subPositionUnit ?? 0)) > 1
+            ) {
+                console.log(
+                    `State:\n${JSON.stringify(
+                        PB.SmartKnobState.toObject(message.smartknobState as PB.SmartKnobState, {defaults: true}),
+                        undefined,
+                        4,
+                    )}`,
+                )
+                lastLoggedState = message.smartknobState
+            }
+        }
+    })
+    smartknob.sendConfig(
+        PB.SmartKnobConfig.create({
+            detentStrengthUnit: 1,
+            endstopStrengthUnit: 1,
+            numPositions: 5,
+            position: 0,
+            positionWidthRadians: (10 * Math.PI) / 180,
+            snapPoint: 1.1,
+            text: 'From TS!',
+        }),
+    )
+}
+
+main()

+ 105 - 0
software/js/packages/example/tsconfig.json

@@ -0,0 +1,105 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Projects */
+    // "incremental": true,                              /* Enable incremental compilation */
+    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
+    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
+    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+    /* Language and Environment */
+    "target": "es5",                                     /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
+    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
+    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
+    // "reactNamespace": "",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
+    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+
+    /* Modules */
+    "module": "commonjs",                                /* Specify what module code is generated. */
+    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
+    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    // "paths" :{
+    //   "*": ["./src/typings/*", "./*"]
+    // },
+    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+    // "typeRoots": ["src/typings"],                        /* Specify multiple folders that act like `./node_modules/@types`. */
+    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+    // "resolveJsonModule": true,                        /* Enable importing .json files */
+    // "noResolve": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
+
+    /* JavaScript Support */
+    "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
+
+    /* Emit */
+    "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
+    "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
+    // "removeComments": true,                           /* Disable emitting comments. */
+    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
+    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types */
+    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
+    // "stripInternal": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
+    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */
+    "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+    // "preserveConstEnums": true,                       /* Disable erasing `const enum` declarations in generated code. */
+    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+
+    /* Interop Constraints */
+    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
+    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+    /* Type Checking */
+    "strict": true,                                      /* Enable all strict type-checking options. */
+    "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied `any` type.. */
+    "strictNullChecks": true,                         /* When type checking, take into account `null` and `undefined`. */
+    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+    // "strictBindCallApply": true,                      /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
+    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+    // "noImplicitThis": true,                           /* Enable error reporting when `this` is given the type `any`. */
+    // "useUnknownInCatchVariables": true,               /* Type catch clause variables as 'unknown' instead of 'any'. */
+    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+    // "noUnusedLocals": true,                           /* Enable error reporting when a local variables aren't read. */
+    // "noUnussedParameters": true,                       /* Raise an error when a function parameter isn't read */
+    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+    "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+    "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+    // "noUncheckedIndexedAccess": true,                 /* Include 'undefined' in index signature results */
+    "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type */
+    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+    /* Completeness */
+    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+  },
+  "include": ["src"],
+  "exclude": ["node_modules"]
+}

+ 201 - 0
software/js/packages/smartknobjs-proto/package-lock.json

@@ -0,0 +1,201 @@
+{
+  "name": "smartknobjs-proto",
+  "version": "0.1.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "smartknobjs-proto",
+      "version": "0.1.0",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "protobufjs": "^6.11.2"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+    },
+    "node_modules/@types/long": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+      "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
+    },
+    "node_modules/@types/node": {
+      "version": "18.11.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
+      "integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
+    },
+    "node_modules/long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+    },
+    "node_modules/protobufjs": {
+      "version": "6.11.3",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+      "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/long": "^4.0.1",
+        "@types/node": ">=13.7.0",
+        "long": "^4.0.0"
+      },
+      "bin": {
+        "pbjs": "bin/pbjs",
+        "pbts": "bin/pbts"
+      }
+    }
+  },
+  "dependencies": {
+    "@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+    },
+    "@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+    },
+    "@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+    },
+    "@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+    },
+    "@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "requires": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+    },
+    "@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+    },
+    "@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+    },
+    "@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+    },
+    "@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+    },
+    "@types/long": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+      "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
+    },
+    "@types/node": {
+      "version": "18.11.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
+      "integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
+    },
+    "long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+    },
+    "protobufjs": {
+      "version": "6.11.3",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+      "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+      "requires": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/long": "^4.0.1",
+        "@types/node": ">=13.7.0",
+        "long": "^4.0.0"
+      }
+    }
+  }
+}

+ 15 - 0
software/js/packages/smartknobjs-proto/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "smartknobjs-proto",
+  "version": "0.1.0",
+  "description": "SmartKnob Protobuf Generated Code",
+  "main": "dist/smartknob_proto.js",
+  "types": "dist/smartknob_proto.d.ts",
+  "scripts": {
+    "build": "mkdir -p dist && pbjs --target static-module --out dist/smartknob_proto.js ../../../../proto/*.proto && pbts -o dist/smartknob_proto.d.ts dist/smartknob_proto.js"
+  },
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "protobufjs": "^6.11.2"
+  }
+}

+ 19 - 0
software/js/packages/smartknobjs/.eslintrc

@@ -0,0 +1,19 @@
+// .eslintrc
+{
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "ecmaVersion": 12,
+        "sourceType": "module"
+    },
+    "plugins": ["@typescript-eslint"],
+    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
+
+    "rules": {
+        "@typescript-eslint/no-unused-vars": "error",
+        "@typescript-eslint/consistent-type-definitions": ["error", "type"]
+    },
+
+    "env": {
+        "node": true
+    }
+}

+ 10 - 0
software/js/packages/smartknobjs/.prettierrc

@@ -0,0 +1,10 @@
+{
+    "printWidth": 120,
+    "tabWidth": 4,
+    "useTabs": false,
+    "semi": false,
+    "singleQuote": true,
+    "trailingComma": "all",
+    "bracketSpacing": false,
+    "arrowParens": "always"
+}

+ 29 - 0
software/js/packages/smartknobjs/package.json

@@ -0,0 +1,29 @@
+{
+    "name": "smartknobjs",
+    "version": "0.1.0",
+    "description": "SmartKnob Interface Library",
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "scripts": {
+        "build": "tsc",
+        "format": "prettier --write \"**/*.+(js|ts|json)\"",
+        "lint": "eslint --ext .js,.ts ."
+    },
+    "author": "",
+    "license": "Apache-2.0",
+    "dependencies": {
+        "cobs": "^0.2.1",
+        "crc-32": "^1.2.0",
+        "serialport": "^9.2.4",
+        "smartknobjs-proto": "^0.1.0"
+    },
+    "devDependencies": {
+        "@types/serialport": "^8.0.2",
+        "@typescript-eslint/eslint-plugin": "^5.40.1",
+        "@typescript-eslint/parser": "^5.40.1",
+        "eslint": "^8.25.0",
+        "prettier": "^2.4.1",
+        "ts-node": "^10.2.1",
+        "typescript": "^4.8.4"
+    }
+}

+ 195 - 0
software/js/packages/smartknobjs/src/index.ts

@@ -0,0 +1,195 @@
+import SerialPort = require('serialport')
+import {decode, encode} from 'cobs'
+import * as CRC32 from 'crc-32'
+
+import {PB} from 'smartknobjs-proto'
+
+export type MessageCallback = (message: PB.FromSmartKnob) => void
+
+type QueueEntry = {
+    nonce: number
+    encodedToSmartknobPayload: Uint8Array
+}
+
+const sleep = (millis: number) => {
+    return new Promise((resolve) => {
+        setTimeout(resolve, millis)
+    })
+}
+
+export class SmartKnob {
+    private static readonly RETRY_MILLIS = 250
+    private static readonly BAUD = 921600
+
+    private port: SerialPort | null
+    private onMessage: MessageCallback
+    private buffer: Buffer
+
+    private outgoingQueue: QueueEntry[] = []
+
+    private lastNonce = 1
+    private retryTimeout: NodeJS.Timeout | null = null
+
+    private currentConfig: PB.SmartKnobConfig
+
+    constructor(serialPath: string | null, onMessage: MessageCallback) {
+        this.onMessage = onMessage
+
+        this.buffer = Buffer.alloc(0)
+
+        if (serialPath !== null) {
+            this.port = new SerialPort(serialPath, {
+                baudRate: SmartKnob.BAUD,
+            })
+            this.port.on('data', (data: Buffer) => {
+                this.buffer = Buffer.concat([this.buffer, data])
+                this.processBuffer()
+            })
+        } else {
+            this.port = null
+        }
+
+        this.currentConfig = PB.SmartKnobConfig.create({
+            detentStrengthUnit: 1,
+            endstopStrengthUnit: 1,
+            numPositions: 5,
+            position: 0,
+            positionWidthRadians: (10 * Math.PI) / 180,
+            snapPoint: 1.1,
+            text: 'From TS!',
+        })
+
+        this.lastNonce = Math.floor(Math.random() * (2 ^ (32 - 1)))
+    }
+
+    /**
+     * Perform a hard reset of the MCU. Takes a few seconds.
+     */
+    public async hardReset(): Promise<void> {
+        if (this.port === null) {
+            console.warn("Not connected to SmartKnob, so hard reset isn't possible")
+            return
+        }
+        this.outgoingQueue = []
+        this.port.set({rts: true, dtr: false})
+        await sleep(200)
+        this.port.set({rts: true, dtr: true})
+        await sleep(200)
+        return
+    }
+
+    public sendConfig(config: PB.SmartKnobConfig): void {
+        this.sendMessage(
+            PB.ToSmartknob.create({
+                smartknobConfig: config,
+            }),
+        )
+    }
+
+    private processBuffer(): void {
+        let i: number
+        // Iterate 0-delimited packets
+        while ((i = this.buffer.indexOf(0)) != -1) {
+            const raw_buffer = this.buffer.slice(0, i)
+            const packet = decode(raw_buffer) as Buffer
+            this.buffer = this.buffer.slice(i + 1)
+            if (packet.length <= 4) {
+                console.debug(`Received short packet ${this.buffer.slice(0, i)}`)
+                continue
+            }
+            const payload = packet.slice(0, packet.length - 4)
+
+            // Validate CRC32
+            const crc_buf = packet.slice(packet.length - 4, packet.length)
+            const provided_crc = crc_buf[0] | (crc_buf[1] << 8) | (crc_buf[2] << 16) | (crc_buf[3] << 24)
+            const crc = CRC32.buf(payload)
+            if (crc !== provided_crc) {
+                console.debug(`Bad CRC. Expected ${crc} but received ${provided_crc}`)
+                console.debug(raw_buffer.toString())
+                continue
+            }
+
+            let message: PB.FromSmartKnob
+            try {
+                message = PB.FromSmartKnob.decode(payload)
+            } catch (err) {
+                console.warn(`Invalid protobuf message ${payload}`)
+                return
+            }
+
+            if (message.payload === 'ack') {
+                const nonce = message.ack?.nonce ?? undefined
+                if (nonce === undefined) {
+                    console.warn('Received ack without nonce')
+                } else {
+                    this.handleAck(nonce)
+                }
+            }
+
+            this.onMessage(message)
+        }
+    }
+
+    private sendMessage(message: PB.ToSmartknob) {
+        if (this.port === null) {
+            return
+        }
+        message.nonce = this.lastNonce++
+
+        // Encode before enqueueing to ensure messages don't change once they're queued
+        const payload = PB.ToSmartknob.encode(message).finish()
+
+        if (this.outgoingQueue.length > 10) {
+            console.warn(`SmartKnob outgoing queue overflowed! Dropping ${this.outgoingQueue.length} pending messages!`)
+            this.outgoingQueue.length = 0
+        }
+        this.outgoingQueue.push({
+            nonce: message.nonce,
+            encodedToSmartknobPayload: payload,
+        })
+        this.serviceQueue()
+    }
+
+    private handleAck(nonce: number): void {
+        if (this.outgoingQueue.length > 0 && nonce === this.outgoingQueue[0].nonce) {
+            if (this.retryTimeout !== null) {
+                clearTimeout(this.retryTimeout)
+                this.retryTimeout = null
+            }
+            this.outgoingQueue.shift()
+            this.serviceQueue()
+        } else {
+            console.debug(`Ignoring unexpected ack for nonce ${nonce}`)
+        }
+    }
+
+    private serviceQueue(): void {
+        if (this.port === null) {
+            return
+        }
+        if (this.retryTimeout !== null) {
+            // Retry is pending; let the pending timeout handle the next step
+            return
+        }
+        if (this.outgoingQueue.length === 0) {
+            return
+        }
+
+        const {encodedToSmartknobPayload: payload} = this.outgoingQueue[0]
+
+        const crc = CRC32.buf(payload)
+        const crcBuffer = Buffer.from([crc & 0xff, (crc >>> 8) & 0xff, (crc >>> 16) & 0xff, (crc >>> 24) & 0xff])
+        const packet = Buffer.concat([payload, crcBuffer])
+
+        const encodedDelimitedPacket = Buffer.concat([encode(packet), Buffer.from([0])])
+
+        this.retryTimeout = setTimeout(() => {
+            this.retryTimeout = null
+            console.log(`Retrying ToSmartknob...`)
+            this.serviceQueue()
+        }, SmartKnob.RETRY_MILLIS)
+
+        console.debug(`Sent ${payload.length} byte packet with CRC ${(crc >>> 0).toString(16)}`)
+        this.port.write(encodedDelimitedPacket)
+    }
+}

+ 4 - 0
software/js/packages/smartknobjs/src/typings/cobs.d.ts

@@ -0,0 +1,4 @@
+declare module 'cobs' {
+    export function decode(buf: Buffer): Buffer
+    export function encode(buf: Buffer, zeroFrame?: boolean): Buffer
+}

+ 105 - 0
software/js/packages/smartknobjs/tsconfig.json

@@ -0,0 +1,105 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Projects */
+    // "incremental": true,                              /* Enable incremental compilation */
+    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
+    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
+    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+    /* Language and Environment */
+    "target": "es5",                                     /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
+    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
+    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
+    // "reactNamespace": "",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
+    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+
+    /* Modules */
+    "module": "commonjs",                                /* Specify what module code is generated. */
+    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
+    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    // "paths" :{
+    //   "*": ["./src/typings/*", "./*"]
+    // },
+    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+    // "typeRoots": ["src/typings"],                        /* Specify multiple folders that act like `./node_modules/@types`. */
+    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+    // "resolveJsonModule": true,                        /* Enable importing .json files */
+    // "noResolve": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
+
+    /* JavaScript Support */
+    "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
+
+    /* Emit */
+    "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
+    "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
+    // "removeComments": true,                           /* Disable emitting comments. */
+    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
+    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types */
+    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
+    // "stripInternal": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
+    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */
+    "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+    // "preserveConstEnums": true,                       /* Disable erasing `const enum` declarations in generated code. */
+    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+
+    /* Interop Constraints */
+    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
+    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+    /* Type Checking */
+    "strict": true,                                      /* Enable all strict type-checking options. */
+    "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied `any` type.. */
+    "strictNullChecks": true,                         /* When type checking, take into account `null` and `undefined`. */
+    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+    // "strictBindCallApply": true,                      /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
+    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+    // "noImplicitThis": true,                           /* Enable error reporting when `this` is given the type `any`. */
+    // "useUnknownInCatchVariables": true,               /* Type catch clause variables as 'unknown' instead of 'any'. */
+    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+    // "noUnusedLocals": true,                           /* Enable error reporting when a local variables aren't read. */
+    // "noUnussedParameters": true,                       /* Raise an error when a function parameter isn't read */
+    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+    "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+    "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+    // "noUncheckedIndexedAccess": true,                 /* Include 'undefined' in index signature results */
+    "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type */
+    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+    /* Completeness */
+    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+  },
+  "include": ["src"],
+  "exclude": ["node_modules"]
+}

+ 1 - 0
thirdparty/nanopb

@@ -0,0 +1 @@
+Subproject commit 80f9d5bcbc0da72c95d3411b642c109a14298d75