Browse Source

Demo improvements (#135)

- LED hue config
- press state reporting
- custom display modes for timeline demo
Scott Bezek 2 years ago
parent
commit
590ba8850c

+ 111 - 49
firmware/src/display_task.cpp

@@ -1,6 +1,7 @@
 #if SK_DISPLAY
 #include "display_task.h"
 #include "semaphore_guard.h"
+#include "util.h"
 
 #include "font/roboto_light_60.h"
 
@@ -19,6 +20,15 @@ DisplayTask::~DisplayTask() {
   vSemaphoreDelete(mutex_);
 }
 
+static void drawPlayButton(TFT_eSprite& spr, int x, int y, int width, int height, uint16_t color) {
+  spr.fillTriangle(
+    x, y - height / 2,
+    x, y + height / 2,
+    x + width, y,
+    color
+  );
+}
+
 void DisplayTask::run() {
     tft_.begin();
     tft_.invertDisplay(1);
@@ -56,43 +66,6 @@ void DisplayTask::run() {
         spr_.fillSprite(TFT_BLACK);
 
         int32_t num_positions = state.config.max_position - state.config.min_position + 1;
-        if (num_positions > 1) {
-          int32_t height = (state.current_position - state.config.min_position) * TFT_HEIGHT / (state.config.max_position - state.config.min_position);
-          spr_.fillRect(0, TFT_HEIGHT - height, TFT_WIDTH, height, FILL_COLOR);
-        }
-
-        spr_.setFreeFont(&Roboto_Light_60);
-        spr_.drawNumber(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.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.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;
-          line_y += spr_.fontHeight(1);
-        }
-
-        float left_bound = PI / 2;
-
-        if (num_positions > 0) {
-          float range_radians = (state.config.max_position - state.config.min_position) * state.config.position_width_radians;
-          left_bound = PI / 2 + range_radians / 2;
-          float right_bound = PI / 2 - range_radians / 2;
-          spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(left_bound), TFT_HEIGHT/2 - RADIUS * sinf(left_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound), TFT_WHITE);
-          spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(right_bound), TFT_HEIGHT/2 - RADIUS * sinf(right_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound), TFT_WHITE);
-        }
-        if (DRAW_ARC) {
-          spr_.drawCircle(TFT_WIDTH/2, TFT_HEIGHT/2, RADIUS, TFT_DARKGREY);
-        }
-
         float adjusted_sub_position = state.sub_position_unit * state.config.position_width_radians;
         if (num_positions > 0) {
           if (state.current_position == state.config.min_position && state.sub_position_unit < 0) {
@@ -102,25 +75,114 @@ void DisplayTask::run() {
           }
         }
 
+        float left_bound = PI / 2;
+        float right_bound = 0;
+        if (num_positions > 0) {
+          float range_radians = (state.config.max_position - state.config.min_position) * state.config.position_width_radians;
+          left_bound = PI / 2 + range_radians / 2;
+          right_bound = PI / 2 - range_radians / 2;
+        }
         float raw_angle = left_bound - (state.current_position - state.config.min_position) * state.config.position_width_radians;
         float adjusted_angle = raw_angle - adjusted_sub_position;
+        
+        bool sk_demo_mode = strncmp(state.config.text, "SKDEMO_", 7) == 0;
 
-        if (num_positions > 0 && ((state.current_position == state.config.min_position && state.sub_position_unit < 0) || (state.current_position == state.config.max_position && state.sub_position_unit > 0))) {
+        if (!sk_demo_mode) {
+          if (num_positions > 1) {
+            int32_t height = (state.current_position - state.config.min_position) * TFT_HEIGHT / (state.config.max_position - state.config.min_position);
+            spr_.fillRect(0, TFT_HEIGHT - height, TFT_WIDTH, height, FILL_COLOR);
+          }
 
-          spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(raw_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(raw_angle), 5, DOT_COLOR);
-          if (raw_angle < adjusted_angle) {
-            for (float r = raw_angle; r <= adjusted_angle; r += 2 * PI / 180) {
-              spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
+          spr_.setFreeFont(&Roboto_Light_60);
+          spr_.drawNumber(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.text;
+          char* end = start + strlen(state.config.text);
+          while (start < end) {
+            char* newline = strchr(start, '\n');
+            if (newline == nullptr) {
+              newline = end;
             }
-            spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
-          } else {
-            for (float r = raw_angle; r >= adjusted_angle; r -= 2 * PI / 180) {
-              spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
+            
+            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;
+            line_y += spr_.fontHeight(1);
+          }
+
+          if (num_positions > 0) {
+            spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(left_bound), TFT_HEIGHT/2 - RADIUS * sinf(left_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound), TFT_WHITE);
+            spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(right_bound), TFT_HEIGHT/2 - RADIUS * sinf(right_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound), TFT_WHITE);
+          }
+          if (DRAW_ARC) {
+            spr_.drawCircle(TFT_WIDTH/2, TFT_HEIGHT/2, RADIUS, TFT_DARKGREY);
+          }
+
+          if (num_positions > 0 && ((state.current_position == state.config.min_position && state.sub_position_unit < 0) || (state.current_position == state.config.max_position && state.sub_position_unit > 0))) {
+            spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(raw_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(raw_angle), 5, DOT_COLOR);
+            if (raw_angle < adjusted_angle) {
+              for (float r = raw_angle; r <= adjusted_angle; r += 2 * PI / 180) {
+                spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
+              }
+              spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
+            } else {
+              for (float r = raw_angle; r >= adjusted_angle; r -= 2 * PI / 180) {
+                spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
+              }
+              spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
             }
-            spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
+          } else {
+            spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 5, DOT_COLOR);
           }
         } else {
-          spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 5, DOT_COLOR);
+          if (strncmp(state.config.text, "SKDEMO_Scroll", 13) == 0) {
+            spr_.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, spr_.color565(150, 0, 0));
+            spr_.setFreeFont(&Roboto_Thin_24);
+            spr_.drawString("Scroll", TFT_WIDTH / 2, TFT_HEIGHT / 2, 1);
+            bool detent = false;
+            for (uint8_t i = 0; i < state.config.detent_positions_count; i++) {
+              if (state.config.detent_positions[i] == state.current_position) {
+                detent = true;
+                break;
+              }
+            }
+            spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 16) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 16) * sinf(adjusted_angle), detent ? 8 : 5, TFT_WHITE);
+          } else if (strncmp(state.config.text, "SKDEMO_Frames", 13) == 0) {
+            int32_t width = (state.current_position - state.config.min_position) * TFT_WIDTH / (state.config.max_position - state.config.min_position);
+            spr_.fillRect(0, 0, width, TFT_HEIGHT, spr_.color565(0, 150, 0));
+            spr_.setFreeFont(&Roboto_Light_60);
+            spr_.drawNumber(state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2, 1);
+            spr_.setFreeFont(&Roboto_Thin_24);
+            spr_.drawString("Frame", TFT_WIDTH / 2, TFT_HEIGHT / 2 - DESCRIPTION_Y_OFFSET - VALUE_OFFSET, 1);
+          } else if (strncmp(state.config.text, "SKDEMO_Speed", 12) == 0) {
+            spr_.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, spr_.color565(0, 0, 150));
+
+            float normalizedFractional = sgn(state.sub_position_unit) *
+                CLAMP(lerp(state.sub_position_unit * sgn(state.sub_position_unit), 0.1, 0.9, 0, 1), (float)0, (float)1);
+            float normalized = state.current_position + normalizedFractional;
+            float speed = sgn(normalized) * powf(2, fabsf(normalized) - 1);
+            float roundedSpeed = truncf(speed * 10) / 10;
+
+            spr_.setFreeFont(&Roboto_Thin_24);
+            if (roundedSpeed == 0) {
+              spr_.drawString("Paused", TFT_WIDTH / 2, TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET + VALUE_OFFSET, 1);
+
+              spr_.fillRect(TFT_WIDTH / 2 + 5, TFT_HEIGHT / 2 - 20, 10, 40, TFT_WHITE);
+              spr_.fillRect(TFT_WIDTH / 2 - 5 - 10, TFT_HEIGHT / 2 - 20, 10, 40, TFT_WHITE);
+            } else {
+              char buf[10];
+              snprintf(buf, sizeof(buf), "%0.1fx", roundedSpeed);
+              spr_.drawString(buf, TFT_WIDTH / 2, TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET + VALUE_OFFSET, 1);
+
+              uint16_t x = TFT_WIDTH / 2;
+              for (uint8_t i = 0; i < max(1, abs(state.current_position)); i++) {
+                drawPlayButton(spr_, x, TFT_HEIGHT / 2, sgn(roundedSpeed) * 20, 40, TFT_WHITE);
+                x += sgn(roundedSpeed) * 20;
+              }
+            }
+          }
         }
 
         spr_.pushSprite(0, 0);
