Forráskód Böngészése

Updated config schema, improved demo (#121)

- Breaking proto updates
- added position nonce to config, and added ability to specify a
sub_position via config as well => more flexibility to control position
via config
- added version as first field of serial protocol messages, so future
breaking proto updates can easily be detected and ignored
- Fixed up web-based demo of a mock video editor/timeline jog/playback
controller
- Fixed config/state flow so that mode changes correctly maintain
playback position
  - Demo: https://www.youtube.com/watch?v=J9192DfZplk
Scott Bezek 2 éve
szülő
commit
2afc38f7f9

+ 24 - 0
firmware/src/interface_task.cpp

@@ -27,6 +27,8 @@ Adafruit_VEML7700 veml = Adafruit_VEML7700();
 
 static PB_SmartKnobConfig configs[] = {
     // int32_t position;
+    // float sub_position_unit;
+    // uint8_t position_nonce;
     // int32_t min_position;
     // int32_t max_position;
     // float position_width_radians;
@@ -39,6 +41,8 @@ static PB_SmartKnobConfig configs[] = {
     // float snap_point_bias;
 
     {
+        0,
+        0,
         0,
         0,
         -1, // max position < min position indicates no bounds
@@ -54,6 +58,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        1,
+        0,
         10,
         10 * PI / 180,
         0,
@@ -67,6 +73,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        2,
+        0,
         72,
         10 * PI / 180,
         0,
@@ -80,6 +88,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        3,
+        0,
         1,
         60 * PI / 180,
         1,
@@ -93,6 +103,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        4,
+        0,
         0,
         60 * PI / 180,
         0.01,
@@ -106,6 +118,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         127,
         0,
+        5,
+        0,
         255,
         1 * PI / 180,
         0,
@@ -119,6 +133,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         127,
         0,
+        5,
+        0,
         255,
         1 * PI / 180,
         1,
@@ -132,6 +148,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        6,
+        0,
         31,
         8.225806452 * PI / 180,
         2,
@@ -145,6 +163,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        6,
+        0,
         31,
         8.225806452 * PI / 180,
         0.2,
@@ -158,6 +178,8 @@ static PB_SmartKnobConfig configs[] = {
     {
         0,
         0,
+        7,
+        0,
         31,
         7 * PI / 180,
         2.5,
@@ -170,6 +192,8 @@ static PB_SmartKnobConfig configs[] = {
     },
     {
         0,
+        0,
+        8,
         -6,
         6,
         60 * PI / 180,

+ 48 - 26
firmware/src/motor_task.cpp

@@ -96,11 +96,15 @@ void MotorTask::run() {
     float current_detent_center = motor.shaft_angle;
     PB_SmartKnobConfig config = {
         .position = 0,
+        .sub_position_unit = 0,
+        .position_nonce = 0,
         .min_position = 0,
         .max_position = 1,
         .position_width_radians = 60 * _PI / 180,
         .detent_strength_unit = 0,
     };
+    int32_t current_position = 0;
+    float latest_sub_position_unit = 0;
 
     float idle_check_velocity_ewma = 0;
     uint32_t last_idle_start = 0;
@@ -118,44 +122,60 @@ void MotorTask::run() {
                     break;
                 case CommandType::CONFIG: {
                     // Check new config for validity
-                    if (command.data.config.detent_strength_unit < 0) {
+                    PB_SmartKnobConfig& new_config = command.data.config;
+                    if (new_config.detent_strength_unit < 0) {
                         log("Ignoring invalid config: detent_strength_unit cannot be negative");
                         break;
                     }
-                    if (command.data.config.endstop_strength_unit < 0) {
+                    if (new_config.endstop_strength_unit < 0) {
                         log("Ignoring invalid config: endstop_strength_unit cannot be negative");
                         break;
                     }
-                    if (command.data.config.snap_point < 0.5) {
+                    if (new_config.snap_point < 0.5) {
                         log("Ignoring invalid config: snap_point must be >= 0.5 for stability");
                         break;
                     }
-                    if (command.data.config.detent_positions_count > COUNT_OF(command.data.config.detent_positions)) {
+                    if (new_config.detent_positions_count > COUNT_OF(new_config.detent_positions)) {
                         log("Ignoring invalid config: detent_positions_count is too large");
                         break;
                     }
-                    if (command.data.config.snap_point_bias < 0) {
+                    if (new_config.snap_point_bias < 0) {
                         log("Ignoring invalid config: snap_point_bias cannot be negative or there is risk of instability");
                         break;
                     }
 
                     // Change haptic input mode
-                    PB_SmartKnobConfig newConfig = command.data.config;
-                    if (newConfig.position == INT32_MIN) {
-                        // INT32_MIN indicates no change to position, so restore from latest_config
-                        log("maintaining position");
-                        newConfig.position = config.position;
+                    bool position_updated = false;
+                    if (new_config.position != config.position
+                            || new_config.sub_position_unit != config.sub_position_unit
+                            || new_config.position_nonce != config.position_nonce) {
+                        log("applying position change");
+                        current_position = new_config.position;
+                        position_updated = true;
                     }
-                    if (newConfig.position != config.position
-                            || newConfig.position_width_radians != config.position_width_radians) {
-                        // Only adjust the detent center if the position or width has changed
+
+                    if (new_config.min_position <= new_config.max_position) {
+                        // Only check bounds if min/max indicate bounds are active (min >= max)
+                        if (current_position < new_config.min_position) {
+                            current_position = new_config.min_position;
+                            log("adjusting position to min");
+                        } else if (current_position > new_config.max_position) {
+                            current_position = new_config.max_position;
+                            log("adjusting position to max");
+                        }
+                    }
+
+                    if (position_updated || new_config.position_width_radians != config.position_width_radians) {
                         log("adjusting detent center");
-                        current_detent_center = motor.shaft_angle;
+                        float new_sub_position = position_updated ? new_config.sub_position_unit : latest_sub_position_unit;
                         #if SK_INVERT_ROTATION
-                            current_detent_center = -motor.shaft_angle;
+                            float shaft_angle = -motor.shaft_angle;
+                        #else
+                            float shaft_angle = motor.shaft_angle;
                         #endif
+                        current_detent_center = shaft_angle + new_sub_position * new_config.position_width_radians;
                     }
-                    config = newConfig;
+                    config = new_config;
                     log("Got new config");
 
                     // Update derivative factor of torque controller based on detent width.
@@ -221,26 +241,28 @@ void MotorTask::run() {
 
         float snap_point_radians = config.position_width_radians * config.snap_point;
         float bias_radians = config.position_width_radians * config.snap_point_bias;
-        float snap_point_radians_decrease = snap_point_radians + (config.position <= 0 ? bias_radians : -bias_radians);
-        float snap_point_radians_increase = -snap_point_radians + (config.position >= 0 ? -bias_radians : bias_radians); 
+        float snap_point_radians_decrease = snap_point_radians + (current_position <= 0 ? bias_radians : -bias_radians);
+        float snap_point_radians_increase = -snap_point_radians + (current_position >= 0 ? -bias_radians : bias_radians); 
 
         int32_t num_positions = config.max_position - config.min_position + 1;
-        if (angle_to_detent_center > snap_point_radians_decrease && (num_positions <= 0 || config.position > config.min_position)) {
+        if (angle_to_detent_center > snap_point_radians_decrease && (num_positions <= 0 || current_position > config.min_position)) {
             current_detent_center += config.position_width_radians;
             angle_to_detent_center -= config.position_width_radians;
-            config.position--;
-        } else if (angle_to_detent_center < snap_point_radians_increase && (num_positions <= 0 || config.position < config.max_position)) {
+            current_position--;
+        } else if (angle_to_detent_center < snap_point_radians_increase && (num_positions <= 0 || current_position < config.max_position)) {
             current_detent_center -= config.position_width_radians;
             angle_to_detent_center += config.position_width_radians;
-            config.position++;
+            current_position++;
         }
 
+        latest_sub_position_unit = -angle_to_detent_center / config.position_width_radians;
+
         float dead_zone_adjustment = CLAMP(
             angle_to_detent_center,
             fmaxf(-config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, -DEAD_ZONE_RAD),
             fminf(config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, DEAD_ZONE_RAD));
 
-        bool out_of_bounds = num_positions > 0 && ((angle_to_detent_center > 0 && config.position == config.min_position) || (angle_to_detent_center < 0 && config.position == config.max_position));
+        bool out_of_bounds = num_positions > 0 && ((angle_to_detent_center > 0 && current_position == config.min_position) || (angle_to_detent_center < 0 && current_position == config.max_position));
         motor.PID_velocity.limit = 10; //out_of_bounds ? 10 : 3;
         motor.PID_velocity.P = out_of_bounds ? config.endstop_strength_unit * 4 : config.detent_strength_unit * 4;
 
@@ -254,7 +276,7 @@ void MotorTask::run() {
             if (!out_of_bounds && config.detent_positions_count > 0) {
                 bool in_detent = false;
                 for (uint8_t i = 0; i < config.detent_positions_count; i++) {
-                    if (config.detent_positions[i] == config.position) {
+                    if (config.detent_positions[i] == current_position) {
                         in_detent = true;
                         break;
                     }
@@ -273,8 +295,8 @@ void MotorTask::run() {
         // Publish current status to other registered tasks periodically
         if (millis() - last_publish > 5) {
             publish({
-                .current_position = config.position,
-                .sub_position_unit = -angle_to_detent_center / config.position_width_radians,
+                .current_position = current_position,
+                .sub_position_unit = latest_sub_position_unit,
                 .has_config = true,
                 .config = config,
             });

+ 3 - 3
firmware/src/proto_gen/smartknob.pb.c

@@ -9,6 +9,9 @@
 PB_BIND(PB_FromSmartKnob, PB_FromSmartKnob, 2)
 
 
+PB_BIND(PB_ToSmartknob, PB_ToSmartknob, AUTO)
+
+
 PB_BIND(PB_Ack, PB_Ack, AUTO)
 
 
@@ -18,9 +21,6 @@ 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)
 
 

+ 68 - 50
firmware/src/proto_gen/smartknob.pb.h

@@ -20,6 +20,14 @@ typedef struct _PB_Log {
 
 typedef struct _PB_SmartKnobConfig {
     int32_t position;
+    float sub_position_unit;
+    /* *
+ Position is normally only applied when it changes, but sometimes it's desirable
+ to reset the position to the same value, so a nonce change can be used to force
+ the position values to be applied as well.
+
+ NOTE: Must be < 256 */
+    uint8_t position_nonce;
     int32_t min_position;
     int32_t max_position;
     float position_width_radians;
@@ -39,8 +47,9 @@ typedef struct _PB_SmartKnobState {
     PB_SmartKnobConfig config;
 } PB_SmartKnobState;
 
-/* Message FROM the SmartKnob to USB host */
+/* Message FROM the SmartKnob to the host */
 typedef struct _PB_FromSmartKnob {
+    uint8_t protocol_version;
     pb_size_t which_payload;
     union {
         PB_Ack ack;
@@ -53,8 +62,9 @@ typedef struct _PB_RequestState {
     char dummy_field;
 } PB_RequestState;
 
-/* Message TO the Smartknob from the USB host */
+/* Message TO the Smartknob from the host */
 typedef struct _PB_ToSmartknob {
+    uint8_t protocol_version;
     uint32_t nonce;
     pb_size_t which_payload;
     union {
@@ -69,55 +79,70 @@ extern "C" {
 #endif
 
 /* Initializer values for message structs */
-#define PB_FromSmartKnob_init_default            {0, {PB_Ack_init_default}}
+#define PB_FromSmartKnob_init_default            {0, 0, {PB_Ack_init_default}}
+#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_ToSmartknob_init_default              {0, 0, {PB_RequestState_init_default}}
-#define PB_SmartKnobConfig_init_default          {0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0}
+#define PB_SmartKnobConfig_init_default          {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0}
 #define PB_RequestState_init_default             {0}
-#define PB_FromSmartKnob_init_zero               {0, {PB_Ack_init_zero}}
+#define PB_FromSmartKnob_init_zero               {0, 0, {PB_Ack_init_zero}}
+#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_ToSmartknob_init_zero                 {0, 0, {PB_RequestState_init_zero}}
-#define PB_SmartKnobConfig_init_zero             {0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0}
+#define PB_SmartKnobConfig_init_zero             {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {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_position_tag          1
-#define PB_SmartKnobConfig_min_position_tag      2
-#define PB_SmartKnobConfig_max_position_tag      3
-#define PB_SmartKnobConfig_position_width_radians_tag 4
-#define PB_SmartKnobConfig_detent_strength_unit_tag 5
-#define PB_SmartKnobConfig_endstop_strength_unit_tag 6
-#define PB_SmartKnobConfig_snap_point_tag        7
-#define PB_SmartKnobConfig_text_tag              8
-#define PB_SmartKnobConfig_detent_positions_tag  9
-#define PB_SmartKnobConfig_snap_point_bias_tag   10
+#define PB_SmartKnobConfig_sub_position_unit_tag 2
+#define PB_SmartKnobConfig_position_nonce_tag    3
+#define PB_SmartKnobConfig_min_position_tag      4
+#define PB_SmartKnobConfig_max_position_tag      5
+#define PB_SmartKnobConfig_position_width_radians_tag 6
+#define PB_SmartKnobConfig_detent_strength_unit_tag 7
+#define PB_SmartKnobConfig_endstop_strength_unit_tag 8
+#define PB_SmartKnobConfig_snap_point_tag        9
+#define PB_SmartKnobConfig_text_tag              10
+#define PB_SmartKnobConfig_detent_positions_tag  11
+#define PB_SmartKnobConfig_snap_point_bias_tag   12
 #define PB_SmartKnobState_current_position_tag   1
 #define PB_SmartKnobState_sub_position_unit_tag  2
 #define PB_SmartKnobState_config_tag             3
-#define PB_FromSmartKnob_ack_tag                 1
-#define PB_FromSmartKnob_log_tag                 2
-#define PB_FromSmartKnob_smartknob_state_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_protocol_version_tag    1
+#define PB_FromSmartKnob_ack_tag                 2
+#define PB_FromSmartKnob_log_tag                 3
+#define PB_FromSmartKnob_smartknob_state_tag     4
+#define PB_ToSmartknob_protocol_version_tag      1
+#define PB_ToSmartknob_nonce_tag                 2
+#define PB_ToSmartknob_request_state_tag         3
+#define PB_ToSmartknob_smartknob_config_tag      4
 
 /* 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)
+X(a, STATIC,   SINGULAR, UINT32,   protocol_version,   1) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,ack,payload.ack),   2) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,log,payload.log),   3) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,smartknob_state,payload.smartknob_state),   4)
 #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_ToSmartknob_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, UINT32,   protocol_version,   1) \
+X(a, STATIC,   SINGULAR, UINT32,   nonce,             2) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,request_state,payload.request_state),   3) \
+X(a, STATIC,   ONEOF,    MESSAGE,  (payload,smartknob_config,payload.smartknob_config),   4)
+#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_Ack_FIELDLIST(X, a) \
 X(a, STATIC,   SINGULAR, UINT32,   nonce,             1)
 #define PB_Ack_CALLBACK NULL
@@ -136,26 +161,19 @@ X(a, STATIC,   OPTIONAL, MESSAGE,  config,            3)
 #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,    position,          1) \
-X(a, STATIC,   SINGULAR, INT32,    min_position,      2) \
-X(a, STATIC,   SINGULAR, INT32,    max_position,      3) \
-X(a, STATIC,   SINGULAR, FLOAT,    position_width_radians,   4) \
-X(a, STATIC,   SINGULAR, FLOAT,    detent_strength_unit,   5) \
-X(a, STATIC,   SINGULAR, FLOAT,    endstop_strength_unit,   6) \
-X(a, STATIC,   SINGULAR, FLOAT,    snap_point,        7) \
-X(a, STATIC,   SINGULAR, STRING,   text,              8) \
-X(a, STATIC,   REPEATED, INT32,    detent_positions,   9) \
-X(a, STATIC,   SINGULAR, FLOAT,    snap_point_bias,  10)
+X(a, STATIC,   SINGULAR, FLOAT,    sub_position_unit,   2) \
+X(a, STATIC,   SINGULAR, UINT32,   position_nonce,    3) \
+X(a, STATIC,   SINGULAR, INT32,    min_position,      4) \
+X(a, STATIC,   SINGULAR, INT32,    max_position,      5) \
+X(a, STATIC,   SINGULAR, FLOAT,    position_width_radians,   6) \
+X(a, STATIC,   SINGULAR, FLOAT,    detent_strength_unit,   7) \
+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)
 #define PB_SmartKnobConfig_CALLBACK NULL
 #define PB_SmartKnobConfig_DEFAULT NULL
 
@@ -165,30 +183,30 @@ X(a, STATIC,   SINGULAR, FLOAT,    snap_point_bias,  10)
 #define PB_RequestState_DEFAULT NULL
 
 extern const pb_msgdesc_t PB_FromSmartKnob_msg;
+extern const pb_msgdesc_t PB_ToSmartknob_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_ToSmartknob_fields &PB_ToSmartknob_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_FromSmartKnob_size                    264
 #define PB_Log_size                              258
 #define PB_RequestState_size                     0
-#define PB_SmartKnobConfig_size                  165
-#define PB_SmartKnobState_size                   184
-#define PB_ToSmartknob_size                      174
+#define PB_SmartKnobConfig_size                  173
+#define PB_SmartKnobState_size                   192
+#define PB_ToSmartknob_size                      185
 
 #ifdef __cplusplus
 } /* extern "C" */

+ 4 - 0
firmware/src/proto_helpers.h → firmware/src/serial/proto_helpers.h

@@ -2,14 +2,18 @@
 
 #include "proto_gen/smartknob.pb.h"
 
+#define PROTOBUF_PROTOCOL_VERSION (1)
+
 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.position == second.position
+        && first.position_nonce == second.position_nonce
         && first.min_position == second.min_position
         && first.max_position == second.max_position
         && first.position_width_radians == second.position_width_radians
         && first.snap_point == second.snap_point
+        && first.sub_position_unit == second.sub_position_unit
         && strcmp(first.text, second.text) == 0
         && first.detent_positions_count == second.detent_positions_count
         && memcmp(first.detent_positions, second.detent_positions, first.detent_positions_count * sizeof(first.detent_positions[0]))

+ 9 - 1
firmware/src/serial/serial_protocol_protobuf.cpp

@@ -2,7 +2,7 @@
 
 #include "../proto_gen/smartknob.pb.h"
 
-#include "../proto_helpers.h"
+#include "proto_helpers.h"
 
 #include "crc32.h"
 #include "pb_encode.h"
@@ -105,6 +105,13 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
         return;
     }
 
+    if (pb_rx_buffer_.protocol_version != PROTOBUF_PROTOCOL_VERSION) {
+        char buf[200];
+        snprintf(buf, sizeof(buf), "Invalid protocol version. Expected %u, received %u", PROTOBUF_PROTOCOL_VERSION, pb_rx_buffer_.protocol_version);
+        log(buf);
+        return;
+    }
+
     // Always ACK immediately
     ack(pb_rx_buffer_.nonce);
     if (pb_rx_buffer_.nonce == last_nonce_) {
@@ -133,6 +140,7 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
 void SerialProtocolProtobuf::sendPbTxBuffer() {
     // Encode protobuf message to byte buffer
     pb_ostream_t stream = pb_ostream_from_buffer(tx_buffer_, sizeof(tx_buffer_));
+    pb_tx_buffer_.protocol_version = PROTOBUF_PROTOCOL_VERSION;
     if (!pb_encode(&stream, PB_FromSmartKnob_fields, &pb_tx_buffer_)) {
         stream_.println(stream.errmsg);
         stream_.flush();

+ 38 - 24
proto/smartknob.proto

@@ -5,13 +5,27 @@ import "nanopb.proto";
 package PB;
 
 /*
- * Message FROM the SmartKnob to USB host
+ * Message FROM the SmartKnob to the host
  */
 message FromSmartKnob {
+    uint32 protocol_version = 1 [(nanopb).int_size = IS_8];
     oneof payload {
-        Ack ack = 1;
-        Log log = 2;
-        SmartKnobState smartknob_state = 3;
+        Ack ack = 2;
+        Log log = 3;
+        SmartKnobState smartknob_state = 4;
+    }
+}
+
+/*
+ * Message TO the Smartknob from the host
+ */
+ message ToSmartknob {
+    uint32 protocol_version = 1 [(nanopb).int_size = IS_8];
+    uint32 nonce = 2;
+
+    oneof payload {
+        RequestState request_state = 3;
+        SmartKnobConfig smartknob_config = 4;
     }
 }
 
@@ -29,29 +43,29 @@ message SmartKnobState {
     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 position = 1;
-    int32 min_position = 2;
-    int32 max_position = 3;
-    float position_width_radians = 4;
-    float detent_strength_unit = 5;
-    float endstop_strength_unit = 6;
-    float snap_point = 7;
-    string text = 8 [(nanopb).max_length = 50];
-    repeated int32 detent_positions = 9 [(nanopb).max_count = 5];
-    float snap_point_bias = 10;
+    float sub_position_unit = 2;
+    
+    /**
+     * Position is normally only applied when it changes, but sometimes it's desirable
+     * to reset the position to the same value, so a nonce change can be used to force
+     * the position values to be applied as well.
+     *
+     * NOTE: Must be < 256
+     */
+    uint32 position_nonce = 3 [(nanopb).int_size = IS_8];
+
+    int32 min_position = 4;
+    int32 max_position = 5;
+    float position_width_radians = 6;
+    float detent_strength_unit = 7;
+    float endstop_strength_unit = 8;
+    float snap_point = 9;
+    string text = 10 [(nanopb).max_length = 50];
+    repeated int32 detent_positions = 11 [(nanopb).max_count = 5];
+    float snap_point_bias = 12;
 }
 
 message RequestState {}

+ 38 - 10
software/js/package-lock.json

@@ -16874,7 +16874,7 @@
       "license": "Apache-2.0",
       "dependencies": {
         "serialport": "^9.2.4",
-        "smartknobjs": "^0.1.0",
+        "smartknobjs": "^1.0.0",
         "socket.io": "^4.5.4"
       },
       "devDependencies": {
@@ -16908,7 +16908,7 @@
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-scripts": "^5.0.1",
-        "smartknobjs-proto": "^0.1.1",
+        "smartknobjs-proto": "^1.0.0",
         "socket.io-client": "^4.5.4",
         "typescript": "^4.9.3",
         "web-vitals": "^2.1.4"
@@ -16927,7 +16927,7 @@
       "license": "Apache-2.0",
       "dependencies": {
         "serialport": "^9.2.4",
-        "smartknobjs": "^0.1.0"
+        "smartknobjs": "^1.0.0"
       },
       "devDependencies": {
         "@types/serialport": "^8.0.2",
@@ -16940,13 +16940,13 @@
       }
     },
     "packages/smartknobjs": {
-      "version": "0.1.0",
+      "version": "1.0.0",
       "license": "Apache-2.0",
       "dependencies": {
         "cobs": "^0.2.1",
         "crc-32": "^1.2.0",
         "serialport": "^9.2.4",
-        "smartknobjs-proto": "^0.1.0"
+        "smartknobjs-proto": "^1.0.0"
       },
       "devDependencies": {
         "@types/serialport": "^8.0.2",
@@ -16959,12 +16959,40 @@
       }
     },
     "packages/smartknobjs-proto": {
-      "version": "0.1.1",
+      "version": "1.0.0",
       "license": "Apache-2.0",
       "dependencies": {
         "protobufjs": "^6.11.2"
       }
     },
+    "packages/webserial": {
+      "version": "0.1.0",
+      "extraneous": true,
+      "dependencies": {
+        "@emotion/react": "^11.10.5",
+        "@emotion/styled": "^11.10.5",
+        "@fontsource/roboto": "^4.5.8",
+        "@mui/material": "^5.10.16",
+        "@testing-library/jest-dom": "^5.16.5",
+        "@testing-library/react": "^13.4.0",
+        "@testing-library/user-event": "^13.5.0",
+        "@types/jest": "^27.5.2",
+        "@types/node": "^16.18.4",
+        "@types/react": "^18.0.25",
+        "@types/react-dom": "^18.0.9",
+        "lodash": "^4.17.21",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0",
+        "react-scripts": "^5.0.1",
+        "smartknobjs-proto": "^1.0.0",
+        "socket.io-client": "^4.5.4",
+        "typescript": "^4.9.3",
+        "web-vitals": "^2.1.4"
+      },
+      "devDependencies": {
+        "@types/lodash": "^4.14.191"
+      }
+    },
     "smartknobjs": {
       "version": "0.1.0",
       "extraneous": true,
@@ -21779,7 +21807,7 @@
         "nodemon": "^2.0.20",
         "prettier": "^2.4.1",
         "serialport": "^9.2.4",
-        "smartknobjs": "^0.1.0",
+        "smartknobjs": "^1.0.0",
         "socket.io": "^4.5.4",
         "ts-node": "^10.2.1",
         "typescript": "^4.9.3"
@@ -21804,7 +21832,7 @@
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-scripts": "^5.0.1",
-        "smartknobjs-proto": "^0.1.1",
+        "smartknobjs-proto": "^1.0.0",
         "socket.io-client": "^4.5.4",
         "typescript": "^4.9.3",
         "web-vitals": "^2.1.4"
@@ -22793,7 +22821,7 @@
         "eslint": "^8.25.0",
         "prettier": "^2.4.1",
         "serialport": "^9.2.4",
-        "smartknobjs": "^0.1.0",
+        "smartknobjs": "^1.0.0",
         "ts-node": "^10.2.1",
         "typescript": "^4.8.4"
       }
@@ -27375,7 +27403,7 @@
         "eslint": "^8.25.0",
         "prettier": "^2.4.1",
         "serialport": "^9.2.4",
-        "smartknobjs-proto": "^0.1.0",
+        "smartknobjs-proto": "^1.0.0",
         "ts-node": "^10.2.1",
         "typescript": "^4.8.4"
       }

+ 30 - 30
software/js/packages/demo-backend/package.json

@@ -1,32 +1,32 @@
 {
-  "name": "demo-backend",
-  "version": "0.1.0",
-  "description": "",
-  "main": "dist/index.js",
-  "types": "dist/index.d.ts",
-  "scripts": {
-    "build": "tsc",
-    "format": "prettier --write \"**/*.+(js|ts|json)\"",
-    "lint": "eslint --ext .js,.ts .",
-    "start": "PORT=3001 ts-node src/index.ts"
-  },
-  "author": "",
-  "license": "Apache-2.0",
-  "dependencies": {
-    "serialport": "^9.2.4",
-    "smartknobjs": "^0.1.0",
-    "socket.io": "^4.5.4"
-  },
-  "devDependencies": {
-    "@types/express": "^4.17.14",
-    "@types/node": "^18.11.10",
-    "@types/serialport": "^8.0.2",
-    "@typescript-eslint/eslint-plugin": "^5.40.1",
-    "@typescript-eslint/parser": "^5.40.1",
-    "eslint": "^8.25.0",
-    "nodemon": "^2.0.20",
-    "prettier": "^2.4.1",
-    "ts-node": "^10.2.1",
-    "typescript": "^4.9.3"
-  }
+    "name": "demo-backend",
+    "version": "0.1.0",
+    "description": "",
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "scripts": {
+        "build": "tsc",
+        "format": "prettier --write \"**/*.+(js|ts|json)\"",
+        "lint": "eslint --ext .js,.ts .",
+        "start": "PORT=3001 ts-node src/index.ts"
+    },
+    "author": "",
+    "license": "Apache-2.0",
+    "dependencies": {
+        "serialport": "^9.2.4",
+        "smartknobjs": "^1.0.0",
+        "socket.io": "^4.5.4"
+    },
+    "devDependencies": {
+        "@types/express": "^4.17.14",
+        "@types/node": "^18.11.10",
+        "@types/serialport": "^8.0.2",
+        "@typescript-eslint/eslint-plugin": "^5.40.1",
+        "@typescript-eslint/parser": "^5.40.1",
+        "eslint": "^8.25.0",
+        "nodemon": "^2.0.20",
+        "prettier": "^2.4.1",
+        "ts-node": "^10.2.1",
+        "typescript": "^4.9.3"
+    }
 }

+ 4 - 2
software/js/packages/demo-backend/src/index.ts

@@ -13,8 +13,10 @@ const start = async () => {
         // 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?.toLowerCase() === '1a86'.toLowerCase() &&
-            portInfo.productId?.toLowerCase() === '7523'.toLowerCase()
+            (portInfo.vendorId?.toLowerCase() === '1a86'.toLowerCase() &&
+                portInfo.productId?.toLowerCase() === '7523'.toLowerCase()) ||
+            (portInfo.vendorId?.toLowerCase() === '303a'.toLowerCase() &&
+                portInfo.productId?.toLowerCase() === '1001'.toLowerCase())
             // && portInfo.serialNumber === 'DEADBEEF'
         )
     })

+ 1 - 1
software/js/packages/demo-frontend/package.json

@@ -18,7 +18,7 @@
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-scripts": "^5.0.1",
-        "smartknobjs-proto": "^0.1.1",
+        "smartknobjs-proto": "^1.0.0",
         "socket.io-client": "^4.5.4",
         "typescript": "^4.9.3",
         "web-vitals": "^2.1.4"

+ 265 - 176
software/js/packages/demo-frontend/src/App.tsx

@@ -1,4 +1,4 @@
-import React, {useEffect, useMemo, useRef, useState} from 'react'
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
 import io from 'socket.io-client'
 import Typography from '@mui/material/Typography'
 import Container from '@mui/material/Container'
@@ -7,7 +7,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
 import {PB} from 'smartknobjs-proto'
 import {VideoInfo} from './types'
 import {Card, CardContent} from '@mui/material'
-import {exhaustiveCheck, findNClosest, INT32_MIN, lerp, NoUndefinedField} from './util'
+import {exhaustiveCheck, findNClosest, lerp, NoUndefinedField} from './util'
 import {groupBy, parseInt} from 'lodash'
 
 const socket = io()
@@ -15,7 +15,7 @@ const socket = io()
 const MIN_ZOOM = 0.01
 const MAX_ZOOM = 60
 
-const PIXELS_PER_POSITION = 10
+const PIXELS_PER_POSITION = 5
 
 enum Mode {
     Scroll = 'Scroll',
@@ -23,10 +23,16 @@ enum Mode {
     Speed = 'Speed',
 }
 
-type State = {
-    mode: Mode
-    playbackSpeed: number
+type PlaybackState = {
+    speed: number
     currentFrame: number
+}
+
+type InterfaceState = {
+    zoomTimelinePixelsPerFrame: number
+}
+
+type Config = NoUndefinedField<PB.ISmartKnobConfig> & {
     zoomTimelinePixelsPerFrame: number
 }
 
@@ -35,197 +41,283 @@ export type AppProps = {
 }
 export const App: React.FC<AppProps> = ({info}) => {
     const [isConnected, setIsConnected] = useState(socket.connected)
-    const [state, setState] = useState<NoUndefinedField<PB.ISmartKnobState>>(
+
+    const [smartKnobState, setSmartKnobState] = useState<NoUndefinedField<PB.ISmartKnobState>>(
         PB.SmartKnobState.toObject(PB.SmartKnobState.create({config: PB.SmartKnobConfig.create()}), {
             defaults: true,
         }) as NoUndefinedField<PB.ISmartKnobState>,
     )
-    const [derivedState, setDerivedState] = useState<State>({
-        mode: Mode.Scroll,
-        playbackSpeed: 0,
+    const [smartKnobConfig, setSmartKnobConfig] = useState<Config>({
+        position: 0,
+        subPositionUnit: 0,
+        positionNonce: Math.floor(Math.random() * 255),
+        minPosition: 0,
+        maxPosition: 0,
+        positionWidthRadians: (15 * Math.PI) / 180,
+        detentStrengthUnit: 0,
+        endstopStrengthUnit: 1,
+        snapPoint: 0.7,
+        text: Mode.Scroll,
+        detentPositions: [],
+        snapPointBias: 0,
+
+        zoomTimelinePixelsPerFrame: 0.1,
+    })
+    useEffect(() => {
+        console.log('send config', smartKnobConfig)
+        socket.emit('set_config', smartKnobConfig)
+    }, [
+        smartKnobConfig.position,
+        smartKnobConfig.subPositionUnit,
+        smartKnobConfig.positionNonce,
+        smartKnobConfig.minPosition,
+        smartKnobConfig.maxPosition,
+        smartKnobConfig.positionWidthRadians,
+        smartKnobConfig.detentStrengthUnit,
+        smartKnobConfig.endstopStrengthUnit,
+        smartKnobConfig.snapPoint,
+        smartKnobConfig.text,
+        smartKnobConfig.detentPositions,
+        smartKnobConfig.snapPointBias,
+    ])
+    const [playbackState, setPlaybackState] = useState<PlaybackState>({
+        speed: 0,
         currentFrame: 0,
+    })
+    const [interfaceState, setInterfaceState] = useState<InterfaceState>({
         zoomTimelinePixelsPerFrame: 0.1,
     })
 
-    useMemo(() => {
-        setDerivedState((cur) => {
-            const modeText = state.config.text
-            if (modeText === Mode.Scroll) {
-                const rawFrame = Math.trunc(
-                    ((state.currentPosition + state.subPositionUnit) * PIXELS_PER_POSITION) /
-                        cur.zoomTimelinePixelsPerFrame,
-                )
-                return {
-                    mode: Mode.Scroll,
-                    playbackSpeed: 0,
-                    currentFrame: Math.min(Math.max(rawFrame, 0), info.totalFrames - 1),
-                    zoomTimelinePixelsPerFrame: cur.zoomTimelinePixelsPerFrame,
-                }
-            } else if (modeText === Mode.Frames) {
-                return {
-                    mode: Mode.Frames,
-                    playbackSpeed: 0,
-                    currentFrame: state.currentPosition ?? 0,
-                    zoomTimelinePixelsPerFrame: cur.zoomTimelinePixelsPerFrame,
-                }
-            } else if (modeText === Mode.Speed) {
-                const normalizedWholeValue = state.currentPosition
-                const normalizedFractional =
-                    Math.sign(state.subPositionUnit) *
-                    lerp(state.subPositionUnit * Math.sign(state.subPositionUnit), 0.1, 0.9, 0, 1)
-                const normalized = normalizedWholeValue + normalizedFractional
-                const speed = Math.sign(normalized) * Math.pow(2, Math.abs(normalized) - 1)
-                return {
-                    mode: Mode.Speed,
-                    playbackSpeed: speed,
-                    currentFrame: cur.currentFrame,
-                    zoomTimelinePixelsPerFrame: cur.zoomTimelinePixelsPerFrame,
-                }
-            }
-            return cur
-        })
-    }, [state.config.text, state.currentPosition, state.subPositionUnit])
-
-    const totalPositions = Math.ceil((info.totalFrames * derivedState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION)
+    const totalPositions = Math.ceil(
+        (info.totalFrames * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
+    )
     const detentPositions = useMemo(() => {
         // Always include the first and last positions at detents
         const positionsToFrames = groupBy([0, ...info.boundaryFrames, info.totalFrames - 1], (frame) =>
-            Math.round((frame * derivedState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION),
+            Math.round((frame * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION),
         )
         console.log(JSON.stringify(positionsToFrames))
         return positionsToFrames
-    }, [info.boundaryFrames, totalPositions, derivedState.zoomTimelinePixelsPerFrame])
-
-    // Continuous config updates for scrolling, to update detent positions
-    useMemo(() => {
-        if (derivedState.mode === Mode.Scroll) {
-            const config = PB.SmartKnobConfig.create({
-                position: INT32_MIN,
-                minPosition: 0,
-                maxPosition: totalPositions - 1,
-                positionWidthRadians: (8 * Math.PI) / 180,
-                detentStrengthUnit: 2.5,
-                endstopStrengthUnit: 1,
-                snapPoint: 0.7,
-                text: Mode.Scroll,
-                detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), state.currentPosition, 5),
-                snapPointBias: 0,
-            })
-            socket.emit('set_config', config)
+    }, [info.boundaryFrames, info.totalFrames, totalPositions, smartKnobConfig.zoomTimelinePixelsPerFrame])
+
+    const scrollPositionWholeMemo = useMemo(() => {
+        const position = (playbackState.currentFrame * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
+        return Math.round(position)
+    }, [playbackState.currentFrame, smartKnobConfig.zoomTimelinePixelsPerFrame])
+    const nClosestMemo = useMemo(() => {
+        return findNClosest(Object.keys(detentPositions).map(parseInt), scrollPositionWholeMemo, 5).sort(
+            (a, b) => a - b,
+        )
+    }, [scrollPositionWholeMemo])
+
+    const changeMode = useCallback(
+        (newMode: Mode) => {
+            if (newMode === Mode.Scroll) {
+                setSmartKnobConfig((curConfig) => {
+                    const position =
+                        (playbackState.currentFrame * curConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
+                    const positionWhole = Math.round(position)
+                    const subPositionUnit = position - positionWhole
+                    return {
+                        position,
+                        subPositionUnit,
+                        positionNonce: (curConfig.positionNonce + 1) % 256,
+                        minPosition: 0,
+                        maxPosition: Math.trunc(
+                            ((info.totalFrames - 1) * curConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
+                        ),
+                        positionWidthRadians: (8 * Math.PI) / 180,
+                        detentStrengthUnit: 3,
+                        endstopStrengthUnit: 1,
+                        snapPoint: 0.7,
+                        text: Mode.Scroll,
+                        detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), position, 5),
+                        snapPointBias: 0,
+
+                        zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
+                    }
+                })
+            } else if (newMode === Mode.Frames) {
+                setSmartKnobConfig((curConfig) => {
+                    return {
+                        position: playbackState.currentFrame,
+                        subPositionUnit: 0,
+                        positionNonce: (curConfig.positionNonce + 1) % 256,
+                        minPosition: 0,
+                        maxPosition: info.totalFrames - 1,
+                        positionWidthRadians: (1.8 * Math.PI) / 180,
+                        detentStrengthUnit: 1,
+                        endstopStrengthUnit: 1,
+                        snapPoint: 1.1,
+                        text: Mode.Frames,
+                        detentPositions: [],
+                        snapPointBias: 0,
+
+                        zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
+                    }
+                })
+            } else if (newMode === Mode.Speed) {
+                setSmartKnobConfig((curConfig) => {
+                    return {
+                        position: 0,
+                        subPositionUnit: 0,
+                        positionNonce: (curConfig.positionNonce + 1) % 256,
+                        minPosition: playbackState.currentFrame === 0 ? 0 : -6,
+                        maxPosition: playbackState.currentFrame === info.totalFrames - 1 ? 0 : 6,
+                        positionWidthRadians: (60 * Math.PI) / 180,
+                        detentStrengthUnit: 1,
+                        endstopStrengthUnit: 1,
+                        snapPoint: 0.55,
+                        text: Mode.Speed,
+                        detentPositions: [],
+                        snapPointBias: 0.4,
+
+                        zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
+                    }
+                })
+            } else {
+                exhaustiveCheck(newMode)
+            }
+        },
+        [detentPositions, info.totalFrames, playbackState],
+    )
+
+    useEffect(() => {
+        if (smartKnobState.config.text === '') {
+            console.debug('No valid state yet')
+            return
         }
-    }, [derivedState.mode, derivedState.zoomTimelinePixelsPerFrame, detentPositions, state.currentPosition])
-
-    // For one-off config pushes, e.g. mode changes
-    const pushConfig = (state: State) => {
-        let config: PB.SmartKnobConfig
-        if (state.mode === Mode.Scroll) {
-            const position = Math.trunc((state.currentFrame * state.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION)
-            config = PB.SmartKnobConfig.create({
-                position,
-                minPosition: 0,
-                maxPosition: Math.trunc(
-                    ((info.totalFrames - 1) * state.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
-                ),
-                positionWidthRadians: (8 * Math.PI) / 180,
-                detentStrengthUnit: 2.5,
-                endstopStrengthUnit: 1,
-                snapPoint: 0.7,
-                text: Mode.Scroll,
-                detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), position, 5),
-                snapPointBias: 0,
+
+        const currentMode = smartKnobState.config.text as Mode
+        if (currentMode !== smartKnobConfig.text) {
+            console.debug('Mode mismatch, ignoring state', {configMode: smartKnobConfig.text, stateMode: currentMode})
+            return
+        }
+
+        // Update playbackState
+        if (currentMode === Mode.Scroll) {
+            // TODO: round input based on zoom level to avoid noise
+            const rawFrame = Math.trunc(
+                ((smartKnobState.currentPosition + smartKnobState.subPositionUnit) * PIXELS_PER_POSITION) /
+                    smartKnobConfig.zoomTimelinePixelsPerFrame,
+            )
+            const frame =
+                detentPositions[smartKnobState.currentPosition]?.[0] ??
+                Math.min(Math.max(rawFrame, 0), info.totalFrames - 1)
+            setPlaybackState({
+                speed: 0,
+                currentFrame: frame,
             })
-        } else if (state.mode === Mode.Frames) {
-            config = PB.SmartKnobConfig.create({
-                position: state.currentFrame,
-                minPosition: 0,
-                maxPosition: info.totalFrames - 1,
-                positionWidthRadians: (1.5 * Math.PI) / 180,
-                detentStrengthUnit: 1,
-                endstopStrengthUnit: 1,
-                snapPoint: 1.1,
-                text: Mode.Frames,
-                detentPositions: [],
-                snapPointBias: 0,
+
+            // Update config with N nearest detents
+            setSmartKnobConfig((curConfig) => {
+                let positionInfo: Partial<Config> = {}
+                if (interfaceState.zoomTimelinePixelsPerFrame !== curConfig.zoomTimelinePixelsPerFrame) {
+                    const position =
+                        (playbackState.currentFrame * interfaceState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
+                    const positionWhole = Math.round(position)
+                    const subPositionUnit = position - positionWhole
+                    positionInfo = {
+                        position,
+                        subPositionUnit,
+                        positionNonce: (curConfig.positionNonce + 1) % 256,
+                        minPosition: 0,
+                        maxPosition: Math.trunc(
+                            ((info.totalFrames - 1) * interfaceState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
+                        ),
+                        zoomTimelinePixelsPerFrame: interfaceState.zoomTimelinePixelsPerFrame,
+                    }
+                }
+                return {
+                    ...curConfig,
+                    ...positionInfo,
+                    detentPositions: nClosestMemo,
+                }
             })
-        } else if (state.mode === Mode.Speed) {
-            config = PB.SmartKnobConfig.create({
-                position: state.playbackSpeed === 0 ? 0 : INT32_MIN,
-                minPosition: state.currentFrame === 0 ? 0 : -6,
-                maxPosition: state.currentFrame === info.totalFrames - 1 ? 0 : 6,
-                positionWidthRadians: (60 * Math.PI) / 180,
-                detentStrengthUnit: 1,
-                endstopStrengthUnit: 1,
-                snapPoint: 0.55,
-                text: Mode.Speed,
-                detentPositions: [],
-                snapPointBias: 0.4,
+        } else if (currentMode === Mode.Frames) {
+            setPlaybackState({
+                speed: 0,
+                currentFrame: smartKnobState.currentPosition,
+            })
+            // No config updates needed
+        } else if (currentMode === Mode.Speed) {
+            const normalizedWholeValue = smartKnobState.currentPosition
+            const normalizedFractional =
+                Math.sign(smartKnobState.subPositionUnit) *
+                lerp(smartKnobState.subPositionUnit * Math.sign(smartKnobState.subPositionUnit), 0.1, 0.9, 0, 1)
+            const normalized = normalizedWholeValue + normalizedFractional
+            const speed = Math.sign(normalized) * Math.pow(2, Math.abs(normalized) - 1)
+            const roundedSpeed = Math.trunc(speed * 10) / 10
+            setPlaybackState((cur) => {
+                return {
+                    speed: roundedSpeed,
+                    currentFrame: cur.currentFrame,
+                }
+            })
+
+            // Update config with bounds depending on current frame
+            setSmartKnobConfig((curConfig) => {
+                return {
+                    ...curConfig,
+                    minPosition: playbackState.currentFrame === 0 ? 0 : -6,
+                    maxPosition: playbackState.currentFrame === info.totalFrames - 1 ? 0 : 6,
+                }
             })
         } else {
-            throw exhaustiveCheck(state.mode)
+            exhaustiveCheck(currentMode)
         }
-        socket.emit('set_config', config)
-    }
-
-    const setCurrentFrame = (fn: (oldFrame: number) => number) => {
-        setDerivedState((cur) => {
-            const newState = {...cur}
-            if (cur.mode === Mode.Speed) {
-                newState.currentFrame = fn(cur.currentFrame)
+    }, [
+        detentPositions,
+        nClosestMemo,
+        info.totalFrames,
+        smartKnobState.config.text,
+        smartKnobState.currentPosition,
+        smartKnobState.subPositionUnit,
+        smartKnobConfig.text,
+        playbackState.currentFrame,
+        playbackState.speed,
+        interfaceState.zoomTimelinePixelsPerFrame,
+    ])
+
+    const refreshInterval = 20
+    const updateFrame = useCallback(() => {
+        const fps = info.frameRate * playbackState.speed
+        setPlaybackState((cur) => {
+            const newFrame = cur.currentFrame + (fps * refreshInterval) / 1000
+            const clampedNewFrame = Math.min(Math.max(newFrame, 0), info.totalFrames - 1)
+            return {
+                speed: cur.speed,
+                currentFrame: clampedNewFrame,
             }
-            return newState
         })
-    }
+    }, [info.frameRate, playbackState.speed])
+
+    // Store the latest callback in a ref so the long-lived interval closure can invoke the latest version.
+    // See https://overreacted.io/making-setinterval-declarative-with-react-hooks/ for more
+    const savedCallback = useRef<() => void | null>()
+    useEffect(() => {
+        savedCallback.current = updateFrame
+    }, [updateFrame])
+
+    const isPlaying = useMemo(() => {
+        return playbackState.speed !== 0
+    }, [playbackState.speed])
 
-    // Timer for speed-based playback
     useEffect(() => {
-        const refreshInterval = 20
-        const fps = info.frameRate * derivedState.playbackSpeed
-        if (derivedState.mode === Mode.Speed && fps !== 0) {
+        if (smartKnobState.config.text === Mode.Speed && isPlaying) {
             const timer = setInterval(() => {
-                setCurrentFrame((oldFrame) => {
-                    const newFrame = oldFrame + (fps * refreshInterval) / 1000
-
-                    const oldFrameTrunc = Math.trunc(oldFrame)
-                    const newFrameTrunc = Math.trunc(newFrame)
-
-                    if (newFrame < 0 || newFrame >= info.totalFrames) {
-                        const clampedNewFrame = Math.min(Math.max(newFrame, 0), info.totalFrames - 1)
-                        if (oldFrame !== clampedNewFrame) {
-                            // If we've hit a boundary, push a config to set the bounds
-                            pushConfig({
-                                mode: Mode.Speed,
-                                playbackSpeed: 0,
-                                currentFrame: Math.trunc(clampedNewFrame),
-                                zoomTimelinePixelsPerFrame: derivedState.zoomTimelinePixelsPerFrame,
-                            })
-                        }
-                        return clampedNewFrame
-                    } else {
-                        if (
-                            (oldFrameTrunc === 0 && newFrameTrunc > 0) ||
-                            (oldFrameTrunc === info.totalFrames - 1 && newFrameTrunc < info.totalFrames - 1)
-                        ) {
-                            // If we've left a boundary condition, push a config to reset the bounds
-                            pushConfig({
-                                mode: derivedState.mode,
-                                playbackSpeed: 0,
-                                currentFrame: newFrameTrunc,
-                                zoomTimelinePixelsPerFrame: derivedState.zoomTimelinePixelsPerFrame,
-                            })
-                        }
-                        return newFrame
-                    }
-                })
+                if (savedCallback.current) {
+                    savedCallback.current()
+                }
             }, refreshInterval)
             return () => clearInterval(timer)
         }
-    }, [derivedState.mode, derivedState.playbackSpeed, info.totalFrames, info.frameRate])
+    }, [smartKnobState.config.text, isPlaying])
 
     // Socket.io subscription
     useEffect(() => {
         socket.on('connect', () => {
             setIsConnected(true)
-            pushConfig(derivedState)
         })
 
         socket.on('disconnect', () => {
@@ -237,7 +329,7 @@ export const App: React.FC<AppProps> = ({info}) => {
             const stateObj = PB.SmartKnobState.toObject(state, {
                 defaults: true,
             }) as NoUndefinedField<PB.ISmartKnobState>
-            setState(stateObj)
+            setSmartKnobState(stateObj)
         })
         return () => {
             socket.off('connect')
@@ -260,16 +352,13 @@ export const App: React.FC<AppProps> = ({info}) => {
                         )}
                         <ToggleButtonGroup
                             color="primary"
-                            value={derivedState.mode}
+                            value={smartKnobConfig.text}
                             exclusive
                             onChange={(e, value: Mode | null) => {
                                 if (value === null) {
                                     return
                                 }
-                                pushConfig({
-                                    ...derivedState,
-                                    mode: value,
-                                })
+                                changeMode(value)
                             }}
                             aria-label="Mode"
                         >
@@ -280,18 +369,18 @@ export const App: React.FC<AppProps> = ({info}) => {
                             ))}
                         </ToggleButtonGroup>
                         <Typography>
-                            Frame {Math.trunc(derivedState.currentFrame)} / {info.totalFrames - 1}
+                            Frame {Math.trunc(playbackState.currentFrame)} / {info.totalFrames - 1}
                             <br />
-                            Speed {Math.trunc(derivedState.playbackSpeed * 10) / 10}
+                            Speed {playbackState.speed}
                         </Typography>
                     </CardContent>
                 </Card>
                 <Timeline
                     info={info}
-                    currentFrame={derivedState.currentFrame}
-                    zoomTimelinePixelsPerFrame={derivedState.zoomTimelinePixelsPerFrame}
+                    currentFrame={playbackState.currentFrame}
+                    zoomTimelinePixelsPerFrame={interfaceState.zoomTimelinePixelsPerFrame}
                     adjustZoom={(factor) => {
-                        setDerivedState((cur) => {
+                        setInterfaceState((cur) => {
                             const newZoom = Math.min(
                                 Math.max(cur.zoomTimelinePixelsPerFrame * factor, MIN_ZOOM),
                                 MAX_ZOOM,
@@ -306,7 +395,7 @@ export const App: React.FC<AppProps> = ({info}) => {
                 />
                 <Card>
                     <CardContent>
-                        <div>{JSON.stringify(detentPositions)}</div>
+                        <div>{JSON.stringify(smartKnobConfig)}</div>
                     </CardContent>
                 </Card>
             </Container>

+ 1 - 1
software/js/packages/demo-frontend/src/index.tsx

@@ -17,7 +17,7 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
 const info: VideoInfo = {
     totalFrames: 30 * 60 * 5,
     frameRate: 30,
-    boundaryFrames: [312, 400, 1234, 1290, 3000, 4000],
+    boundaryFrames: [312, 400, 1234, 1290, 3000, 3300, 4000, 8000, 8100],
 }
 root.render(
     <React.StrictMode>

+ 0 - 2
software/js/packages/demo-frontend/src/util.ts

@@ -18,8 +18,6 @@ export type NoUndefinedField<T> = {
     [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>
 }
 
-export const INT32_MIN = -2147483648
-
 export function findNClosest(numbers: number[], target: number, n: number): number[] {
     // First, we sort the numbers in ascending order based on their absolute difference
     // from the target number. This means that the numbers that are closest to the target

+ 2 - 2
software/js/packages/example/package.json

@@ -1,7 +1,7 @@
 {
     "name": "example",
     "version": "0.1.0",
-    "description": "SmartKnob Interface Library",
+    "description": "SmartKnob Interface Example",
     "main": "dist/index.js",
     "types": "dist/index.d.ts",
     "scripts": {
@@ -14,7 +14,7 @@
     "license": "Apache-2.0",
     "dependencies": {
         "serialport": "^9.2.4",
-        "smartknobjs": "^0.1.0"
+        "smartknobjs": "^1.0.0"
     },
     "devDependencies": {
         "@types/serialport": "^8.0.2",

+ 5 - 1
software/js/packages/example/src/index.ts

@@ -30,9 +30,11 @@ const main = async () => {
         if (message.payload === 'log' && message.log) {
             console.log('LOG', message.log.msg)
         } else if (message.payload === 'smartknobState' && message.smartknobState) {
+            // Only log if it's a significant change (major position change, or at least 5 degrees)
+            const radianChange = (message.smartknobState.subPositionUnit ?? 0) * (message.smartknobState.config?.positionWidthRadians ?? 0) - (lastLoggedState?.subPositionUnit ?? 0) * (lastLoggedState?.config?.positionWidthRadians ?? 0)
             if (
                 message.smartknobState.currentPosition !== lastLoggedState?.currentPosition ||
-                Math.abs((message.smartknobState.subPositionUnit ?? 0) - (lastLoggedState?.subPositionUnit ?? 0)) > 1
+                Math.abs(radianChange)*180/Math.PI > 5
             ) {
                 console.log(
                     `State:\n${JSON.stringify(
@@ -50,6 +52,8 @@ const main = async () => {
             detentStrengthUnit: 1,
             endstopStrengthUnit: 1,
             position: 0,
+            subPositionUnit: 0,
+            positionNonce: Math.floor(Math.random()*255), // Pick a random nonce to force a position reset on start
             minPosition: 0,
             maxPosition: 4,
             positionWidthRadians: (10 * Math.PI) / 180,

+ 1 - 1
software/js/packages/smartknobjs-proto/package.json

@@ -1,6 +1,6 @@
 {
   "name": "smartknobjs-proto",
-  "version": "0.1.1",
+  "version": "1.0.0",
   "description": "SmartKnob Protobuf Generated Code",
   "main": "dist/smartknob_proto.js",
   "types": "dist/smartknob_proto.d.ts",

+ 2 - 2
software/js/packages/smartknobjs/package.json

@@ -1,6 +1,6 @@
 {
     "name": "smartknobjs",
-    "version": "0.1.0",
+    "version": "1.0.0",
     "description": "SmartKnob Interface Library",
     "main": "dist/index.js",
     "types": "dist/index.d.ts",
@@ -15,7 +15,7 @@
         "cobs": "^0.2.1",
         "crc-32": "^1.2.0",
         "serialport": "^9.2.4",
-        "smartknobjs-proto": "^0.1.0"
+        "smartknobjs-proto": "^1.0.0"
     },
     "devDependencies": {
         "@types/serialport": "^8.0.2",

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

@@ -1,9 +1,11 @@
 import SerialPort = require('serialport')
-import {decode, encode} from 'cobs'
+import {decode as cobsDecode, encode as cobsEncode} from 'cobs'
 import * as CRC32 from 'crc-32'
 
 import {PB} from 'smartknobjs-proto'
 
+const PROTOBUF_PROTOCOL_VERSION = 1
+
 export type MessageCallback = (message: PB.FromSmartKnob) => void
 
 type QueueEntry = {
@@ -83,7 +85,7 @@ export class SmartKnob {
         // 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
+            const packet = cobsDecode(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)}`)
@@ -109,6 +111,13 @@ export class SmartKnob {
                 return
             }
 
+            if (message.protocolVersion !== PROTOBUF_PROTOCOL_VERSION) {
+                console.warn(
+                    `Invalid protocol version. Expected ${PROTOBUF_PROTOCOL_VERSION}, received ${message.protocolVersion}`,
+                )
+                return
+            }
+
             if (message.payload === 'ack') {
                 const nonce = message.ack?.nonce ?? undefined
                 if (nonce === undefined) {
@@ -126,6 +135,7 @@ export class SmartKnob {
         if (this.port === null) {
             return
         }
+        message.protocolVersion = PROTOBUF_PROTOCOL_VERSION
         message.nonce = this.lastNonce++
 
         // Encode before enqueueing to ensure messages don't change once they're queued
@@ -173,7 +183,7 @@ export class SmartKnob {
         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])])
+        const encodedDelimitedPacket = Buffer.concat([cobsEncode(packet), Buffer.from([0])])
 
         this.retryTimeout = setTimeout(() => {
             this.retryTimeout = null

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 3
software/python/proto_gen/nanopb_pb2.py


+ 29 - 23
software/python/proto_gen/smartknob_pb2.py

@@ -15,15 +15,15 @@ _sym_db = _symbol_database.Default()
 import nanopb_pb2 as nanopb__pb2
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fsmartknob.proto\x12\x02PB\x1a\x0cnanopb.proto\"y\n\rFromSmartKnob\x12\x16\n\x03\x61\x63k\x18\x01 \x01(\x0b\x32\x07.PB.AckH\x00\x12\x16\n\x03log\x18\x02 \x01(\x0b\x32\x07.PB.LogH\x00\x12-\n\x0fsmartknob_state\x18\x03 \x01(\x0b\x32\x12.PB.SmartKnobStateH\x00\x42\t\n\x07payload\"\x14\n\x03\x41\x63k\x12\r\n\x05nonce\x18\x01 \x01(\r\"\x1a\n\x03Log\x12\x13\n\x03msg\x18\x01 \x01(\tB\x06\x92?\x03p\xff\x01\"j\n\x0eSmartKnobState\x12\x18\n\x10\x63urrent_position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12#\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfig\"\x83\x01\n\x0bToSmartknob\x12\r\n\x05nonce\x18\x01 \x01(\r\x12)\n\rrequest_state\x18\x02 \x01(\x0b\x32\x10.PB.RequestStateH\x00\x12/\n\x10smartknob_config\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfigH\x00\x42\t\n\x07payload\"\x8f\x02\n\x0fSmartKnobConfig\x12\x10\n\x08position\x18\x01 \x01(\x05\x12\x14\n\x0cmin_position\x18\x02 \x01(\x05\x12\x14\n\x0cmax_position\x18\x03 \x01(\x05\x12\x1e\n\x16position_width_radians\x18\x04 \x01(\x02\x12\x1c\n\x14\x64\x65tent_strength_unit\x18\x05 \x01(\x02\x12\x1d\n\x15\x65ndstop_strength_unit\x18\x06 \x01(\x02\x12\x12\n\nsnap_point\x18\x07 \x01(\x02\x12\x13\n\x04text\x18\x08 \x01(\tB\x05\x92?\x02p2\x12\x1f\n\x10\x64\x65tent_positions\x18\t \x03(\x05\x42\x05\x92?\x02\x10\x05\x12\x17\n\x0fsnap_point_bias\x18\n \x01(\x02\"\x0e\n\x0cRequestStateb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fsmartknob.proto\x12\x02PB\x1a\x0cnanopb.proto\"\x9a\x01\n\rFromSmartKnob\x12\x1f\n\x10protocol_version\x18\x01 \x01(\rB\x05\x92?\x02\x38\x08\x12\x16\n\x03\x61\x63k\x18\x02 \x01(\x0b\x32\x07.PB.AckH\x00\x12\x16\n\x03log\x18\x03 \x01(\x0b\x32\x07.PB.LogH\x00\x12-\n\x0fsmartknob_state\x18\x04 \x01(\x0b\x32\x12.PB.SmartKnobStateH\x00\x42\t\n\x07payload\"\xa4\x01\n\x0bToSmartknob\x12\x1f\n\x10protocol_version\x18\x01 \x01(\rB\x05\x92?\x02\x38\x08\x12\r\n\x05nonce\x18\x02 \x01(\r\x12)\n\rrequest_state\x18\x03 \x01(\x0b\x32\x10.PB.RequestStateH\x00\x12/\n\x10smartknob_config\x18\x04 \x01(\x0b\x32\x13.PB.SmartKnobConfigH\x00\x42\t\n\x07payload\"\x14\n\x03\x41\x63k\x12\r\n\x05nonce\x18\x01 \x01(\r\"\x1a\n\x03Log\x12\x13\n\x03msg\x18\x01 \x01(\tB\x06\x92?\x03p\xff\x01\"j\n\x0eSmartKnobState\x12\x18\n\x10\x63urrent_position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12#\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfig\"\xc9\x02\n\x0fSmartKnobConfig\x12\x10\n\x08position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12\x1d\n\x0eposition_nonce\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12\x14\n\x0cmin_position\x18\x04 \x01(\x05\x12\x14\n\x0cmax_position\x18\x05 \x01(\x05\x12\x1e\n\x16position_width_radians\x18\x06 \x01(\x02\x12\x1c\n\x14\x64\x65tent_strength_unit\x18\x07 \x01(\x02\x12\x1d\n\x15\x65ndstop_strength_unit\x18\x08 \x01(\x02\x12\x12\n\nsnap_point\x18\t \x01(\x02\x12\x13\n\x04text\x18\n \x01(\tB\x05\x92?\x02p2\x12\x1f\n\x10\x64\x65tent_positions\x18\x0b \x03(\x05\x42\x05\x92?\x02\x10\x05\x12\x17\n\x0fsnap_point_bias\x18\x0c \x01(\x02\"\x0e\n\x0cRequestStateb\x06proto3')
 
 
 
 _FROMSMARTKNOB = DESCRIPTOR.message_types_by_name['FromSmartKnob']
+_TOSMARTKNOB = DESCRIPTOR.message_types_by_name['ToSmartknob']
 _ACK = DESCRIPTOR.message_types_by_name['Ack']
 _LOG = DESCRIPTOR.message_types_by_name['Log']
 _SMARTKNOBSTATE = DESCRIPTOR.message_types_by_name['SmartKnobState']
-_TOSMARTKNOB = DESCRIPTOR.message_types_by_name['ToSmartknob']
 _SMARTKNOBCONFIG = DESCRIPTOR.message_types_by_name['SmartKnobConfig']
 _REQUESTSTATE = DESCRIPTOR.message_types_by_name['RequestState']
 FromSmartKnob = _reflection.GeneratedProtocolMessageType('FromSmartKnob', (_message.Message,), {
@@ -33,6 +33,13 @@ FromSmartKnob = _reflection.GeneratedProtocolMessageType('FromSmartKnob', (_mess
   })
 _sym_db.RegisterMessage(FromSmartKnob)
 
+ToSmartknob = _reflection.GeneratedProtocolMessageType('ToSmartknob', (_message.Message,), {
+  'DESCRIPTOR' : _TOSMARTKNOB,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.ToSmartknob)
+  })
+_sym_db.RegisterMessage(ToSmartknob)
+
 Ack = _reflection.GeneratedProtocolMessageType('Ack', (_message.Message,), {
   'DESCRIPTOR' : _ACK,
   '__module__' : 'smartknob_pb2'
@@ -54,13 +61,6 @@ SmartKnobState = _reflection.GeneratedProtocolMessageType('SmartKnobState', (_me
   })
 _sym_db.RegisterMessage(SmartKnobState)
 
-ToSmartknob = _reflection.GeneratedProtocolMessageType('ToSmartknob', (_message.Message,), {
-  'DESCRIPTOR' : _TOSMARTKNOB,
-  '__module__' : 'smartknob_pb2'
-  # @@protoc_insertion_point(class_scope:PB.ToSmartknob)
-  })
-_sym_db.RegisterMessage(ToSmartknob)
-
 SmartKnobConfig = _reflection.GeneratedProtocolMessageType('SmartKnobConfig', (_message.Message,), {
   'DESCRIPTOR' : _SMARTKNOBCONFIG,
   '__module__' : 'smartknob_pb2'
@@ -78,24 +78,30 @@ _sym_db.RegisterMessage(RequestState)
 if _descriptor._USE_C_DESCRIPTORS == False:
 
   DESCRIPTOR._options = None
+  _FROMSMARTKNOB.fields_by_name['protocol_version']._options = None
+  _FROMSMARTKNOB.fields_by_name['protocol_version']._serialized_options = b'\222?\0028\010'
+  _TOSMARTKNOB.fields_by_name['protocol_version']._options = None
+  _TOSMARTKNOB.fields_by_name['protocol_version']._serialized_options = b'\222?\0028\010'
   _LOG.fields_by_name['msg']._options = None
   _LOG.fields_by_name['msg']._serialized_options = b'\222?\003p\377\001'
+  _SMARTKNOBCONFIG.fields_by_name['position_nonce']._options = None
+  _SMARTKNOBCONFIG.fields_by_name['position_nonce']._serialized_options = b'\222?\0028\010'
   _SMARTKNOBCONFIG.fields_by_name['text']._options = None
   _SMARTKNOBCONFIG.fields_by_name['text']._serialized_options = b'\222?\002p2'
   _SMARTKNOBCONFIG.fields_by_name['detent_positions']._options = None
   _SMARTKNOBCONFIG.fields_by_name['detent_positions']._serialized_options = b'\222?\002\020\005'
-  _FROMSMARTKNOB._serialized_start=37
-  _FROMSMARTKNOB._serialized_end=158
-  _ACK._serialized_start=160
-  _ACK._serialized_end=180
-  _LOG._serialized_start=182
-  _LOG._serialized_end=208
-  _SMARTKNOBSTATE._serialized_start=210
-  _SMARTKNOBSTATE._serialized_end=316
-  _TOSMARTKNOB._serialized_start=319
-  _TOSMARTKNOB._serialized_end=450
-  _SMARTKNOBCONFIG._serialized_start=453
-  _SMARTKNOBCONFIG._serialized_end=724
-  _REQUESTSTATE._serialized_start=726
-  _REQUESTSTATE._serialized_end=740
+  _FROMSMARTKNOB._serialized_start=38
+  _FROMSMARTKNOB._serialized_end=192
+  _TOSMARTKNOB._serialized_start=195
+  _TOSMARTKNOB._serialized_end=359
+  _ACK._serialized_start=361
+  _ACK._serialized_end=381
+  _LOG._serialized_start=383
+  _LOG._serialized_end=409
+  _SMARTKNOBSTATE._serialized_start=411
+  _SMARTKNOBSTATE._serialized_end=517
+  _SMARTKNOBCONFIG._serialized_start=520
+  _SMARTKNOBCONFIG._serialized_end=849
+  _REQUESTSTATE._serialized_start=851
+  _REQUESTSTATE._serialized_end=865
 # @@protoc_insertion_point(module_scope)

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott