Demo improvements: LED hue config, press state reporting, custom display modes for timeline demo

This commit is contained in:
Scott Bezek 2023-07-10 00:38:28 -07:00
parent 1e2b1983c2
commit f63deabf10
18 changed files with 308 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,10 +14,10 @@ static SerialProtocolProtobuf* singleton_for_packet_serial = 0;
static const uint16_t MIN_STATE_INTERVAL_MILLIS = 5;
static const uint16_t PERIODIC_STATE_INTERVAL_MILLIS = 5000;
SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task) :
SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback) :
SerialProtocol(),
stream_(stream),
motor_task_(motor_task),
config_callback_(config_callback),
packet_serial_() {
packet_serial_.setStream(&stream);
@ -125,7 +125,7 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
switch (pb_rx_buffer_.which_payload) {
case PB_ToSmartknob_smartknob_config_tag: {
motor_task_.setConfig(pb_rx_buffer_.payload.smartknob_config);
config_callback_(pb_rx_buffer_.payload.smartknob_config);
break;
}
default: {

View File

@ -4,21 +4,22 @@
#include "../proto_gen/smartknob.pb.h"
#include "interface_callbacks.h"
#include "motor_task.h"
#include "serial_protocol.h"
#include "uart_stream.h"
class SerialProtocolProtobuf : public SerialProtocol {
public:
SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task);
~SerialProtocolProtobuf(){}
SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback);
~SerialProtocolProtobuf(){};
void log(const char* msg) override;
void loop() override;
void handleState(const PB_SmartKnobState& state) override;
private:
Stream& stream_;
MotorTask& motor_task_;
ConfigCallback config_callback_;
PB_FromSmartKnob pb_tx_buffer_;
PB_ToSmartknob pb_rx_buffer_;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ _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\"\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\x0cRequestState\"v\n\x17PersistentConfiguration\x12\x0f\n\x07version\x18\x01 \x01(\r\x12#\n\x05motor\x18\x02 \x01(\x0b\x32\x14.PB.MotorCalibration\x12%\n\x06strain\x18\x03 \x01(\x0b\x32\x15.PB.StrainCalibration\"p\n\x10MotorCalibration\x12\x12\n\ncalibrated\x18\x01 \x01(\x08\x12\x1e\n\x16zero_electrical_offset\x18\x02 \x01(\x02\x12\x14\n\x0c\x64irection_cw\x18\x03 \x01(\x08\x12\x12\n\npole_pairs\x18\x04 \x01(\r\"<\n\x11StrainCalibration\x12\x12\n\nidle_value\x18\x01 \x01(\x05\x12\x13\n\x0bpress_delta\x18\x02 \x01(\x05\x62\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\"\x86\x01\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\x12\x1a\n\x0bpress_nonce\x18\x04 \x01(\rB\x05\x92?\x02\x38\x08\"\xe1\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\x12\x16\n\x07led_hue\x18\r \x01(\x05\x42\x05\x92?\x02\x38\x10\"\x0e\n\x0cRequestState\"v\n\x17PersistentConfiguration\x12\x0f\n\x07version\x18\x01 \x01(\r\x12#\n\x05motor\x18\x02 \x01(\x0b\x32\x14.PB.MotorCalibration\x12%\n\x06strain\x18\x03 \x01(\x0b\x32\x15.PB.StrainCalibration\"p\n\x10MotorCalibration\x12\x12\n\ncalibrated\x18\x01 \x01(\x08\x12\x1e\n\x16zero_electrical_offset\x18\x02 \x01(\x02\x12\x14\n\x0c\x64irection_cw\x18\x03 \x01(\x08\x12\x12\n\npole_pairs\x18\x04 \x01(\r\"<\n\x11StrainCalibration\x12\x12\n\nidle_value\x18\x01 \x01(\x05\x12\x13\n\x0bpress_delta\x18\x02 \x01(\x05\x62\x06proto3')
@ -108,12 +108,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_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'
_SMARTKNOBSTATE.fields_by_name['press_nonce']._options = None
_SMARTKNOBSTATE.fields_by_name['press_nonce']._serialized_options = b'\222?\0028\010'
_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'
_SMARTKNOBCONFIG.fields_by_name['led_hue']._options = None
_SMARTKNOBCONFIG.fields_by_name['led_hue']._serialized_options = b'\222?\0028\020'
_FROMSMARTKNOB._serialized_start=38
_FROMSMARTKNOB._serialized_end=192
_TOSMARTKNOB._serialized_start=195
@ -122,16 +126,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_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
_PERSISTENTCONFIGURATION._serialized_start=867
_PERSISTENTCONFIGURATION._serialized_end=985
_MOTORCALIBRATION._serialized_start=987
_MOTORCALIBRATION._serialized_end=1099
_STRAINCALIBRATION._serialized_start=1101
_STRAINCALIBRATION._serialized_end=1161
_SMARTKNOBSTATE._serialized_start=412
_SMARTKNOBSTATE._serialized_end=546
_SMARTKNOBCONFIG._serialized_start=549
_SMARTKNOBCONFIG._serialized_end=902
_REQUESTSTATE._serialized_start=904
_REQUESTSTATE._serialized_end=918
_PERSISTENTCONFIGURATION._serialized_start=920
_PERSISTENTCONFIGURATION._serialized_end=1038
_MOTORCALIBRATION._serialized_start=1040
_MOTORCALIBRATION._serialized_end=1152
_STRAINCALIBRATION._serialized_start=1154
_STRAINCALIBRATION._serialized_end=1214
# @@protoc_insertion_point(module_scope)

View File

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