@@ -129,7 +191,7 @@ void DisplayTask::run() {
           SemaphoreGuard lock(mutex_);
           ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, brightness_);
         }
-        delay(2);
+        delay(5);
     }
 }
 

+ 8 - 0
firmware/src/interface_callbacks.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <functional>
+
+#include "proto_gen/smartknob.pb.h"
+
+typedef std::function<void(PB_SmartKnobConfig&)> ConfigCallback;
+typedef std::function<void(void)> MotorCalibrationCallback;

+ 57 - 25
firmware/src/interface_task.cpp

@@ -40,6 +40,7 @@ static PB_SmartKnobConfig configs[] = {
     // pb_size_t detent_positions_count;
     // int32_t detent_positions[5];
     // float snap_point_bias;
+    // int8_t led_hue;
 
     {
         0,
@@ -55,6 +56,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        200,
     },
     {
         0,
@@ -70,6 +72,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        0,
     },
     {
         0,
@@ -85,6 +88,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        73,
     },
     {
         0,
@@ -100,6 +104,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        157,
     },
     {
         0,
@@ -115,6 +120,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        45,
     },
     {
         127,
@@ -130,6 +136,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        219,
     },
     {
         127,
@@ -145,6 +152,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        25,
     },
     {
         0,
@@ -160,6 +168,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        200,
     },
     {
         0,
@@ -175,6 +184,7 @@ static PB_SmartKnobConfig configs[] = {
         0,
         {},
         0,
+        0,
     },
     {
         0,
@@ -190,6 +200,7 @@ static PB_SmartKnobConfig configs[] = {
         4,
         {2, 10, 21, 22},
         0,
+        73,
     },
     {
         0,
@@ -204,7 +215,8 @@ static PB_SmartKnobConfig configs[] = {
         "Return-to-center\nwith detents",
         0,
         {},
-        0.4
+        0.4,
+        157,
     },
 };
 
@@ -213,12 +225,17 @@ InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, Dis
         stream_(),
         motor_task_(motor_task),
         display_task_(display_task),
-        plaintext_protocol_(stream_, motor_task_),
-        proto_protocol_(stream_, motor_task_) {
+        plaintext_protocol_(stream_, [this] () {
+            motor_task_.runCalibration();
+        }),
+        proto_protocol_(stream_, [this] (PB_SmartKnobConfig& config) {
+            applyConfig(config, true);
+        }) {
     #if SK_DISPLAY
         assert(display_task != nullptr);
     #endif
 
+
     log_queue_ = xQueueCreate(10, sizeof(std::string *));
     assert(log_queue_ != NULL);
 
@@ -257,7 +274,7 @@ void InterfaceTask::run() {
         }
     #endif
 
-    motor_task_.setConfig(configs[0]);
+    applyConfig(configs[0], false);
     motor_task_.addListener(knob_state_queue_);
 
     plaintext_protocol_.init([this] () {
@@ -291,15 +308,15 @@ void InterfaceTask::run() {
     });
 
     // Start in legacy protocol mode
-    SerialProtocol* current_protocol = &plaintext_protocol_;
+    current_protocol_ = &plaintext_protocol_;
 
-    ProtocolChangeCallback protocol_change_callback = [this, &current_protocol] (uint8_t protocol) {
+    ProtocolChangeCallback protocol_change_callback = [this] (uint8_t protocol) {
         switch (protocol) {
             case SERIAL_PROTOCOL_LEGACY:
-                current_protocol = &plaintext_protocol_;
+                current_protocol_ = &plaintext_protocol_;
                 break;
             case SERIAL_PROTOCOL_PROTO:
-                current_protocol = &proto_protocol_;
+                current_protocol_ = &proto_protocol_;
                 break;
             default:
                 log("Unknown protocol requested");
@@ -312,16 +329,15 @@ void InterfaceTask::run() {
 
     // Interface loop:
     while (1) {
-        PB_SmartKnobState state;
-        if (xQueueReceive(knob_state_queue_, &state, 0) == pdTRUE) {
-            current_protocol->handleState(state);
+        if (xQueueReceive(knob_state_queue_, &latest_state_, 0) == pdTRUE) {
+            publishState();
         }
 
-        current_protocol->loop();
+        current_protocol_->loop();
 
         std::string* log_string;
         while (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) {
-            current_protocol->log(log_string->c_str());
+            current_protocol_->log(log_string->c_str());
             delete log_string;
         }
 
@@ -360,7 +376,7 @@ void InterfaceTask::changeConfig(bool next) {
     
     snprintf(buf_, sizeof(buf_), "Changing config to %d -- %s", current_config_, configs[current_config_].text);
     log(buf_);
-    motor_task_.setConfig(configs[current_config_]);
+    applyConfig(configs[current_config_], false);
 }
 
 void InterfaceTask::updateHardware() {
@@ -380,6 +396,7 @@ void InterfaceTask::updateHardware() {
         }
     #endif
 
+    static bool pressed;
     #if SK_STRAIN
         if (scale.wait_ready_timeout(100)) {
             strain_reading_ = scale.read();
@@ -396,23 +413,26 @@ void InterfaceTask::updateHardware() {
 
                 // Ignore readings that are way out of expected bounds
                 if (-1 < press_value_unit && press_value_unit < 2) {
-                    static bool pressed;
-                    static uint8_t press_count;
-                    if (!pressed && press_value_unit > 0.75) {
-                        press_count++;
-                        if (press_count > 2) {
+                    static uint8_t press_readings;
+                    if (!pressed && press_value_unit > 1) {
+                        press_readings++;
+                        if (press_readings > 2) {
                             motor_task_.playHaptic(true);
                             pressed = true;
-                            changeConfig(true);
+                            press_count_++;
+                            publishState();
+                            if (!remote_controlled_) {
+                                changeConfig(true);
+                            }
                         }
-                    } else if (pressed && press_value_unit < 0.25) {
-                        press_count++;
-                        if (press_count > 2) {
+                    } else if (pressed && press_value_unit < 0.5) {
+                        press_readings++;
+                        if (press_readings > 2) {
                             motor_task_.playHaptic(false);
                             pressed = false;
                         }
                     } else {
-                        press_count = 0;
+                        press_readings = 0;
                     }
                 }
             }
@@ -440,7 +460,7 @@ void InterfaceTask::updateHardware() {
 
     #if SK_LEDS
         for (uint8_t i = 0; i < NUM_LEDS; i++) {
-            leds[i].setHSV(200 * CLAMP(press_value_unit, (float)0, (float)1), 255, brightness >> 8);
+            leds[i].setHSV(latest_config_.led_hue, 255 - 180*CLAMP(press_value_unit, (float)0, (float)1) - 75*pressed, brightness >> 8);
 
             // Gamma adjustment
             leds[i].r = dim8_video(leds[i].r);
@@ -455,3 +475,15 @@ void InterfaceTask::setConfiguration(Configuration* configuration) {
     SemaphoreGuard lock(mutex_);
     configuration_ = configuration;
 }
+
+void InterfaceTask::publishState() {
+    // Apply local state before publishing to serial
+    latest_state_.press_nonce = press_count_;
+    current_protocol_->handleState(latest_state_);
+}
+
+void InterfaceTask::applyConfig(PB_SmartKnobConfig& config, bool from_remote) {
+    remote_controlled_ = from_remote;
+    latest_config_ = config;
+    motor_task_.setConfig(config);
+}

+ 8 - 0
firmware/src/interface_task.h

@@ -44,7 +44,13 @@ class InterfaceTask : public Task<InterfaceTask>, public Logger {
         uint8_t strain_calibration_step_ = 0;
         int32_t strain_reading_ = 0;
 
+        SerialProtocol* current_protocol_ = nullptr;
+        bool remote_controlled_ = false;
         int current_config_ = 0;
+        uint8_t press_count_ = 1;
+
+        PB_SmartKnobState latest_state_ = {};
+        PB_SmartKnobConfig latest_config_ = {};
 
         QueueHandle_t log_queue_;
         QueueHandle_t knob_state_queue_;
@@ -53,4 +59,6 @@ class InterfaceTask : public Task<InterfaceTask>, public Logger {
 
         void changeConfig(bool next);
         void updateHardware();
+        void publishState();
+        void applyConfig(PB_SmartKnobConfig& config, bool from_remote);
 };

+ 0 - 2
firmware/src/motor_task.cpp

@@ -295,8 +295,6 @@ void MotorTask::run() {
             last_publish = millis();
         }
 
-        motor.monitor();
-
         delay(1);
     }
 }

+ 19 - 9
firmware/src/proto_gen/smartknob.pb.h

@@ -38,6 +38,10 @@ typedef struct _PB_SmartKnobConfig {
     pb_size_t detent_positions_count;
     int32_t detent_positions[5];
     float snap_point_bias;
+    /* *
+ Hue (0-255) for all 8 ring LEDs, if supported. Note: this will likely be replaced
+ with more configurability in a future protocol version. */
+    int16_t led_hue;
 } PB_SmartKnobConfig;
 
 typedef struct _PB_SmartKnobState {
@@ -45,6 +49,8 @@ typedef struct _PB_SmartKnobState {
     float sub_position_unit;
     bool has_config;
     PB_SmartKnobConfig config;
+    /* * Value that changes each time the knob is pressed */
+    uint8_t press_nonce;
 } PB_SmartKnobState;
 
 /* Message FROM the SmartKnob to the host */
@@ -103,8 +109,8 @@ extern "C" {
 #define PB_ToSmartknob_init_default              {0, 0, 0, {PB_RequestState_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_SmartKnobConfig_init_default          {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0}
+#define PB_SmartKnobState_init_default           {0, 0, false, PB_SmartKnobConfig_init_default, 0}
+#define PB_SmartKnobConfig_init_default          {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0, 0}
 #define PB_RequestState_init_default             {0}
 #define PB_PersistentConfiguration_init_default  {0, false, PB_MotorCalibration_init_default, false, PB_StrainCalibration_init_default}
 #define PB_MotorCalibration_init_default         {0, 0, 0, 0}
@@ -113,8 +119,8 @@ extern "C" {
 #define PB_ToSmartknob_init_zero                 {0, 0, 0, {PB_RequestState_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_SmartKnobConfig_init_zero             {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0}
+#define PB_SmartKnobState_init_zero              {0, 0, false, PB_SmartKnobConfig_init_zero, 0}
+#define PB_SmartKnobConfig_init_zero             {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0, 0}
 #define PB_RequestState_init_zero                {0}
 #define PB_PersistentConfiguration_init_zero     {0, false, PB_MotorCalibration_init_zero, false, PB_StrainCalibration_init_zero}
 #define PB_MotorCalibration_init_zero            {0, 0, 0, 0}
@@ -135,9 +141,11 @@ extern "C" {
 #define PB_SmartKnobConfig_text_tag              10
 #define PB_SmartKnobConfig_detent_positions_tag  11
 #define PB_SmartKnobConfig_snap_point_bias_tag   12
+#define PB_SmartKnobConfig_led_hue_tag           13
 #define PB_SmartKnobState_current_position_tag   1
 #define PB_SmartKnobState_sub_position_unit_tag  2
 #define PB_SmartKnobState_config_tag             3
+#define PB_SmartKnobState_press_nonce_tag        4
 #define PB_FromSmartKnob_protocol_version_tag    1
 #define PB_FromSmartKnob_ack_tag                 2
 #define PB_FromSmartKnob_log_tag                 3
@@ -191,7 +199,8 @@ X(a, STATIC,   SINGULAR, STRING,   msg,               1)
 #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)
+X(a, STATIC,   OPTIONAL, MESSAGE,  config,            3) \
+X(a, STATIC,   SINGULAR, UINT32,   press_nonce,       4)
 #define PB_SmartKnobState_CALLBACK NULL
 #define PB_SmartKnobState_DEFAULT NULL
 #define PB_SmartKnobState_config_MSGTYPE PB_SmartKnobConfig
@@ -208,7 +217,8 @@ X(a, STATIC,   SINGULAR, FLOAT,    endstop_strength_unit,   8) \
 X(a, STATIC,   SINGULAR, FLOAT,    snap_point,        9) \
 X(a, STATIC,   SINGULAR, STRING,   text,             10) \
 X(a, STATIC,   REPEATED, INT32,    detent_positions,  11) \
-X(a, STATIC,   SINGULAR, FLOAT,    snap_point_bias,  12)
+X(a, STATIC,   SINGULAR, FLOAT,    snap_point_bias,  12) \
+X(a, STATIC,   SINGULAR, INT32,    led_hue,          13)
 #define PB_SmartKnobConfig_CALLBACK NULL
 #define PB_SmartKnobConfig_DEFAULT NULL
 
@@ -270,10 +280,10 @@ extern const pb_msgdesc_t PB_StrainCalibration_msg;
 #define PB_MotorCalibration_size                 15
 #define PB_PersistentConfiguration_size          47
 #define PB_RequestState_size                     0
-#define PB_SmartKnobConfig_size                  173
-#define PB_SmartKnobState_size                   192
+#define PB_SmartKnobConfig_size                  184
+#define PB_SmartKnobState_size                   206
 #define PB_StrainCalibration_size                22
-#define PB_ToSmartknob_size                      185
+#define PB_ToSmartknob_size                      196
 
 #ifdef __cplusplus
 } /* extern "C" */

+ 1 - 1
firmware/src/serial/serial_protocol_plaintext.cpp

@@ -40,7 +40,7 @@ void SerialProtocolPlaintext::loop() {
                 demo_config_change_callback_();
             }
         } else if (b == 'C') {
-            motor_task_.runCalibration();
+            motor_calibration_callback_();
         } else if (b == 'S') {
             if (strain_calibration_callback_) {
                 strain_calibration_callback_();

+ 3 - 2
firmware/src/serial/serial_protocol_plaintext.h

@@ -2,6 +2,7 @@
 
 #include "../proto_gen/smartknob.pb.h"
 
+#include "interface_callbacks.h"
 #include "motor_task.h"
 #include "serial_protocol.h"
 #include "uart_stream.h"
@@ -11,7 +12,7 @@ typedef std::function<void(void)> StrainCalibrationCallback;
 
 class SerialProtocolPlaintext : public SerialProtocol {
     public:
-        SerialProtocolPlaintext(Stream& stream, MotorTask& motor_task) : SerialProtocol(), stream_(stream), motor_task_(motor_task) {}
+        SerialProtocolPlaintext(Stream& stream, MotorCalibrationCallback motor_calibration_callback) : SerialProtocol(), stream_(stream), motor_calibration_callback_(motor_calibration_callback) {}
         ~SerialProtocolPlaintext(){}
         void log(const char* msg) override;
         void loop() override;
@@ -21,7 +22,7 @@ class SerialProtocolPlaintext : public SerialProtocol {
     
     private:
         Stream& stream_;
-        MotorTask& motor_task_;
+        MotorCalibrationCallback motor_calibration_callback_;
         PB_SmartKnobState latest_state_ = {};
         DemoConfigChangeCallback demo_config_change_callback_;
         StrainCalibrationCallback strain_calibration_callback_;

+ 3 - 3
firmware/src/serial/serial_protocol_protobuf.cpp

@@ -14,10 +14,10 @@ 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) :
+SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback) :
         SerialProtocol(),
         stream_(stream),
-        motor_task_(motor_task),
+        config_callback_(config_callback),
         packet_serial_() {
     packet_serial_.setStream(&stream);
 
@@ -125,7 +125,7 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
     
     switch (pb_rx_buffer_.which_payload) {
         case PB_ToSmartknob_smartknob_config_tag: {
-            motor_task_.setConfig(pb_rx_buffer_.payload.smartknob_config);
+            config_callback_(pb_rx_buffer_.payload.smartknob_config);
             break;
         }
         default: {

+ 4 - 3
firmware/src/serial/serial_protocol_protobuf.h

@@ -4,21 +4,22 @@
 
 #include "../proto_gen/smartknob.pb.h"
 
+#include "interface_callbacks.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(){}
+        SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback);
+        ~SerialProtocolProtobuf(){};
         void log(const char* msg) override;
         void loop() override;
         void handleState(const PB_SmartKnobState& state) override;
     
     private:
         Stream& stream_;
-        MotorTask& motor_task_;
+        ConfigCallback config_callback_;
         
         PB_FromSmartKnob pb_tx_buffer_;
         PB_ToSmartknob pb_rx_buffer_;

+ 4 - 0
firmware/src/util.h

@@ -9,3 +9,7 @@ template <typename T> T CLAMP(const T& value, const T& low, const T& high)
 #define COUNT_OF(A) (sizeof(A) / sizeof(A[0]))
 
 float lerp(const float value, const float inMin, const float inMax, const float min, const float max);
+
+template <typename T> int sgn(T val) {
+    return (T(0) < val) - (val < T(0));
+}

+ 9 - 0
proto/smartknob.proto

@@ -41,6 +41,9 @@ message SmartKnobState {
     int32 current_position = 1;
     float sub_position_unit = 2;
     SmartKnobConfig config = 3;
+
+    /** Value that changes each time the knob is pressed */
+    uint32 press_nonce = 4 [(nanopb).int_size = IS_8];
 }
 
 
@@ -66,6 +69,12 @@ message SmartKnobConfig {
     string text = 10 [(nanopb).max_length = 50];
     repeated int32 detent_positions = 11 [(nanopb).max_count = 5];
     float snap_point_bias = 12;
+
+    /**
+     * Hue (0-255) for all 8 ring LEDs, if supported. Note: this will likely be replaced
+     * with more configurability in a future protocol version.
+     */
+    int32 led_hue = 13 [(nanopb).int_size = IS_16];
 }
 
 message RequestState {}

+ 2 - 0
software/js/packages/example-webserial-basic/src/App.tsx

@@ -21,6 +21,7 @@ const defaultConfig: Config = {
     text: 'Hello from\nweb serial!',
     detentPositions: [],
     snapPointBias: 0,
+    ledHue: 0,
 }
 
 export type AppProps = object
@@ -125,6 +126,7 @@ export const App: React.FC<AppProps> = () => {
                                         text: pendingSmartKnobConfig.text,
                                         detentPositions: [],
                                         snapPointBias: parseFloat(pendingSmartKnobConfig.snapPointBias) || 0,
+                                        ledHue: 0,
                                     })
                                 }}
                             >

+ 44 - 6
software/js/packages/example-webserial-timeline/src/App.tsx

@@ -17,9 +17,9 @@ const MAX_ZOOM = 60
 const PIXELS_PER_POSITION = 5
 
 enum Mode {
-    Scroll = 'Scroll',
-    Frames = 'Frames',
-    Speed = 'Speed',
+    Scroll = 'SKDEMO_Scroll',
+    Frames = 'SKDEMO_Frames',
+    Speed = 'SKDEMO_Speed',
 }
 
 type PlaybackState = {
@@ -58,6 +58,7 @@ export const App: React.FC<AppProps> = ({info}) => {
         text: Mode.Scroll,
         detentPositions: [],
         snapPointBias: 0,
+        ledHue: 0,
 
         zoomTimelinePixelsPerFrame: 0.1,
     })
@@ -78,6 +79,7 @@ export const App: React.FC<AppProps> = ({info}) => {
         smartKnobConfig.text,
         smartKnobConfig.detentPositions,
         smartKnobConfig.snapPointBias,
+        smartKnobConfig.ledHue,
     ])
     const [playbackState, setPlaybackState] = useState<PlaybackState>({
         speed: 0,
@@ -139,6 +141,7 @@ export const App: React.FC<AppProps> = ({info}) => {
                         text: Mode.Scroll,
                         detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), position, 5),
                         snapPointBias: 0,
+                        ledHue: 0,
 
                         zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
                     }
@@ -158,6 +161,7 @@ export const App: React.FC<AppProps> = ({info}) => {
                         text: Mode.Frames,
                         detentPositions: [],
                         snapPointBias: 0,
+                        ledHue: (120 * 255) / 360,
 
                         zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
                     }
@@ -177,6 +181,7 @@ export const App: React.FC<AppProps> = ({info}) => {
                         text: Mode.Speed,
                         detentPositions: [],
                         snapPointBias: 0.4,
+                        ledHue: (240 * 255) / 360,
 
                         zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
                     }
@@ -187,6 +192,21 @@ export const App: React.FC<AppProps> = ({info}) => {
         },
         [detentPositions, info.totalFrames, playbackState],
     )
+    const nextMode = useCallback(() => {
+        const curMode = smartKnobConfig.text as unknown as Mode
+        console.log('nextMode', curMode)
+        if (curMode === Mode.Scroll) {
+            changeMode(Mode.Frames)
+        } else if (curMode === Mode.Frames) {
+            changeMode(Mode.Speed)
+        } else if (curMode === Mode.Speed) {
+            changeMode(Mode.Scroll)
+        } else {
+            exhaustiveCheck(curMode)
+        }
+    }, [smartKnobConfig.text, changeMode])
+
+    // Initialize to Scroll mode
     useEffect(() => {
         changeMode(Mode.Scroll)
     }, [])
@@ -288,6 +308,21 @@ export const App: React.FC<AppProps> = ({info}) => {
         interfaceState.zoomTimelinePixelsPerFrame,
     ])
 
+    // Change mode when pressed
+    const receivedPressNonceRef = useRef<boolean>(false)
+    const previousPressNonceRef = useRef<number>(0)
+    useEffect(() => {
+        if (previousPressNonceRef.current !== smartKnobState.pressNonce) {
+            if (!receivedPressNonceRef.current) {
+                // Ignore first nonce change
+                receivedPressNonceRef.current = true
+            } else {
+                nextMode()
+            }
+        }
+        previousPressNonceRef.current = smartKnobState.pressNonce
+    }, [smartKnobState.pressNonce, nextMode])
+
     const refreshInterval = 20
     const updateFrame = useCallback(() => {
         const fps = info.frameRate * playbackState.speed
@@ -326,6 +361,9 @@ export const App: React.FC<AppProps> = ({info}) => {
     const connectToSerial = async () => {
         try {
             if (navigator.serial) {
+                previousPressNonceRef.current = 0
+                receivedPressNonceRef.current = false
+
                 const serialPort = await navigator.serial.requestPort({
                     filters: SmartKnobWebSerial.USB_DEVICE_FILTERS,
                 })
@@ -376,9 +414,9 @@ export const App: React.FC<AppProps> = ({info}) => {
                                 }}
                                 aria-label="Mode"
                             >
-                                {Object.keys(Mode).map((mode) => (
-                                    <ToggleButton value={mode} key={mode}>
-                                        {mode}
+                                {Object.entries(Mode).map((mode_entry) => (
+                                    <ToggleButton value={mode_entry[1]} key={mode_entry[1]}>
+                                        {mode_entry[0]}
                                     </ToggleButton>
                                 ))}
                             </ToggleButtonGroup>

+ 1 - 1
software/js/packages/example-webserial-timeline/src/util.ts

@@ -1,5 +1,5 @@
 export const exhaustiveCheck = (x: never): never => {
-    throw new Error("Didn't expect to get here", x)
+    throw new Error(`Unexpected value: ${x}`, x)
 }
 
 export const isSome = <T>(v: T | null | undefined): v is T => {

+ 13 - 8
software/js/packages/smartknobjs-webserial/src/index.ts

@@ -6,12 +6,7 @@ export class SmartKnobWebSerial extends SmartKnobCore {
 
     constructor(port: SerialPort, onMessage: MessageCallback) {
         super(onMessage, (packet: Uint8Array) => {
-            this.writer?.write(packet).catch((e) => {
-                console.error('Error writing serial', e)
-                this.port?.close()
-                this.port = null
-                this.portAvailable = false
-            })
+            this.writer?.write(packet).catch(this.onError)
         })
         this.port = port
         this.portAvailable = true
@@ -33,7 +28,9 @@ export class SmartKnobWebSerial extends SmartKnobCore {
 
         const reader = this.port.readable.getReader()
         try {
-            this.writer = this.port.writable.getWriter()
+            const writer = this.port.writable.getWriter()
+            writer.write(Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0])).catch(this.onError)
+            this.writer = writer
             try {
                 // eslint-disable-next-line no-constant-condition
                 while (true) {
@@ -47,11 +44,19 @@ export class SmartKnobWebSerial extends SmartKnobCore {
                 }
             } finally {
                 console.log('Releasing writer')
-                this.writer?.releaseLock()
+                writer?.releaseLock()
             }
         } finally {
             console.log('Releasing reader')
             reader.releaseLock()
         }
     }
+
+    private onError(e: unknown) {
+        console.error('Error writing serial', e)
+        this.port?.close()
+        this.port = null
+        this.portAvailable = false
+        this.writer = undefined
+    }
 }

File diff suppressed because it is too large
+ 0 - 0
software/python/proto_gen/smartknob_pb2.py


+ 4 - 0
software/python/smartknob_io.py

@@ -34,6 +34,7 @@ sys.path.append(os.path.join(software_root, 'proto_gen'))
 from proto_gen import smartknob_pb2
 
 SMARTKNOB_BAUD = 921600
+PROTOBUF_PROTOCOL_VERSION = 1
 
 
 class Smartknob(object):
@@ -92,6 +93,8 @@ class Smartknob(object):
         message = smartknob_pb2.FromSmartKnob()
         message.ParseFromString(payload)
         self._logger.debug(message)
+        if message.protocol_version != PROTOBUF_PROTOCOL_VERSION:
+            self._logger.warn(f'Invalid protocol version. Expected {PROTOBUF_PROTOCOL_VERSION}, received {message.protocol_version}')
 
         payload_type = message.WhichOneof('payload')
 
@@ -145,6 +148,7 @@ class Smartknob(object):
         nonce = self._next_nonce
         self._next_nonce += 1
 
+        message.protocol_version = PROTOBUF_PROTOCOL_VERSION
         message.nonce = nonce
 
         payload = bytearray(message.SerializeToString())

Some files were not shown because too many files changed in this diff