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
This commit is contained in:
Scott Bezek 2023-06-13 22:45:49 -07:00 committed by GitHub
parent 08146e292d
commit 2afc38f7f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 680 additions and 428 deletions

View File

@ -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,

View File

@ -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,
});

View File

@ -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)

View File

@ -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" */

View File

@ -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]))

View File

@ -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();

View File

@ -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 {}

View File

@ -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"
}

View File

@ -14,7 +14,7 @@
"license": "Apache-2.0",
"dependencies": {
"serialport": "^9.2.4",
"smartknobjs": "^0.1.0",
"smartknobjs": "^1.0.0",
"socket.io": "^4.5.4"
},
"devDependencies": {

View File

@ -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'
)
})

View File

@ -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"

View File

@ -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,123 +41,129 @@ 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,
currentFrame: 0,
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 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),
)
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,
const [smartKnobConfig, setSmartKnobConfig] = useState<Config>({
position: 0,
subPositionUnit: 0,
positionNonce: Math.floor(Math.random() * 255),
minPosition: 0,
maxPosition: totalPositions - 1,
positionWidthRadians: (8 * Math.PI) / 180,
detentStrengthUnit: 2.5,
maxPosition: 0,
positionWidthRadians: (15 * Math.PI) / 180,
detentStrengthUnit: 0,
endstopStrengthUnit: 1,
snapPoint: 0.7,
text: Mode.Scroll,
detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), state.currentPosition, 5),
detentPositions: [],
snapPointBias: 0,
})
socket.emit('set_config', config)
}
}, [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({
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,
})
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 * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION),
)
console.log(JSON.stringify(positionsToFrames))
return positionsToFrames
}, [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) * state.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
((info.totalFrames - 1) * curConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
),
positionWidthRadians: (8 * Math.PI) / 180,
detentStrengthUnit: 2.5,
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 (state.mode === Mode.Frames) {
config = PB.SmartKnobConfig.create({
position: state.currentFrame,
} 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.5 * Math.PI) / 180,
positionWidthRadians: (1.8 * Math.PI) / 180,
detentStrengthUnit: 1,
endstopStrengthUnit: 1,
snapPoint: 1.1,
text: Mode.Frames,
detentPositions: [],
snapPointBias: 0,
zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
}
})
} 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,
} 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,
@ -159,73 +171,153 @@ export const App: React.FC<AppProps> = ({info}) => {
text: Mode.Speed,
detentPositions: [],
snapPointBias: 0.4,
zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
}
})
} else {
throw exhaustiveCheck(state.mode)
}
socket.emit('set_config', config)
exhaustiveCheck(newMode)
}
},
[detentPositions, info.totalFrames, playbackState],
)
const setCurrentFrame = (fn: (oldFrame: number) => number) => {
setDerivedState((cur) => {
const newState = {...cur}
if (cur.mode === Mode.Speed) {
newState.currentFrame = fn(cur.currentFrame)
}
return newState
})
}
// Timer for speed-based playback
useEffect(() => {
const refreshInterval = 20
const fps = info.frameRate * derivedState.playbackSpeed
if (derivedState.mode === Mode.Speed && fps !== 0) {
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,
})
if (smartKnobState.config.text === '') {
console.debug('No valid state yet')
return
}
return clampedNewFrame
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,
})
// 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 (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 {
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,
})
exhaustiveCheck(currentMode)
}
return newFrame
}, [
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,
}
})
}, [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])
useEffect(() => {
if (smartKnobState.config.text === Mode.Speed && isPlaying) {
const timer = setInterval(() => {
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>

View File

@ -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>

View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: nanopb.proto
import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
@ -20,8 +19,9 @@ DESCRIPTOR = _descriptor.FileDescriptor(
name='nanopb.proto',
package='',
syntax='proto2',
serialized_options=_b('\n\030fi.kapsi.koti.jpa.nanopb'),
serialized_pb=_b('\n\x0cnanopb.proto\x1a google/protobuf/descriptor.proto\"\xa4\x07\n\rNanoPBOptions\x12\x10\n\x08max_size\x18\x01 \x01(\x05\x12\x12\n\nmax_length\x18\x0e \x01(\x05\x12\x11\n\tmax_count\x18\x02 \x01(\x05\x12&\n\x08int_size\x18\x07 \x01(\x0e\x32\x08.IntSize:\nIS_DEFAULT\x12$\n\x04type\x18\x03 \x01(\x0e\x32\n.FieldType:\nFT_DEFAULT\x12\x18\n\nlong_names\x18\x04 \x01(\x08:\x04true\x12\x1c\n\rpacked_struct\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bpacked_enum\x18\n \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0cskip_message\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tno_unions\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05msgid\x18\t \x01(\r\x12\x1e\n\x0f\x61nonymous_oneof\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x15\n\x06proto3\x18\x0c \x01(\x08:\x05\x66\x61lse\x12#\n\x14proto3_singular_msgs\x18\x15 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x0e\x65num_to_string\x18\r \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0c\x66ixed_length\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x66ixed_count\x18\x10 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0fsubmsg_callback\x18\x16 \x01(\x08:\x05\x66\x61lse\x12/\n\x0cmangle_names\x18\x11 \x01(\x0e\x32\x11.TypenameMangling:\x06M_NONE\x12(\n\x11\x63\x61llback_datatype\x18\x12 \x01(\t:\rpb_callback_t\x12\x34\n\x11\x63\x61llback_function\x18\x13 \x01(\t:\x19pb_default_field_callback\x12\x30\n\x0e\x64\x65scriptorsize\x18\x14 \x01(\x0e\x32\x0f.DescriptorSize:\x07\x44S_AUTO\x12\x1a\n\x0b\x64\x65\x66\x61ult_has\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x0f\n\x07include\x18\x18 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x1a \x03(\t\x12\x0f\n\x07package\x18\x19 \x01(\t\x12\x41\n\rtype_override\x18\x1b \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x19\n\x0bsort_by_tag\x18\x1c \x01(\x08:\x04true\x12.\n\rfallback_type\x18\x1d \x01(\x0e\x32\n.FieldType:\x0b\x46T_CALLBACK*i\n\tFieldType\x12\x0e\n\nFT_DEFAULT\x10\x00\x12\x0f\n\x0b\x46T_CALLBACK\x10\x01\x12\x0e\n\nFT_POINTER\x10\x04\x12\r\n\tFT_STATIC\x10\x02\x12\r\n\tFT_IGNORE\x10\x03\x12\r\n\tFT_INLINE\x10\x05*D\n\x07IntSize\x12\x0e\n\nIS_DEFAULT\x10\x00\x12\x08\n\x04IS_8\x10\x08\x12\t\n\x05IS_16\x10\x10\x12\t\n\x05IS_32\x10 \x12\t\n\x05IS_64\x10@*Z\n\x10TypenameMangling\x12\n\n\x06M_NONE\x10\x00\x12\x13\n\x0fM_STRIP_PACKAGE\x10\x01\x12\r\n\tM_FLATTEN\x10\x02\x12\x16\n\x12M_PACKAGE_INITIALS\x10\x03*E\n\x0e\x44\x65scriptorSize\x12\x0b\n\x07\x44S_AUTO\x10\x00\x12\x08\n\x04\x44S_1\x10\x01\x12\x08\n\x04\x44S_2\x10\x02\x12\x08\n\x04\x44S_4\x10\x04\x12\x08\n\x04\x44S_8\x10\x08:E\n\x0enanopb_fileopt\x12\x1c.google.protobuf.FileOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:G\n\rnanopb_msgopt\x12\x1f.google.protobuf.MessageOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:E\n\x0enanopb_enumopt\x12\x1c.google.protobuf.EnumOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:>\n\x06nanopb\x12\x1d.google.protobuf.FieldOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptionsB\x1a\n\x18\x66i.kapsi.koti.jpa.nanopb')
serialized_options=b'\n\030fi.kapsi.koti.jpa.nanopb',
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x0cnanopb.proto\x1a google/protobuf/descriptor.proto\"\xa4\x07\n\rNanoPBOptions\x12\x10\n\x08max_size\x18\x01 \x01(\x05\x12\x12\n\nmax_length\x18\x0e \x01(\x05\x12\x11\n\tmax_count\x18\x02 \x01(\x05\x12&\n\x08int_size\x18\x07 \x01(\x0e\x32\x08.IntSize:\nIS_DEFAULT\x12$\n\x04type\x18\x03 \x01(\x0e\x32\n.FieldType:\nFT_DEFAULT\x12\x18\n\nlong_names\x18\x04 \x01(\x08:\x04true\x12\x1c\n\rpacked_struct\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bpacked_enum\x18\n \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0cskip_message\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tno_unions\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05msgid\x18\t \x01(\r\x12\x1e\n\x0f\x61nonymous_oneof\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x15\n\x06proto3\x18\x0c \x01(\x08:\x05\x66\x61lse\x12#\n\x14proto3_singular_msgs\x18\x15 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x0e\x65num_to_string\x18\r \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0c\x66ixed_length\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x66ixed_count\x18\x10 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0fsubmsg_callback\x18\x16 \x01(\x08:\x05\x66\x61lse\x12/\n\x0cmangle_names\x18\x11 \x01(\x0e\x32\x11.TypenameMangling:\x06M_NONE\x12(\n\x11\x63\x61llback_datatype\x18\x12 \x01(\t:\rpb_callback_t\x12\x34\n\x11\x63\x61llback_function\x18\x13 \x01(\t:\x19pb_default_field_callback\x12\x30\n\x0e\x64\x65scriptorsize\x18\x14 \x01(\x0e\x32\x0f.DescriptorSize:\x07\x44S_AUTO\x12\x1a\n\x0b\x64\x65\x66\x61ult_has\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x0f\n\x07include\x18\x18 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x1a \x03(\t\x12\x0f\n\x07package\x18\x19 \x01(\t\x12\x41\n\rtype_override\x18\x1b \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x19\n\x0bsort_by_tag\x18\x1c \x01(\x08:\x04true\x12.\n\rfallback_type\x18\x1d \x01(\x0e\x32\n.FieldType:\x0b\x46T_CALLBACK*i\n\tFieldType\x12\x0e\n\nFT_DEFAULT\x10\x00\x12\x0f\n\x0b\x46T_CALLBACK\x10\x01\x12\x0e\n\nFT_POINTER\x10\x04\x12\r\n\tFT_STATIC\x10\x02\x12\r\n\tFT_IGNORE\x10\x03\x12\r\n\tFT_INLINE\x10\x05*D\n\x07IntSize\x12\x0e\n\nIS_DEFAULT\x10\x00\x12\x08\n\x04IS_8\x10\x08\x12\t\n\x05IS_16\x10\x10\x12\t\n\x05IS_32\x10 \x12\t\n\x05IS_64\x10@*Z\n\x10TypenameMangling\x12\n\n\x06M_NONE\x10\x00\x12\x13\n\x0fM_STRIP_PACKAGE\x10\x01\x12\r\n\tM_FLATTEN\x10\x02\x12\x16\n\x12M_PACKAGE_INITIALS\x10\x03*E\n\x0e\x44\x65scriptorSize\x12\x0b\n\x07\x44S_AUTO\x10\x00\x12\x08\n\x04\x44S_1\x10\x01\x12\x08\n\x04\x44S_2\x10\x02\x12\x08\n\x04\x44S_4\x10\x04\x12\x08\n\x04\x44S_8\x10\x08:E\n\x0enanopb_fileopt\x12\x1c.google.protobuf.FileOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:G\n\rnanopb_msgopt\x12\x1f.google.protobuf.MessageOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:E\n\x0enanopb_enumopt\x12\x1c.google.protobuf.EnumOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:>\n\x06nanopb\x12\x1d.google.protobuf.FieldOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptionsB\x1a\n\x18\x66i.kapsi.koti.jpa.nanopb'
,
dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,])
@ -30,31 +30,38 @@ _FIELDTYPE = _descriptor.EnumDescriptor(
full_name='FieldType',
filename=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
values=[
_descriptor.EnumValueDescriptor(
name='FT_DEFAULT', index=0, number=0,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='FT_CALLBACK', index=1, number=1,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='FT_POINTER', index=2, number=4,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='FT_STATIC', index=3, number=2,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='FT_IGNORE', index=4, number=3,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='FT_INLINE', index=5, number=5,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
@ -69,27 +76,33 @@ _INTSIZE = _descriptor.EnumDescriptor(
full_name='IntSize',
filename=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
values=[
_descriptor.EnumValueDescriptor(
name='IS_DEFAULT', index=0, number=0,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='IS_8', index=1, number=8,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='IS_16', index=2, number=16,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='IS_32', index=3, number=32,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='IS_64', index=4, number=64,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
@ -104,23 +117,28 @@ _TYPENAMEMANGLING = _descriptor.EnumDescriptor(
full_name='TypenameMangling',
filename=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
values=[
_descriptor.EnumValueDescriptor(
name='M_NONE', index=0, number=0,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='M_STRIP_PACKAGE', index=1, number=1,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='M_FLATTEN', index=2, number=2,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='M_PACKAGE_INITIALS', index=3, number=3,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
@ -135,27 +153,33 @@ _DESCRIPTORSIZE = _descriptor.EnumDescriptor(
full_name='DescriptorSize',
filename=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
values=[
_descriptor.EnumValueDescriptor(
name='DS_AUTO', index=0, number=0,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='DS_1', index=1, number=1,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='DS_2', index=2, number=2,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='DS_4', index=3, number=4,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='DS_8', index=4, number=8,
serialized_options=None,
type=None),
type=None,
create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
@ -193,7 +217,7 @@ nanopb_fileopt = _descriptor.FieldDescriptor(
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=True, extension_scope=None,
serialized_options=None, file=DESCRIPTOR)
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
NANOPB_MSGOPT_FIELD_NUMBER = 1010
nanopb_msgopt = _descriptor.FieldDescriptor(
name='nanopb_msgopt', full_name='nanopb_msgopt', index=1,
@ -201,7 +225,7 @@ nanopb_msgopt = _descriptor.FieldDescriptor(
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=True, extension_scope=None,
serialized_options=None, file=DESCRIPTOR)
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
NANOPB_ENUMOPT_FIELD_NUMBER = 1010
nanopb_enumopt = _descriptor.FieldDescriptor(
name='nanopb_enumopt', full_name='nanopb_enumopt', index=2,
@ -209,7 +233,7 @@ nanopb_enumopt = _descriptor.FieldDescriptor(
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=True, extension_scope=None,
serialized_options=None, file=DESCRIPTOR)
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
NANOPB_FIELD_NUMBER = 1010
nanopb = _descriptor.FieldDescriptor(
name='nanopb', full_name='nanopb', index=3,
@ -217,7 +241,7 @@ nanopb = _descriptor.FieldDescriptor(
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=True, extension_scope=None,
serialized_options=None, file=DESCRIPTOR)
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
_NANOPBOPTIONS = _descriptor.Descriptor(
@ -226,6 +250,7 @@ _NANOPBOPTIONS = _descriptor.Descriptor(
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='max_size', full_name='NanoPBOptions.max_size', index=0,
@ -233,203 +258,203 @@ _NANOPBOPTIONS = _descriptor.Descriptor(
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='max_length', full_name='NanoPBOptions.max_length', index=1,
number=14, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='max_count', full_name='NanoPBOptions.max_count', index=2,
number=2, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='int_size', full_name='NanoPBOptions.int_size', index=3,
number=7, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='type', full_name='NanoPBOptions.type', index=4,
number=3, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='long_names', full_name='NanoPBOptions.long_names', index=5,
number=4, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=True,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='packed_struct', full_name='NanoPBOptions.packed_struct', index=6,
number=5, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='packed_enum', full_name='NanoPBOptions.packed_enum', index=7,
number=10, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='skip_message', full_name='NanoPBOptions.skip_message', index=8,
number=6, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='no_unions', full_name='NanoPBOptions.no_unions', index=9,
number=8, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='msgid', full_name='NanoPBOptions.msgid', index=10,
number=9, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='anonymous_oneof', full_name='NanoPBOptions.anonymous_oneof', index=11,
number=11, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='proto3', full_name='NanoPBOptions.proto3', index=12,
number=12, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='proto3_singular_msgs', full_name='NanoPBOptions.proto3_singular_msgs', index=13,
number=21, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='enum_to_string', full_name='NanoPBOptions.enum_to_string', index=14,
number=13, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='fixed_length', full_name='NanoPBOptions.fixed_length', index=15,
number=15, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='fixed_count', full_name='NanoPBOptions.fixed_count', index=16,
number=16, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='submsg_callback', full_name='NanoPBOptions.submsg_callback', index=17,
number=22, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='mangle_names', full_name='NanoPBOptions.mangle_names', index=18,
number=17, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='callback_datatype', full_name='NanoPBOptions.callback_datatype', index=19,
number=18, type=9, cpp_type=9, label=1,
has_default_value=True, default_value=_b("pb_callback_t").decode('utf-8'),
has_default_value=True, default_value=b"pb_callback_t".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='callback_function', full_name='NanoPBOptions.callback_function', index=20,
number=19, type=9, cpp_type=9, label=1,
has_default_value=True, default_value=_b("pb_default_field_callback").decode('utf-8'),
has_default_value=True, default_value=b"pb_default_field_callback".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='descriptorsize', full_name='NanoPBOptions.descriptorsize', index=21,
number=20, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='default_has', full_name='NanoPBOptions.default_has', index=22,
number=23, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='include', full_name='NanoPBOptions.include', index=23,
number=24, type=9, cpp_type=9, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='exclude', full_name='NanoPBOptions.exclude', index=24,
number=26, type=9, cpp_type=9, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='package', full_name='NanoPBOptions.package', index=25,
number=25, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='type_override', full_name='NanoPBOptions.type_override', index=26,
number=27, type=14, cpp_type=8, label=1,
has_default_value=False, default_value=1,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='sort_by_tag', full_name='NanoPBOptions.sort_by_tag', index=27,
number=28, type=8, cpp_type=7, label=1,
has_default_value=True, default_value=True,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='fallback_type', full_name='NanoPBOptions.fallback_type', index=28,
number=29, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=1,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
@ -463,11 +488,11 @@ DESCRIPTOR.extensions_by_name['nanopb_enumopt'] = nanopb_enumopt
DESCRIPTOR.extensions_by_name['nanopb'] = nanopb
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
NanoPBOptions = _reflection.GeneratedProtocolMessageType('NanoPBOptions', (_message.Message,), dict(
DESCRIPTOR = _NANOPBOPTIONS,
__module__ = 'nanopb_pb2'
NanoPBOptions = _reflection.GeneratedProtocolMessageType('NanoPBOptions', (_message.Message,), {
'DESCRIPTOR' : _NANOPBOPTIONS,
'__module__' : 'nanopb_pb2'
# @@protoc_insertion_point(class_scope:NanoPBOptions)
))
})
_sym_db.RegisterMessage(NanoPBOptions)
nanopb_fileopt.message_type = _NANOPBOPTIONS

View File

@ -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)