mirror of
https://github.com/scottbez1/smartknob.git
synced 2025-09-26 23:09:27 +08:00
Protobuf serial protocol (#101)
- Firmware - Refactor all code to use a log interface rather than `Serial` directly - Moved platformio.ini to root, so you can load the entire repo in VS Code and still use platformio - Reduced graphic buffer bit depth to 8 bits (short on RAM :( ) - Implemented threadsafe log interface in interface_task (using a queue for posting log message) - Created serial protocol interface and 2 implementations - plaintext (default) and protobuf (selected by sending a NULL byte) - Protobuf protocol is roughly the same architecture as Splitflap's: - PacketSerial (cobs) framing with NULL delimiters - CRC32 checksums for packets - nanopb generated code for encoding/decoding (generated firmware code is checked in, since it should change less frequently and this reduces burden to build the project from scratch) - Software - Typescript example host-side code: - smartknobjs-proto - Autogenerated types/encoding/decoding protobuf code (using protobufjs) - smartknobjs - Helper library for interfacing the with smartknob via serial/protobuf. Implements basic outgoing queue (with retries and ACK checking) and message callback for responding to messages from the SmartKnob - example - Basic demo CLI app that uses smartknobjs to connect to the smartknob, send a haptic config, and print state changes and log messages to the console
This commit is contained in:
parent
aec9799e10
commit
ae28d523e0
2
.github/workflows/pio.yml
vendored
2
.github/workflows/pio.yml
vendored
@ -39,5 +39,5 @@ jobs:
|
||||
# Run regardless of other build step failures, as long as setup steps completed
|
||||
if: always() && steps.pio_install.outcome == 'success'
|
||||
run: |
|
||||
pio run -d ./firmware \
|
||||
pio run \
|
||||
-e view \
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "thirdparty/nanopb"]
|
||||
path = thirdparty/nanopb
|
||||
url = git@github.com:nanopb/nanopb.git
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@ -2,6 +2,11 @@
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"platformio.platformio-ide",
|
||||
"rvest.vs-code-prettier-eslint"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
|
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"eslint.workingDirectories": [
|
||||
"./software/js/packages/example",
|
||||
"./software/js/packages/smartknobjs",
|
||||
"./software/js"
|
||||
],
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
}
|
5
firmware/.vscode/settings.json
vendored
Normal file
5
firmware/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"functional": "cpp"
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@
|
||||
|
||||
static const uint8_t LEDC_CHANNEL_LCD_BACKLIGHT = 0;
|
||||
|
||||
DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 4048, 1, task_core} {
|
||||
knob_state_queue_ = xQueueCreate(1, sizeof(KnobState));
|
||||
DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 2048, 1, task_core} {
|
||||
knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState));
|
||||
assert(knob_state_queue_ != NULL);
|
||||
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
@ -19,63 +19,6 @@ DisplayTask::~DisplayTask() {
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
static void HSV_to_RGB(float h, float s, float v, uint8_t *r, uint8_t *g, uint8_t *b)
|
||||
{
|
||||
int i;
|
||||
float f,p,q,t;
|
||||
|
||||
h = fmax(0.0, fmin(360.0, h));
|
||||
s = fmax(0.0, fmin(100.0, s));
|
||||
v = fmax(0.0, fmin(100.0, v));
|
||||
|
||||
s /= 100;
|
||||
v /= 100;
|
||||
|
||||
if(s == 0) {
|
||||
// Achromatic (grey)
|
||||
*r = *g = *b = round(v*255);
|
||||
return;
|
||||
}
|
||||
|
||||
h /= 60; // sector 0 to 5
|
||||
i = floor(h);
|
||||
f = h - i; // factorial part of h
|
||||
p = v * (1 - s);
|
||||
q = v * (1 - s * f);
|
||||
t = v * (1 - s * (1 - f));
|
||||
switch(i) {
|
||||
case 0:
|
||||
*r = round(255*v);
|
||||
*g = round(255*t);
|
||||
*b = round(255*p);
|
||||
break;
|
||||
case 1:
|
||||
*r = round(255*q);
|
||||
*g = round(255*v);
|
||||
*b = round(255*p);
|
||||
break;
|
||||
case 2:
|
||||
*r = round(255*p);
|
||||
*g = round(255*v);
|
||||
*b = round(255*t);
|
||||
break;
|
||||
case 3:
|
||||
*r = round(255*p);
|
||||
*g = round(255*q);
|
||||
*b = round(255*v);
|
||||
break;
|
||||
case 4:
|
||||
*r = round(255*t);
|
||||
*g = round(255*p);
|
||||
*b = round(255*v);
|
||||
break;
|
||||
default: // case 5:
|
||||
*r = round(255*v);
|
||||
*g = round(255*p);
|
||||
*b = round(255*q);
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayTask::run() {
|
||||
tft_.begin();
|
||||
tft_.invertDisplay(1);
|
||||
@ -86,28 +29,23 @@ void DisplayTask::run() {
|
||||
ledcAttachPin(PIN_LCD_BACKLIGHT, LEDC_CHANNEL_LCD_BACKLIGHT);
|
||||
ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, UINT16_MAX);
|
||||
|
||||
spr_.setColorDepth(16);
|
||||
spr_.setColorDepth(8);
|
||||
|
||||
if (spr_.createSprite(TFT_WIDTH, TFT_HEIGHT) == nullptr) {
|
||||
Serial.println("ERROR: sprite allocation failed!");
|
||||
log("ERROR: sprite allocation failed!");
|
||||
tft_.fillScreen(TFT_RED);
|
||||
} else {
|
||||
Serial.println("Sprite created!");
|
||||
log("Sprite created!");
|
||||
tft_.fillScreen(TFT_PURPLE);
|
||||
}
|
||||
spr_.setTextColor(0xFFFF, TFT_BLACK);
|
||||
|
||||
KnobState state;
|
||||
PB_SmartKnobState state;
|
||||
|
||||
const int RADIUS = TFT_WIDTH / 2;
|
||||
const uint16_t FILL_COLOR = spr_.color565(90, 18, 151);
|
||||
const uint16_t DOT_COLOR = spr_.color565(80, 100, 200);
|
||||
|
||||
int32_t pointer_center_x = TFT_WIDTH / 2;
|
||||
int32_t pointer_center_y = TFT_HEIGHT / 2;
|
||||
int32_t pointer_length_short = 10;
|
||||
int32_t pointer_length_long = TFT_WIDTH / 2 - 5;
|
||||
|
||||
spr_.setTextDatum(CC_DATUM);
|
||||
spr_.setTextColor(TFT_WHITE);
|
||||
while(1) {
|
||||
@ -125,15 +63,15 @@ void DisplayTask::run() {
|
||||
spr_.drawString(String() + state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2 - VALUE_OFFSET, 1);
|
||||
spr_.setFreeFont(&DESCRIPTION_FONT);
|
||||
int32_t line_y = TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET;
|
||||
char* start = state.config.descriptor;
|
||||
char* end = start + strlen(state.config.descriptor);
|
||||
char* start = state.config.text;
|
||||
char* end = start + strlen(state.config.text);
|
||||
while (start < end) {
|
||||
char* newline = strchr(start, '\n');
|
||||
if (newline == nullptr) {
|
||||
newline = end;
|
||||
}
|
||||
|
||||
char buf[sizeof(state.config.descriptor)] = {};
|
||||
char buf[sizeof(state.config.text)] = {};
|
||||
strncat(buf, start, min(sizeof(buf) - 1, (size_t)(newline - start)));
|
||||
spr_.drawString(String(buf), TFT_WIDTH / 2, line_y, 1);
|
||||
start = newline + 1;
|
||||
@ -202,4 +140,14 @@ void DisplayTask::setBrightness(uint16_t brightness) {
|
||||
brightness_ = brightness;
|
||||
}
|
||||
|
||||
void DisplayTask::setLogger(Logger* logger) {
|
||||
logger_ = logger;
|
||||
}
|
||||
|
||||
void DisplayTask::log(const char* msg) {
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -5,7 +5,8 @@
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#include "knob_data.h"
|
||||
#include "logger.h"
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
#include "task.h"
|
||||
|
||||
class DisplayTask : public Task<DisplayTask> {
|
||||
@ -18,6 +19,7 @@ class DisplayTask : public Task<DisplayTask> {
|
||||
QueueHandle_t getKnobStateQueue();
|
||||
|
||||
void setBrightness(uint16_t brightness);
|
||||
void setLogger(Logger* logger);
|
||||
|
||||
protected:
|
||||
void run();
|
||||
@ -30,11 +32,11 @@ class DisplayTask : public Task<DisplayTask> {
|
||||
|
||||
QueueHandle_t knob_state_queue_;
|
||||
|
||||
KnobState state_;
|
||||
|
||||
PB_SmartKnobState state_;
|
||||
SemaphoreHandle_t mutex_;
|
||||
|
||||
uint16_t brightness_;
|
||||
Logger* logger_;
|
||||
void log(const char* msg);
|
||||
};
|
||||
|
||||
#else
|
||||
|
@ -1,5 +1,3 @@
|
||||
#include <AceButton.h>
|
||||
|
||||
#if SK_LEDS
|
||||
#include <FastLED.h>
|
||||
#endif
|
||||
@ -15,8 +13,6 @@
|
||||
#include "interface_task.h"
|
||||
#include "util.h"
|
||||
|
||||
using namespace ace_button;
|
||||
|
||||
#define COUNT_OF(A) (sizeof(A) / sizeof(A[0]))
|
||||
|
||||
#if SK_LEDS
|
||||
@ -31,14 +27,14 @@ HX711 scale;
|
||||
Adafruit_VEML7700 veml = Adafruit_VEML7700();
|
||||
#endif
|
||||
|
||||
static KnobConfig configs[] = {
|
||||
static PB_SmartKnobConfig configs[] = {
|
||||
// int32_t num_positions;
|
||||
// int32_t position;
|
||||
// float position_width_radians;
|
||||
// float detent_strength_unit;
|
||||
// float endstop_strength_unit;
|
||||
// float snap_point;
|
||||
// char descriptor[50];
|
||||
// char text[51];
|
||||
|
||||
{
|
||||
0,
|
||||
@ -123,32 +119,26 @@ static KnobConfig configs[] = {
|
||||
},
|
||||
};
|
||||
|
||||
InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task) : Task("Interface", 4048, 1, task_core), motor_task_(motor_task), display_task_(display_task) {
|
||||
InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task) :
|
||||
Task("Interface", 3000, 1, task_core),
|
||||
stream_(),
|
||||
motor_task_(motor_task),
|
||||
display_task_(display_task),
|
||||
plaintext_protocol_(stream_, motor_task_),
|
||||
proto_protocol_(stream_, motor_task_) {
|
||||
#if SK_DISPLAY
|
||||
assert(display_task != nullptr);
|
||||
#endif
|
||||
|
||||
log_queue_ = xQueueCreate(10, sizeof(std::string *));
|
||||
assert(log_queue_ != NULL);
|
||||
|
||||
knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState));
|
||||
assert(knob_state_queue_ != NULL);
|
||||
}
|
||||
|
||||
InterfaceTask::~InterfaceTask() {}
|
||||
|
||||
void InterfaceTask::run() {
|
||||
#if PIN_BUTTON_NEXT >= 34
|
||||
pinMode(PIN_BUTTON_NEXT, INPUT);
|
||||
#else
|
||||
pinMode(PIN_BUTTON_NEXT, INPUT_PULLUP);
|
||||
#endif
|
||||
AceButton button_next((uint8_t) PIN_BUTTON_NEXT);
|
||||
button_next.getButtonConfig()->setIEventHandler(this);
|
||||
|
||||
#if PIN_BUTTON_PREV > -1
|
||||
#if PIN_BUTTON_PREV >= 34
|
||||
pinMode(PIN_BUTTON_PREV, INPUT);
|
||||
#else
|
||||
pinMode(PIN_BUTTON_PREV, INPUT_PULLUP);
|
||||
#endif
|
||||
AceButton button_prev((uint8_t) PIN_BUTTON_PREV);
|
||||
button_prev.getButtonConfig()->setIEventHandler(this);
|
||||
#endif
|
||||
stream_.begin();
|
||||
|
||||
#if SK_LEDS
|
||||
FastLED.addLeds<SK6812, PIN_LED_DATA, GRB>(leds, NUM_LEDS);
|
||||
@ -167,121 +157,64 @@ void InterfaceTask::run() {
|
||||
veml.setGain(VEML7700_GAIN_2);
|
||||
veml.setIntegrationTime(VEML7700_IT_400MS);
|
||||
} else {
|
||||
Serial.println("ALS sensor not found!");
|
||||
log("ALS sensor not found!");
|
||||
}
|
||||
#endif
|
||||
|
||||
motor_task_.setConfig(configs[0]);
|
||||
motor_task_.addListener(knob_state_queue_);
|
||||
|
||||
// How far button is pressed, in range [0, 1]
|
||||
float press_value_unit = 0;
|
||||
|
||||
// Start in legacy protocol mode
|
||||
plaintext_protocol_.init([this] () {
|
||||
changeConfig(true);
|
||||
});
|
||||
SerialProtocol* current_protocol = &plaintext_protocol_;
|
||||
|
||||
ProtocolChangeCallback protocol_change_callback = [this, ¤t_protocol] (uint8_t protocol) {
|
||||
switch (protocol) {
|
||||
case SERIAL_PROTOCOL_LEGACY:
|
||||
current_protocol = &plaintext_protocol_;
|
||||
break;
|
||||
case SERIAL_PROTOCOL_PROTO:
|
||||
current_protocol = &proto_protocol_;
|
||||
break;
|
||||
default:
|
||||
log("Unknown protocol requested");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
plaintext_protocol_.setProtocolChangeCallback(protocol_change_callback);
|
||||
proto_protocol_.setProtocolChangeCallback(protocol_change_callback);
|
||||
|
||||
// Interface loop:
|
||||
while (1) {
|
||||
button_next.check();
|
||||
#if PIN_BUTTON_PREV > -1
|
||||
button_prev.check();
|
||||
#endif
|
||||
if (Serial.available()) {
|
||||
int v = Serial.read();
|
||||
if (v == ' ') {
|
||||
changeConfig(true);
|
||||
}
|
||||
PB_SmartKnobState state;
|
||||
if (xQueueReceive(knob_state_queue_, &state, 0) == pdTRUE) {
|
||||
current_protocol->handleState(state);
|
||||
}
|
||||
|
||||
#if SK_ALS
|
||||
const float LUX_ALPHA = 0.005;
|
||||
static float lux_avg;
|
||||
float lux = veml.readLux();
|
||||
lux_avg = lux * LUX_ALPHA + lux_avg * (1 - LUX_ALPHA);
|
||||
static uint32_t last_als;
|
||||
if (millis() - last_als > 1000) {
|
||||
Serial.print("millilux: "); Serial.println(lux*1000);
|
||||
last_als = millis();
|
||||
}
|
||||
#endif
|
||||
current_protocol->loop();
|
||||
|
||||
#if SK_STRAIN
|
||||
// TODO: calibrate and track (long term moving average) zero point (lower); allow calibration of set point offset
|
||||
const int32_t lower = 950000;
|
||||
const int32_t upper = 1800000;
|
||||
if (scale.wait_ready_timeout(100)) {
|
||||
int32_t reading = scale.read();
|
||||
std::string* log_string;
|
||||
while (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) {
|
||||
current_protocol->log(log_string->c_str());
|
||||
delete log_string;
|
||||
}
|
||||
|
||||
// Ignore readings that are way out of expected bounds
|
||||
if (reading >= lower - (upper - lower) && reading < upper + (upper - lower)*2) {
|
||||
static uint32_t last_reading_display;
|
||||
if (millis() - last_reading_display > 1000) {
|
||||
Serial.print("HX711 reading: ");
|
||||
Serial.println(reading);
|
||||
last_reading_display = millis();
|
||||
}
|
||||
long value = CLAMP(reading, lower, upper);
|
||||
press_value_unit = 1. * (value - lower) / (upper - lower);
|
||||
updateHardware();
|
||||
|
||||
static bool pressed;
|
||||
if (!pressed && press_value_unit > 0.75) {
|
||||
motor_task_.playHaptic(true);
|
||||
pressed = true;
|
||||
changeConfig(true);
|
||||
} else if (pressed && press_value_unit < 0.25) {
|
||||
motor_task_.playHaptic(false);
|
||||
pressed = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.println("HX711 not found.");
|
||||
|
||||
#if SK_LEDS
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
leds[i] = CRGB::Red;
|
||||
}
|
||||
FastLED.show();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t brightness = UINT16_MAX;
|
||||
// TODO: brightness scale factor should be configurable (depends on reflectivity of surface)
|
||||
#if SK_ALS
|
||||
brightness = (uint16_t)CLAMP(lux_avg * 13000, (float)1280, (float)UINT16_MAX);
|
||||
#endif
|
||||
|
||||
#if SK_DISPLAY
|
||||
display_task_->setBrightness(brightness); // TODO: apply gamma correction
|
||||
#endif
|
||||
|
||||
#if SK_LEDS
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
leds[i].setHSV(200 * press_value_unit, 255, brightness >> 8);
|
||||
|
||||
// Gamma adjustment
|
||||
leds[i].r = dim8_video(leds[i].r);
|
||||
leds[i].g = dim8_video(leds[i].g);
|
||||
leds[i].b = dim8_video(leds[i].b);
|
||||
}
|
||||
FastLED.show();
|
||||
#endif
|
||||
|
||||
delay(10);
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void InterfaceTask::handleEvent(AceButton* button, uint8_t event_type, uint8_t button_state) {
|
||||
switch (event_type) {
|
||||
case AceButton::kEventPressed:
|
||||
if (button->getPin() == PIN_BUTTON_NEXT) {
|
||||
changeConfig(true);
|
||||
}
|
||||
#if PIN_BUTTON_PREV > -1
|
||||
if (button->getPin() == PIN_BUTTON_PREV) {
|
||||
changeConfig(false);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case AceButton::kEventReleased:
|
||||
break;
|
||||
}
|
||||
void InterfaceTask::log(const char* msg) {
|
||||
// Allocate a string for the duration it's in the queue; it is free'd by the queue consumer
|
||||
std::string* msg_str = new std::string(msg);
|
||||
|
||||
// Put string in queue (or drop if full to avoid blocking)
|
||||
xQueueSendToBack(log_queue_, &msg_str, 0);
|
||||
}
|
||||
|
||||
void InterfaceTask::changeConfig(bool next) {
|
||||
@ -295,9 +228,89 @@ void InterfaceTask::changeConfig(bool next) {
|
||||
}
|
||||
}
|
||||
|
||||
Serial.print("Changing config to ");
|
||||
Serial.print(current_config_);
|
||||
Serial.print(" -- ");
|
||||
Serial.println(configs[current_config_].descriptor);
|
||||
char buf_[256];
|
||||
snprintf(buf_, sizeof(buf_), "Changing config to %d -- %s", current_config_, configs[current_config_].text);
|
||||
log(buf_);
|
||||
motor_task_.setConfig(configs[current_config_]);
|
||||
}
|
||||
|
||||
void InterfaceTask::updateHardware() {
|
||||
// How far button is pressed, in range [0, 1]
|
||||
float press_value_unit = 0;
|
||||
|
||||
#if SK_ALS
|
||||
const float LUX_ALPHA = 0.005;
|
||||
static float lux_avg;
|
||||
float lux = veml.readLux();
|
||||
lux_avg = lux * LUX_ALPHA + lux_avg * (1 - LUX_ALPHA);
|
||||
static uint32_t last_als;
|
||||
if (millis() - last_als > 1000) {
|
||||
snprintf(buf_, sizeof(buf_), "millilux: %.2f", lux*1000);
|
||||
log(buf_);
|
||||
last_als = millis();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SK_STRAIN
|
||||
if (scale.wait_ready_timeout(100)) {
|
||||
int32_t reading = scale.read();
|
||||
|
||||
static uint32_t last_reading_display;
|
||||
if (millis() - last_reading_display > 1000) {
|
||||
snprintf(buf_, sizeof(buf_), "HX711 reading: %d", reading);
|
||||
log(buf_);
|
||||
last_reading_display = millis();
|
||||
}
|
||||
|
||||
// TODO: calibrate and track (long term moving average) zero point (lower); allow calibration of set point offset
|
||||
const int32_t lower = 950000;
|
||||
const int32_t upper = 1800000;
|
||||
// Ignore readings that are way out of expected bounds
|
||||
if (reading >= lower - (upper - lower) && reading < upper + (upper - lower)*2) {
|
||||
long value = CLAMP(reading, lower, upper);
|
||||
press_value_unit = 1. * (value - lower) / (upper - lower);
|
||||
|
||||
static bool pressed;
|
||||
if (!pressed && press_value_unit > 0.75) {
|
||||
motor_task_.playHaptic(true);
|
||||
pressed = true;
|
||||
changeConfig(true);
|
||||
} else if (pressed && press_value_unit < 0.25) {
|
||||
motor_task_.playHaptic(false);
|
||||
pressed = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log("HX711 not found.");
|
||||
|
||||
#if SK_LEDS
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
leds[i] = CRGB::Red;
|
||||
}
|
||||
FastLED.show();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t brightness = UINT16_MAX;
|
||||
// TODO: brightness scale factor should be configurable (depends on reflectivity of surface)
|
||||
#if SK_ALS
|
||||
brightness = (uint16_t)CLAMP(lux_avg * 13000, (float)1280, (float)UINT16_MAX);
|
||||
#endif
|
||||
|
||||
#if SK_DISPLAY
|
||||
display_task_->setBrightness(brightness); // TODO: apply gamma correction
|
||||
#endif
|
||||
|
||||
#if SK_LEDS
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
leds[i].setHSV(200 * press_value_unit, 255, brightness >> 8);
|
||||
|
||||
// Gamma adjustment
|
||||
leds[i].r = dim8_video(leds[i].r);
|
||||
leds[i].g = dim8_video(leds[i].g);
|
||||
leds[i].b = dim8_video(leds[i].b);
|
||||
}
|
||||
FastLED.show();
|
||||
#endif
|
||||
}
|
||||
|
@ -4,26 +4,38 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "display_task.h"
|
||||
#include "logger.h"
|
||||
#include "motor_task.h"
|
||||
#include "serial/serial_protocol_plaintext.h"
|
||||
#include "serial/serial_protocol_protobuf.h"
|
||||
#include "serial/uart_stream.h"
|
||||
#include "task.h"
|
||||
|
||||
class InterfaceTask : public Task<InterfaceTask>, public ace_button::IEventHandler {
|
||||
class InterfaceTask : public Task<InterfaceTask>, public Logger {
|
||||
friend class Task<InterfaceTask>; // Allow base Task to invoke protected run()
|
||||
|
||||
public:
|
||||
InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task);
|
||||
~InterfaceTask();
|
||||
virtual ~InterfaceTask() {};
|
||||
|
||||
void handleEvent(ace_button::AceButton* button, uint8_t event_type, uint8_t button_state) override;
|
||||
void log(const char* msg) override;
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
UartStream stream_;
|
||||
MotorTask& motor_task_;
|
||||
DisplayTask* display_task_;
|
||||
char buf_[64];
|
||||
|
||||
int current_config_ = 0;
|
||||
|
||||
QueueHandle_t log_queue_;
|
||||
QueueHandle_t knob_state_queue_;
|
||||
SerialProtocolPlaintext plaintext_protocol_;
|
||||
SerialProtocolProtobuf proto_protocol_;
|
||||
|
||||
void changeConfig(bool next);
|
||||
void updateHardware();
|
||||
};
|
||||
|
@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
struct KnobConfig {
|
||||
int32_t num_positions;
|
||||
int32_t position;
|
||||
float position_width_radians;
|
||||
float detent_strength_unit;
|
||||
float endstop_strength_unit;
|
||||
float snap_point;
|
||||
char descriptor[50];
|
||||
};
|
||||
|
||||
struct KnobState {
|
||||
int32_t current_position;
|
||||
float sub_position_unit;
|
||||
KnobConfig config;
|
||||
};
|
@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
Logger() {};
|
||||
virtual ~Logger() {};
|
||||
|
||||
virtual void log(const char* msg) = 0;
|
||||
};
|
@ -1,66 +1,54 @@
|
||||
#include <Arduino.h>
|
||||
#include <SimpleFOC.h>
|
||||
|
||||
#include "display_task.h"
|
||||
#include "interface_task.h"
|
||||
#include "motor_task.h"
|
||||
|
||||
#if SK_DISPLAY
|
||||
static DisplayTask display_task = DisplayTask(0);
|
||||
static DisplayTask display_task(0);
|
||||
static DisplayTask* display_task_p = &display_task;
|
||||
#else
|
||||
static DisplayTask* display_task_p = nullptr;
|
||||
#endif
|
||||
static MotorTask motor_task = MotorTask(1);
|
||||
static MotorTask motor_task(1);
|
||||
|
||||
|
||||
InterfaceTask interface_task = InterfaceTask(0, motor_task, display_task_p);
|
||||
|
||||
static QueueHandle_t knob_state_debug_queue;
|
||||
InterfaceTask interface_task(0, motor_task, display_task_p);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
motor_task.begin();
|
||||
interface_task.begin();
|
||||
|
||||
#if SK_DISPLAY
|
||||
display_task.setLogger(&interface_task);
|
||||
display_task.begin();
|
||||
|
||||
// Connect display to motor_task's knob state feed
|
||||
motor_task.addListener(display_task.getKnobStateQueue());
|
||||
#endif
|
||||
|
||||
// Create a queue and register it with motor_task to print knob state to serial (see loop() below)
|
||||
knob_state_debug_queue = xQueueCreate(1, sizeof(KnobState));
|
||||
assert(knob_state_debug_queue != NULL);
|
||||
|
||||
motor_task.addListener(knob_state_debug_queue);
|
||||
motor_task.setLogger(&interface_task);
|
||||
motor_task.begin();
|
||||
interface_task.begin();
|
||||
|
||||
// Free up the Arduino loop task
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
|
||||
static KnobState state = {};
|
||||
uint32_t last_debug;
|
||||
|
||||
void loop() {
|
||||
// Print any new state, at most 5 times per second
|
||||
if (millis() - last_debug > 200 && xQueueReceive(knob_state_debug_queue, &state, portMAX_DELAY) == pdTRUE) {
|
||||
Serial.println(state.current_position);
|
||||
last_debug = millis();
|
||||
}
|
||||
|
||||
static uint32_t last_stack_debug;
|
||||
if (millis() - last_stack_debug > 1000) {
|
||||
Serial.println("Stack high water:");
|
||||
Serial.printf("main: %d\n", uxTaskGetStackHighWaterMark(NULL));
|
||||
#if SK_DISPLAY
|
||||
Serial.printf("display: %d\n", uxTaskGetStackHighWaterMark(display_task.getHandle()));
|
||||
#endif
|
||||
Serial.printf("motor: %d\n", uxTaskGetStackHighWaterMark(motor_task.getHandle()));
|
||||
Serial.printf("interface: %d\n", uxTaskGetStackHighWaterMark(interface_task.getHandle()));
|
||||
last_stack_debug = millis();
|
||||
}
|
||||
// char buf[50];
|
||||
// static uint32_t last_stack_debug;
|
||||
// if (millis() - last_stack_debug > 1000) {
|
||||
// interface_task.log("Stack high water:");
|
||||
// snprintf(buf, sizeof(buf), " main: %d", uxTaskGetStackHighWaterMark(NULL));
|
||||
// interface_task.log(buf);
|
||||
// #if SK_DISPLAY
|
||||
// snprintf(buf, sizeof(buf), " display: %d", uxTaskGetStackHighWaterMark(display_task.getHandle()));
|
||||
// interface_task.log(buf);
|
||||
// #endif
|
||||
// snprintf(buf, sizeof(buf), " motor: %d", uxTaskGetStackHighWaterMark(motor_task.getHandle()));
|
||||
// interface_task.log(buf);
|
||||
// snprintf(buf, sizeof(buf), " interface: %d", uxTaskGetStackHighWaterMark(interface_task.getHandle()));
|
||||
// interface_task.log(buf);
|
||||
// snprintf(buf, sizeof(buf), "Heap -- free: %d, largest: %d", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
|
||||
// interface_task.log(buf);
|
||||
// last_stack_debug = millis();
|
||||
// }
|
||||
}
|
@ -29,7 +29,7 @@ static const float IDLE_CORRECTION_MAX_ANGLE_RAD = 5 * PI / 180;
|
||||
static const float IDLE_CORRECTION_RATE_ALPHA = 0.0005;
|
||||
|
||||
|
||||
MotorTask::MotorTask(const uint8_t task_core) : Task("Motor", 2048, 1, task_core) {
|
||||
MotorTask::MotorTask(const uint8_t task_core) : Task("Motor", 2500, 1, task_core) {
|
||||
queue_ = xQueueCreate(5, sizeof(Command));
|
||||
assert(queue_ != NULL);
|
||||
}
|
||||
@ -43,16 +43,13 @@ MotorTask::~MotorTask() {}
|
||||
MT6701Sensor encoder = MT6701Sensor();
|
||||
#endif
|
||||
|
||||
Commander command = Commander(Serial);
|
||||
|
||||
|
||||
void MotorTask::run() {
|
||||
|
||||
driver.voltage_power_supply = 5;
|
||||
driver.init();
|
||||
|
||||
#if SENSOR_TLV
|
||||
encoder.init(Wire, false);
|
||||
encoder.init(&Wire, false);
|
||||
#endif
|
||||
|
||||
#if SENSOR_MT6701
|
||||
@ -82,29 +79,12 @@ void MotorTask::run() {
|
||||
motor.pole_pairs = MOTOR_POLE_PAIRS;
|
||||
motor.initFOC(ZERO_ELECTRICAL_OFFSET, FOC_DIRECTION);
|
||||
|
||||
bool calibrate = false;
|
||||
|
||||
Serial.println("Press Y to run calibration");
|
||||
uint32_t t = millis();
|
||||
while (millis() - t < 3000) {
|
||||
if (Serial.read() == 'Y') {
|
||||
calibrate = true;
|
||||
break;
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
if (calibrate) {
|
||||
this->calibrate();
|
||||
}
|
||||
|
||||
Serial.println(motor.zero_electric_angle);
|
||||
|
||||
motor.monitor_downsample = 0; // disable monitor at first - optional
|
||||
|
||||
// disableCore0WDT();
|
||||
|
||||
float current_detent_center = motor.shaft_angle;
|
||||
KnobConfig config = {
|
||||
PB_SmartKnobConfig config = {
|
||||
.num_positions = 2,
|
||||
.position = 0,
|
||||
.position_width_radians = 60 * _PI / 180,
|
||||
@ -115,6 +95,8 @@ void MotorTask::run() {
|
||||
uint32_t last_idle_start = 0;
|
||||
uint32_t last_publish = 0;
|
||||
|
||||
PB_SmartKnobConfig latest_config = config;
|
||||
|
||||
while (1) {
|
||||
motor.loopFOC();
|
||||
|
||||
@ -122,10 +104,14 @@ void MotorTask::run() {
|
||||
Command command;
|
||||
if (xQueueReceive(queue_, &command, 0) == pdTRUE) {
|
||||
switch (command.command_type) {
|
||||
case CommandType::CALIBRATE:
|
||||
calibrate();
|
||||
break;
|
||||
case CommandType::CONFIG: {
|
||||
// Change haptic input mode
|
||||
config = command.data.config;
|
||||
Serial.println("Got new config");
|
||||
latest_config = config;
|
||||
log("Got new config");
|
||||
current_detent_center = motor.shaft_angle;
|
||||
#if SK_INVERT_ROTATION
|
||||
current_detent_center = -motor.shaft_angle;
|
||||
@ -222,10 +208,11 @@ void MotorTask::run() {
|
||||
}
|
||||
|
||||
// Publish current status to other registered tasks periodically
|
||||
if (millis() - last_publish > 10) {
|
||||
if (millis() - last_publish > 5) {
|
||||
publish({
|
||||
.current_position = config.position,
|
||||
.sub_position_unit = -angle_to_detent_center / config.position_width_radians,
|
||||
.has_config = true,
|
||||
.config = config,
|
||||
});
|
||||
last_publish = millis();
|
||||
@ -237,7 +224,7 @@ void MotorTask::run() {
|
||||
}
|
||||
}
|
||||
|
||||
void MotorTask::setConfig(const KnobConfig& config) {
|
||||
void MotorTask::setConfig(const PB_SmartKnobConfig& config) {
|
||||
Command command = {
|
||||
.command_type = CommandType::CONFIG,
|
||||
.data = {
|
||||
@ -260,12 +247,22 @@ void MotorTask::playHaptic(bool press) {
|
||||
xQueueSend(queue_, &command, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void MotorTask::runCalibration() {
|
||||
Command command = {
|
||||
.command_type = CommandType::CALIBRATE,
|
||||
.data = {
|
||||
.unused = 0,
|
||||
}
|
||||
};
|
||||
xQueueSend(queue_, &command, portMAX_DELAY);
|
||||
}
|
||||
|
||||
|
||||
void MotorTask::addListener(QueueHandle_t queue) {
|
||||
listeners_.push_back(queue);
|
||||
}
|
||||
|
||||
void MotorTask::publish(const KnobState& state) {
|
||||
void MotorTask::publish(const PB_SmartKnobState& state) {
|
||||
for (auto listener : listeners_) {
|
||||
xQueueOverwrite(listener, &state);
|
||||
}
|
||||
@ -277,7 +274,7 @@ void MotorTask::calibrate() {
|
||||
// So this value is based on experimentation.
|
||||
// TODO: dig into SimpleFOC calibration and find/fix the issue
|
||||
|
||||
Serial.println("\n\n\nStarting calibration, please do not touch to motor until complete!");
|
||||
log("\n\n\nStarting calibration, please DO NOT TOUCH MOTOR until complete!");
|
||||
|
||||
motor.controller = MotionControlType::angle_openloop;
|
||||
motor.pole_pairs = 1;
|
||||
@ -309,16 +306,16 @@ void MotorTask::calibrate() {
|
||||
motor.voltage_limit = 0;
|
||||
motor.move(a);
|
||||
|
||||
Serial.println();
|
||||
log("");
|
||||
|
||||
// TODO: check for no motor movement!
|
||||
|
||||
Serial.print("Sensor measures positive for positive motor rotation: ");
|
||||
log("Sensor measures positive for positive motor rotation:");
|
||||
if (end_sensor > start_sensor) {
|
||||
Serial.println("YES, Direction=CW");
|
||||
log("YES, Direction=CW");
|
||||
motor.initFOC(0, Direction::CW);
|
||||
} else {
|
||||
Serial.println("NO, Direction=CCW");
|
||||
log("NO, Direction=CCW");
|
||||
motor.initFOC(0, Direction::CCW);
|
||||
}
|
||||
|
||||
@ -326,22 +323,23 @@ void MotorTask::calibrate() {
|
||||
// #### Determine pole-pairs
|
||||
// Rotate 20 electrical revolutions and measure mechanical angle traveled, to calculate pole-pairs
|
||||
uint8_t electrical_revolutions = 20;
|
||||
Serial.printf("Going to measure %d electrical revolutions...\n", electrical_revolutions);
|
||||
snprintf(buf_, sizeof(buf_), "Going to measure %d electrical revolutions...", electrical_revolutions);
|
||||
log(buf_);
|
||||
motor.voltage_limit = 5;
|
||||
motor.move(a);
|
||||
Serial.println("Going to electrical zero...");
|
||||
log("Going to electrical zero...");
|
||||
float destination = a + _2PI;
|
||||
for (; a < destination; a += 0.03) {
|
||||
encoder.update();
|
||||
motor.move(a);
|
||||
delay(1);
|
||||
}
|
||||
Serial.println("pause..."); // Let momentum settle...
|
||||
log("pause..."); // Let momentum settle...
|
||||
for (uint16_t i = 0; i < 1000; i++) {
|
||||
encoder.update();
|
||||
delay(1);
|
||||
}
|
||||
Serial.println("Measuring...");
|
||||
log("Measuring...");
|
||||
|
||||
start_sensor = motor.sensor_direction * encoder.getAngle();
|
||||
destination = a + electrical_revolutions * _2PI;
|
||||
@ -360,16 +358,17 @@ void MotorTask::calibrate() {
|
||||
motor.move(a);
|
||||
|
||||
if (fabsf(motor.shaft_angle - motor.target) > 1 * PI / 180) {
|
||||
Serial.println("ERROR: motor did not reach target!");
|
||||
log("ERROR: motor did not reach target!");
|
||||
while(1) {}
|
||||
}
|
||||
|
||||
float electrical_per_mechanical = electrical_revolutions * _2PI / (end_sensor - start_sensor);
|
||||
Serial.print("Electrical angle / mechanical angle (i.e. pole pairs) = ");
|
||||
Serial.println(electrical_per_mechanical);
|
||||
snprintf(buf_, sizeof(buf_), "Electrical angle / mechanical angle (i.e. pole pairs) = %.2f", electrical_per_mechanical);
|
||||
log(buf_);
|
||||
|
||||
int measured_pole_pairs = (int)round(electrical_per_mechanical);
|
||||
Serial.printf("Pole pairs set to %d\n", measured_pole_pairs);
|
||||
snprintf(buf_, sizeof(buf_), "Pole pairs set to %d", measured_pole_pairs);
|
||||
log(buf_);
|
||||
|
||||
delay(1000);
|
||||
|
||||
@ -396,11 +395,8 @@ void MotorTask::calibrate() {
|
||||
offset_x += cosf(offset_angle);
|
||||
offset_y += sinf(offset_angle);
|
||||
|
||||
Serial.print(degrees(real_electrical_angle));
|
||||
Serial.print(", ");
|
||||
Serial.print(degrees(measured_electrical_angle));
|
||||
Serial.print(", ");
|
||||
Serial.println(degrees(_normalizeAngle(offset_angle)));
|
||||
snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
|
||||
log(buf_);
|
||||
}
|
||||
for (; a > destination2; a -= 0.4) {
|
||||
motor.move(a);
|
||||
@ -416,11 +412,8 @@ void MotorTask::calibrate() {
|
||||
offset_x += cosf(offset_angle);
|
||||
offset_y += sinf(offset_angle);
|
||||
|
||||
Serial.print(degrees(real_electrical_angle));
|
||||
Serial.print(", ");
|
||||
Serial.print(degrees(measured_electrical_angle));
|
||||
Serial.print(", ");
|
||||
Serial.println(degrees(_normalizeAngle(offset_angle)));
|
||||
snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
|
||||
log(buf_);
|
||||
}
|
||||
motor.voltage_limit = 0;
|
||||
motor.move(a);
|
||||
@ -435,14 +428,39 @@ void MotorTask::calibrate() {
|
||||
motor.voltage_limit = 5;
|
||||
motor.controller = MotionControlType::torque;
|
||||
|
||||
Serial.print("\n\nRESULTS:\n Update these constants at the top of " __FILE__ "\n ZERO_ELECTRICAL_OFFSET: ");
|
||||
Serial.println(motor.zero_electric_angle);
|
||||
Serial.print(" FOC_DIRECTION: ");
|
||||
log("\n\nRESULTS:\n Update these constants at the top of " __FILE__);
|
||||
snprintf(buf_, sizeof(buf_), " ZERO_ELECTRICAL_OFFSET: %.2f", motor.zero_electric_angle);
|
||||
log(buf_);
|
||||
if (motor.sensor_direction == Direction::CW) {
|
||||
Serial.println("Direction::CW");
|
||||
log(" FOC_DIRECTION: Direction::CW");
|
||||
} else {
|
||||
Serial.println("Direction::CCW");
|
||||
log(" FOC_DIRECTION: Direction::CCW");
|
||||
}
|
||||
Serial.printf(" MOTOR_POLE_PAIRS: %d\n", motor.pole_pairs);
|
||||
snprintf(buf_, sizeof(buf_), " MOTOR_POLE_PAIRS: %d", motor.pole_pairs);
|
||||
log(buf_);
|
||||
delay(2000);
|
||||
}
|
||||
|
||||
void MotorTask::checkSensorError() {
|
||||
#if SENSOR_TLV
|
||||
if (encoder.getAndClearError()) {
|
||||
log("LOCKED!");
|
||||
}
|
||||
#elif SENSOR_MT6701
|
||||
MT6701Error error = encoder.getAndClearError();
|
||||
if (error.error) {
|
||||
snprintf(buf_, sizeof(buf_), "CRC error. Received %d; calculated %d", error.received_crc, error.calculated_crc);
|
||||
log(buf_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotorTask::setLogger(Logger* logger) {
|
||||
logger_ = logger;
|
||||
}
|
||||
|
||||
void MotorTask::log(const char* msg) {
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log(msg);
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,13 @@
|
||||
#include <SimpleFOC.h>
|
||||
#include <vector>
|
||||
|
||||
#include "knob_data.h"
|
||||
#include "logger.h"
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
#include "task.h"
|
||||
|
||||
|
||||
enum class CommandType {
|
||||
CALIBRATE,
|
||||
CONFIG,
|
||||
HAPTIC,
|
||||
};
|
||||
@ -20,7 +22,8 @@ struct HapticData {
|
||||
struct Command {
|
||||
CommandType command_type;
|
||||
union CommandData {
|
||||
KnobConfig config;
|
||||
uint8_t unused;
|
||||
PB_SmartKnobConfig config;
|
||||
HapticData haptic;
|
||||
};
|
||||
CommandData data;
|
||||
@ -33,23 +36,28 @@ class MotorTask : public Task<MotorTask> {
|
||||
MotorTask(const uint8_t task_core);
|
||||
~MotorTask();
|
||||
|
||||
void setConfig(const KnobConfig& config);
|
||||
void setConfig(const PB_SmartKnobConfig& config);
|
||||
void playHaptic(bool press);
|
||||
void runCalibration();
|
||||
|
||||
void addListener(QueueHandle_t queue);
|
||||
void setLogger(Logger* logger);
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
QueueHandle_t queue_;
|
||||
|
||||
Logger* logger_;
|
||||
std::vector<QueueHandle_t> listeners_;
|
||||
char buf_[72];
|
||||
|
||||
// BLDC motor & driver instance
|
||||
BLDCMotor motor = BLDCMotor(1);
|
||||
BLDCDriver6PWM driver = BLDCDriver6PWM(PIN_UH, PIN_UL, PIN_VH, PIN_VL, PIN_WH, PIN_WL);
|
||||
|
||||
void publish(const KnobState& state);
|
||||
void publish(const PB_SmartKnobState& state);
|
||||
void calibrate();
|
||||
void checkSensorError();
|
||||
void log(const char* msg);
|
||||
};
|
||||
|
@ -105,7 +105,11 @@ float MT6701Sensor::getSensorAngle() {
|
||||
x_ = new_x * ALPHA + x_ * (1-ALPHA);
|
||||
y_ = new_y * ALPHA + y_ * (1-ALPHA);
|
||||
} else {
|
||||
Serial.printf("Bad CRC. expected %d, actual %d\n", calculated_crc, received_crc);
|
||||
error_ = {
|
||||
.error = true,
|
||||
.received_crc = received_crc,
|
||||
.calculated_crc = calculated_crc,
|
||||
};
|
||||
}
|
||||
|
||||
last_update_ = now;
|
||||
@ -117,4 +121,10 @@ float MT6701Sensor::getSensorAngle() {
|
||||
return rad;
|
||||
}
|
||||
|
||||
MT6701Error MT6701Sensor::getAndClearError() {
|
||||
MT6701Error out = error_;
|
||||
error_ = {};
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif
|
@ -3,6 +3,12 @@
|
||||
#include <SimpleFOC.h>
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
struct MT6701Error {
|
||||
bool error;
|
||||
uint8_t received_crc;
|
||||
uint8_t calculated_crc;
|
||||
};
|
||||
|
||||
class MT6701Sensor : public Sensor {
|
||||
public:
|
||||
MT6701Sensor();
|
||||
@ -16,6 +22,8 @@ class MT6701Sensor : public Sensor {
|
||||
// Calling this method directly does not update the base-class internal fields.
|
||||
// Use update() when calling from outside code.
|
||||
float getSensorAngle();
|
||||
|
||||
MT6701Error getAndClearError();
|
||||
private:
|
||||
|
||||
spi_device_handle_t spi_device_;
|
||||
@ -24,4 +32,6 @@ class MT6701Sensor : public Sensor {
|
||||
float x_;
|
||||
float y_;
|
||||
uint32_t last_update_;
|
||||
|
||||
MT6701Error error_ = {};
|
||||
};
|
||||
|
30
firmware/src/proto_gen/smartknob.pb.c
Normal file
30
firmware/src/proto_gen/smartknob.pb.c
Normal file
@ -0,0 +1,30 @@
|
||||
/* Automatically generated nanopb constant definitions */
|
||||
/* Generated by nanopb-0.4.7-dev */
|
||||
|
||||
#include "smartknob.pb.h"
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(PB_FromSmartKnob, PB_FromSmartKnob, 2)
|
||||
|
||||
|
||||
PB_BIND(PB_Ack, PB_Ack, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_Log, PB_Log, 2)
|
||||
|
||||
|
||||
PB_BIND(PB_SmartKnobState, PB_SmartKnobState, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_ToSmartknob, PB_ToSmartknob, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_SmartKnobConfig, PB_SmartKnobConfig, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_RequestState, PB_RequestState, AUTO)
|
||||
|
||||
|
||||
|
187
firmware/src/proto_gen/smartknob.pb.h
Normal file
187
firmware/src/proto_gen/smartknob.pb.h
Normal file
@ -0,0 +1,187 @@
|
||||
/* Automatically generated nanopb header */
|
||||
/* Generated by nanopb-0.4.7-dev */
|
||||
|
||||
#ifndef PB_PB_SMARTKNOB_PB_H_INCLUDED
|
||||
#define PB_PB_SMARTKNOB_PB_H_INCLUDED
|
||||
#include <pb.h>
|
||||
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _PB_RequestState {
|
||||
char dummy_field;
|
||||
} PB_RequestState;
|
||||
|
||||
typedef struct _PB_Ack {
|
||||
uint32_t nonce;
|
||||
} PB_Ack;
|
||||
|
||||
typedef struct _PB_Log {
|
||||
char msg[256];
|
||||
} PB_Log;
|
||||
|
||||
typedef struct _PB_SmartKnobConfig {
|
||||
int32_t num_positions;
|
||||
int32_t position;
|
||||
float position_width_radians;
|
||||
float detent_strength_unit;
|
||||
float endstop_strength_unit;
|
||||
float snap_point;
|
||||
char text[51];
|
||||
} PB_SmartKnobConfig;
|
||||
|
||||
typedef struct _PB_SmartKnobState {
|
||||
int32_t current_position;
|
||||
float sub_position_unit;
|
||||
bool has_config;
|
||||
PB_SmartKnobConfig config;
|
||||
} PB_SmartKnobState;
|
||||
|
||||
/* Message TO the Smartknob from the USB host */
|
||||
typedef struct _PB_ToSmartknob {
|
||||
uint32_t nonce;
|
||||
pb_size_t which_payload;
|
||||
union {
|
||||
PB_RequestState request_state;
|
||||
PB_SmartKnobConfig smartknob_config;
|
||||
} payload;
|
||||
} PB_ToSmartknob;
|
||||
|
||||
/* Message FROM the SmartKnob to USB host */
|
||||
typedef struct _PB_FromSmartKnob {
|
||||
pb_size_t which_payload;
|
||||
union {
|
||||
PB_Ack ack;
|
||||
PB_Log log;
|
||||
PB_SmartKnobState smartknob_state;
|
||||
} payload;
|
||||
} PB_FromSmartKnob;
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define PB_FromSmartKnob_init_default {0, {PB_Ack_init_default}}
|
||||
#define PB_Ack_init_default {0}
|
||||
#define PB_Log_init_default {""}
|
||||
#define PB_SmartKnobState_init_default {0, 0, false, PB_SmartKnobConfig_init_default}
|
||||
#define PB_ToSmartknob_init_default {0, 0, {PB_RequestState_init_default}}
|
||||
#define PB_SmartKnobConfig_init_default {0, 0, 0, 0, 0, 0, ""}
|
||||
#define PB_RequestState_init_default {0}
|
||||
#define PB_FromSmartKnob_init_zero {0, {PB_Ack_init_zero}}
|
||||
#define PB_Ack_init_zero {0}
|
||||
#define PB_Log_init_zero {""}
|
||||
#define PB_SmartKnobState_init_zero {0, 0, false, PB_SmartKnobConfig_init_zero}
|
||||
#define PB_ToSmartknob_init_zero {0, 0, {PB_RequestState_init_zero}}
|
||||
#define PB_SmartKnobConfig_init_zero {0, 0, 0, 0, 0, 0, ""}
|
||||
#define PB_RequestState_init_zero {0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define PB_Ack_nonce_tag 1
|
||||
#define PB_Log_msg_tag 1
|
||||
#define PB_SmartKnobConfig_num_positions_tag 1
|
||||
#define PB_SmartKnobConfig_position_tag 2
|
||||
#define PB_SmartKnobConfig_position_width_radians_tag 3
|
||||
#define PB_SmartKnobConfig_detent_strength_unit_tag 4
|
||||
#define PB_SmartKnobConfig_endstop_strength_unit_tag 5
|
||||
#define PB_SmartKnobConfig_snap_point_tag 6
|
||||
#define PB_SmartKnobConfig_text_tag 7
|
||||
#define PB_SmartKnobState_current_position_tag 1
|
||||
#define PB_SmartKnobState_sub_position_unit_tag 2
|
||||
#define PB_SmartKnobState_config_tag 3
|
||||
#define PB_ToSmartknob_nonce_tag 1
|
||||
#define PB_ToSmartknob_request_state_tag 2
|
||||
#define PB_ToSmartknob_smartknob_config_tag 3
|
||||
#define PB_FromSmartKnob_ack_tag 1
|
||||
#define PB_FromSmartKnob_log_tag 2
|
||||
#define PB_FromSmartKnob_smartknob_state_tag 3
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define PB_FromSmartKnob_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,ack,payload.ack), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,log,payload.log), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,smartknob_state,payload.smartknob_state), 3)
|
||||
#define PB_FromSmartKnob_CALLBACK NULL
|
||||
#define PB_FromSmartKnob_DEFAULT NULL
|
||||
#define PB_FromSmartKnob_payload_ack_MSGTYPE PB_Ack
|
||||
#define PB_FromSmartKnob_payload_log_MSGTYPE PB_Log
|
||||
#define PB_FromSmartKnob_payload_smartknob_state_MSGTYPE PB_SmartKnobState
|
||||
|
||||
#define PB_Ack_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, nonce, 1)
|
||||
#define PB_Ack_CALLBACK NULL
|
||||
#define PB_Ack_DEFAULT NULL
|
||||
|
||||
#define PB_Log_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, msg, 1)
|
||||
#define PB_Log_CALLBACK NULL
|
||||
#define PB_Log_DEFAULT NULL
|
||||
|
||||
#define PB_SmartKnobState_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, current_position, 1) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, sub_position_unit, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, config, 3)
|
||||
#define PB_SmartKnobState_CALLBACK NULL
|
||||
#define PB_SmartKnobState_DEFAULT NULL
|
||||
#define PB_SmartKnobState_config_MSGTYPE PB_SmartKnobConfig
|
||||
|
||||
#define PB_ToSmartknob_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, nonce, 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,request_state,payload.request_state), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,smartknob_config,payload.smartknob_config), 3)
|
||||
#define PB_ToSmartknob_CALLBACK NULL
|
||||
#define PB_ToSmartknob_DEFAULT NULL
|
||||
#define PB_ToSmartknob_payload_request_state_MSGTYPE PB_RequestState
|
||||
#define PB_ToSmartknob_payload_smartknob_config_MSGTYPE PB_SmartKnobConfig
|
||||
|
||||
#define PB_SmartKnobConfig_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, num_positions, 1) \
|
||||
X(a, STATIC, SINGULAR, INT32, position, 2) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, position_width_radians, 3) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, detent_strength_unit, 4) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, endstop_strength_unit, 5) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, snap_point, 6) \
|
||||
X(a, STATIC, SINGULAR, STRING, text, 7)
|
||||
#define PB_SmartKnobConfig_CALLBACK NULL
|
||||
#define PB_SmartKnobConfig_DEFAULT NULL
|
||||
|
||||
#define PB_RequestState_FIELDLIST(X, a) \
|
||||
|
||||
#define PB_RequestState_CALLBACK NULL
|
||||
#define PB_RequestState_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t PB_FromSmartKnob_msg;
|
||||
extern const pb_msgdesc_t PB_Ack_msg;
|
||||
extern const pb_msgdesc_t PB_Log_msg;
|
||||
extern const pb_msgdesc_t PB_SmartKnobState_msg;
|
||||
extern const pb_msgdesc_t PB_ToSmartknob_msg;
|
||||
extern const pb_msgdesc_t PB_SmartKnobConfig_msg;
|
||||
extern const pb_msgdesc_t PB_RequestState_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define PB_FromSmartKnob_fields &PB_FromSmartKnob_msg
|
||||
#define PB_Ack_fields &PB_Ack_msg
|
||||
#define PB_Log_fields &PB_Log_msg
|
||||
#define PB_SmartKnobState_fields &PB_SmartKnobState_msg
|
||||
#define PB_ToSmartknob_fields &PB_ToSmartknob_msg
|
||||
#define PB_SmartKnobConfig_fields &PB_SmartKnobConfig_msg
|
||||
#define PB_RequestState_fields &PB_RequestState_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define PB_Ack_size 6
|
||||
#define PB_FromSmartKnob_size 261
|
||||
#define PB_Log_size 258
|
||||
#define PB_RequestState_size 0
|
||||
#define PB_SmartKnobConfig_size 94
|
||||
#define PB_SmartKnobState_size 112
|
||||
#define PB_ToSmartknob_size 102
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
20
firmware/src/proto_helpers.h
Normal file
20
firmware/src/proto_helpers.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
|
||||
bool config_eq(PB_SmartKnobConfig& first, PB_SmartKnobConfig& second) {
|
||||
return first.detent_strength_unit == second.detent_strength_unit
|
||||
&& first.endstop_strength_unit == second.endstop_strength_unit
|
||||
&& first.num_positions == second.num_positions
|
||||
&& first.position == second.position
|
||||
&& first.position_width_radians == second.position_width_radians
|
||||
&& first.snap_point == second.snap_point
|
||||
&& strcmp(first.text, second.text) == 0;
|
||||
}
|
||||
|
||||
bool state_eq(PB_SmartKnobState& first, PB_SmartKnobState& second) {
|
||||
return first.has_config == second.has_config
|
||||
&& (!first.has_config || config_eq(first.config, second.config))
|
||||
&& first.current_position == second.current_position
|
||||
&& first.sub_position_unit == second.sub_position_unit;
|
||||
}
|
21
firmware/src/serial/crc32.cpp
Normal file
21
firmware/src/serial/crc32.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
/* Simple public domain implementation of the standard CRC32 checksum.
|
||||
* Outputs the checksum for each file given as a command line argument.
|
||||
* Invalid file names and files that cause errors are silently skipped.
|
||||
* The program reads from stdin if it is called with no arguments. */
|
||||
|
||||
#include "crc32.h"
|
||||
|
||||
static uint32_t crc32_for_byte(uint32_t r) {
|
||||
for(int j = 0; j < 8; ++j)
|
||||
r = (r & 1? 0: (uint32_t)0xEDB88320L) ^ r >> 1;
|
||||
return r ^ (uint32_t)0xFF000000L;
|
||||
}
|
||||
|
||||
void crc32(const void *data, size_t n_bytes, uint32_t* crc) {
|
||||
static uint32_t table[0x100];
|
||||
if(!*table)
|
||||
for(size_t i = 0; i < 0x100; ++i)
|
||||
table[i] = crc32_for_byte(i);
|
||||
for(size_t i = 0; i < n_bytes; ++i)
|
||||
*crc = table[(uint8_t)*crc ^ ((uint8_t*)data)[i]] ^ *crc >> 8;
|
||||
}
|
11
firmware/src/serial/crc32.h
Normal file
11
firmware/src/serial/crc32.h
Normal file
@ -0,0 +1,11 @@
|
||||
/* Simple public domain implementation of the standard CRC32 checksum.
|
||||
* Outputs the checksum for each file given as a command line argument.
|
||||
* Invalid file names and files that cause errors are silently skipped.
|
||||
* The program reads from stdin if it is called with no arguments. */
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void crc32(const void *data, size_t n_bytes, uint32_t* crc);
|
28
firmware/src/serial/serial_protocol.h
Normal file
28
firmware/src/serial/serial_protocol.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "../logger.h"
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#define SERIAL_PROTOCOL_LEGACY 0
|
||||
#define SERIAL_PROTOCOL_PROTO 1
|
||||
|
||||
typedef std::function<void(uint8_t)> ProtocolChangeCallback;
|
||||
|
||||
class SerialProtocol : public Logger {
|
||||
public:
|
||||
SerialProtocol() : Logger() {}
|
||||
virtual ~SerialProtocol(){}
|
||||
|
||||
virtual void loop() = 0;
|
||||
|
||||
virtual void handleState(const PB_SmartKnobState& state) = 0;
|
||||
|
||||
virtual void setProtocolChangeCallback(ProtocolChangeCallback cb) {
|
||||
protocol_change_callback_ = cb;
|
||||
}
|
||||
|
||||
protected:
|
||||
ProtocolChangeCallback protocol_change_callback_;
|
||||
};
|
44
firmware/src/serial/serial_protocol_plaintext.cpp
Normal file
44
firmware/src/serial/serial_protocol_plaintext.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "serial_protocol_plaintext.h"
|
||||
|
||||
void SerialProtocolPlaintext::handleState(const PB_SmartKnobState& state) {
|
||||
bool substantial_change = (latest_state_.current_position != state.current_position)
|
||||
|| (latest_state_.config.detent_strength_unit != state.config.detent_strength_unit)
|
||||
|| (latest_state_.config.endstop_strength_unit != state.config.endstop_strength_unit)
|
||||
|| (latest_state_.config.num_positions != state.config.num_positions);
|
||||
latest_state_ = state;
|
||||
|
||||
if (substantial_change) {
|
||||
stream_.printf("STATE: %d/%d (detent strength: %0.2f, width: %0.0f deg, endstop strength: %0.2f)\n", state.current_position, state.config.num_positions - 1, state.config.detent_strength_unit, degrees(state.config.position_width_radians), state.config.endstop_strength_unit);
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProtocolPlaintext::log(const char* msg) {
|
||||
stream_.print("LOG: ");
|
||||
stream_.println(msg);
|
||||
}
|
||||
|
||||
void SerialProtocolPlaintext::loop() {
|
||||
while (stream_.available() > 0) {
|
||||
int b = stream_.read();
|
||||
if (b == 0) {
|
||||
if (protocol_change_callback_) {
|
||||
protocol_change_callback_(SERIAL_PROTOCOL_PROTO);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (b == ' ') {
|
||||
if (demo_config_change_callback_) {
|
||||
demo_config_change_callback_();
|
||||
}
|
||||
} else if (b == 'C') {
|
||||
motor_task_.runCalibration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProtocolPlaintext::init(DemoConfigChangeCallback cb) {
|
||||
demo_config_change_callback_ = cb;
|
||||
stream_.println("SmartKnob starting!\n\nSerial mode: plaintext\nPress 'C' at any time to calibrate.\nPress <Space> to change haptic modes.");
|
||||
}
|
26
firmware/src/serial/serial_protocol_plaintext.h
Normal file
26
firmware/src/serial/serial_protocol_plaintext.h
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "motor_task.h"
|
||||
#include "serial_protocol.h"
|
||||
#include "uart_stream.h"
|
||||
|
||||
typedef std::function<void(void)> DemoConfigChangeCallback;
|
||||
|
||||
class SerialProtocolPlaintext : public SerialProtocol {
|
||||
public:
|
||||
SerialProtocolPlaintext(Stream& stream, MotorTask& motor_task) : SerialProtocol(), stream_(stream), motor_task_(motor_task) {}
|
||||
~SerialProtocolPlaintext(){}
|
||||
void log(const char* msg) override;
|
||||
void loop() override;
|
||||
void handleState(const PB_SmartKnobState& state) override;
|
||||
|
||||
void init(DemoConfigChangeCallback cb);
|
||||
|
||||
private:
|
||||
Stream& stream_;
|
||||
MotorTask& motor_task_;
|
||||
PB_SmartKnobState latest_state_ = {};
|
||||
DemoConfigChangeCallback demo_config_change_callback_;
|
||||
};
|
152
firmware/src/serial/serial_protocol_protobuf.cpp
Normal file
152
firmware/src/serial/serial_protocol_protobuf.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include <PacketSerial.h>
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "../proto_helpers.h"
|
||||
|
||||
#include "crc32.h"
|
||||
#include "pb_encode.h"
|
||||
#include "pb_decode.h"
|
||||
#include "serial_protocol_protobuf.h"
|
||||
|
||||
static SerialProtocolProtobuf* singleton_for_packet_serial = 0;
|
||||
|
||||
static const uint16_t MIN_STATE_INTERVAL_MILLIS = 5;
|
||||
static const uint16_t PERIODIC_STATE_INTERVAL_MILLIS = 5000;
|
||||
|
||||
SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task) :
|
||||
SerialProtocol(),
|
||||
stream_(stream),
|
||||
motor_task_(motor_task),
|
||||
packet_serial_() {
|
||||
packet_serial_.setStream(&stream);
|
||||
|
||||
// Note: not threadsafe or instance safe!! but PacketSerial requires a legacy function pointer, so we can't
|
||||
// use a member, std::function, or lambda with captures
|
||||
assert(singleton_for_packet_serial == 0);
|
||||
singleton_for_packet_serial = this;
|
||||
|
||||
packet_serial_.setPacketHandler([](const uint8_t* buffer, size_t size) {
|
||||
singleton_for_packet_serial->handlePacket(buffer, size);
|
||||
});
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::handleState(const PB_SmartKnobState& state) {
|
||||
latest_state_ = state;
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::ack(uint32_t nonce) {
|
||||
pb_tx_buffer_ = {};
|
||||
pb_tx_buffer_.which_payload = PB_FromSmartKnob_ack_tag;
|
||||
pb_tx_buffer_.payload.ack.nonce = nonce;
|
||||
sendPbTxBuffer();
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::log(const char* msg) {
|
||||
pb_tx_buffer_ = {};
|
||||
pb_tx_buffer_.which_payload = PB_FromSmartKnob_log_tag;
|
||||
|
||||
strlcpy(pb_tx_buffer_.payload.log.msg, msg, sizeof(pb_tx_buffer_.payload.log.msg));
|
||||
|
||||
sendPbTxBuffer();
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::loop() {
|
||||
do {
|
||||
packet_serial_.update();
|
||||
} while (stream_.available());
|
||||
|
||||
// Rate limit state change transmissions
|
||||
bool state_changed = !state_eq(latest_state_, last_sent_state_) && millis() - last_sent_state_millis_ >= MIN_STATE_INTERVAL_MILLIS;
|
||||
|
||||
// Send state periodically or when forced, regardless of rate limit for state changes
|
||||
bool force_send_state = state_requested_ || millis() - last_sent_state_millis_ > PERIODIC_STATE_INTERVAL_MILLIS;
|
||||
if (state_changed || force_send_state) {
|
||||
state_requested_ = false;
|
||||
pb_tx_buffer_ = {};
|
||||
pb_tx_buffer_.which_payload = PB_FromSmartKnob_smartknob_state_tag;
|
||||
pb_tx_buffer_.payload.smartknob_state = latest_state_;
|
||||
|
||||
sendPbTxBuffer();
|
||||
|
||||
last_sent_state_ = latest_state_;
|
||||
last_sent_state_millis_ = millis();
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
|
||||
if (size <= 4) {
|
||||
// Too small, ignore bad packet
|
||||
log("Small packet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute and append little-endian CRC32
|
||||
uint32_t expected_crc = 0;
|
||||
crc32(buffer, size - 4, &expected_crc);
|
||||
|
||||
uint32_t provided_crc = buffer[size - 4]
|
||||
| (buffer[size - 3] << 8)
|
||||
| (buffer[size - 2] << 16)
|
||||
| (buffer[size - 1] << 24);
|
||||
|
||||
if (expected_crc != provided_crc) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Bad CRC (%u byte packet). Expected %08x but got %08x.", size - 4, expected_crc, provided_crc);
|
||||
log(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
pb_istream_t stream = pb_istream_from_buffer(buffer, size - 4);
|
||||
if (!pb_decode(&stream, PB_ToSmartknob_fields, &pb_rx_buffer_)) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Decoding failed: %s", PB_GET_ERROR(&stream));
|
||||
log(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always ACK immediately
|
||||
ack(pb_rx_buffer_.nonce);
|
||||
if (pb_rx_buffer_.nonce == last_nonce_) {
|
||||
// Ignore any extraneous retries
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Already handled nonce %u", pb_rx_buffer_.nonce);
|
||||
log(buf);
|
||||
return;
|
||||
}
|
||||
last_nonce_ = pb_rx_buffer_.nonce;
|
||||
|
||||
switch (pb_rx_buffer_.which_payload) {
|
||||
case PB_ToSmartknob_smartknob_config_tag: {
|
||||
motor_task_.setConfig(pb_rx_buffer_.payload.smartknob_config);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Unknown payload type: %d", pb_rx_buffer_.which_payload);
|
||||
log(buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProtocolProtobuf::sendPbTxBuffer() {
|
||||
// Encode protobuf message to byte buffer
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(tx_buffer_, sizeof(tx_buffer_));
|
||||
if (!pb_encode(&stream, PB_FromSmartKnob_fields, &pb_tx_buffer_)) {
|
||||
stream_.println(stream.errmsg);
|
||||
stream_.flush();
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// Compute and append little-endian CRC32
|
||||
uint32_t crc = 0;
|
||||
crc32(tx_buffer_, stream.bytes_written, &crc);
|
||||
tx_buffer_[stream.bytes_written + 0] = (crc >> 0) & 0xFF;
|
||||
tx_buffer_[stream.bytes_written + 1] = (crc >> 8) & 0xFF;
|
||||
tx_buffer_[stream.bytes_written + 2] = (crc >> 16) & 0xFF;
|
||||
tx_buffer_[stream.bytes_written + 3] = (crc >> 24) & 0xFF;
|
||||
|
||||
// Encode and send proto+CRC as a COBS packet
|
||||
packet_serial_.send(tx_buffer_, stream.bytes_written + 4);
|
||||
}
|
41
firmware/src/serial/serial_protocol_protobuf.h
Normal file
41
firmware/src/serial/serial_protocol_protobuf.h
Normal file
@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <PacketSerial.h>
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "motor_task.h"
|
||||
#include "serial_protocol.h"
|
||||
#include "uart_stream.h"
|
||||
|
||||
class SerialProtocolProtobuf : public SerialProtocol {
|
||||
public:
|
||||
SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task);
|
||||
~SerialProtocolProtobuf(){}
|
||||
void log(const char* msg) override;
|
||||
void loop() override;
|
||||
void handleState(const PB_SmartKnobState& state) override;
|
||||
|
||||
private:
|
||||
Stream& stream_;
|
||||
MotorTask& motor_task_;
|
||||
|
||||
PB_FromSmartKnob pb_tx_buffer_;
|
||||
PB_ToSmartknob pb_rx_buffer_;
|
||||
|
||||
uint8_t tx_buffer_[PB_FromSmartKnob_size + 4]; // Max message size + CRC32
|
||||
|
||||
PacketSerial_<COBS, 0, (PB_ToSmartknob_size + 4) * 2 + 10> packet_serial_;
|
||||
|
||||
uint32_t last_nonce_;
|
||||
|
||||
PB_SmartKnobState latest_state_ = {};
|
||||
PB_SmartKnobState last_sent_state_ = {};
|
||||
uint32_t last_sent_state_millis_ = 0;
|
||||
|
||||
bool state_requested_;
|
||||
|
||||
void sendPbTxBuffer();
|
||||
void handlePacket(const uint8_t* buffer, size_t size);
|
||||
void ack(uint32_t nonce);
|
||||
};
|
64
firmware/src/serial/uart_stream.cpp
Normal file
64
firmware/src/serial/uart_stream.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2021 Scott Bezek and the splitflap contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <driver/uart.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "uart_stream.h"
|
||||
|
||||
UartStream::UartStream() : Stream() {
|
||||
}
|
||||
|
||||
void UartStream::begin() {
|
||||
uart_config_t conf;
|
||||
conf.baud_rate = MONITOR_SPEED;
|
||||
conf.data_bits = UART_DATA_8_BITS;
|
||||
conf.parity = UART_PARITY_DISABLE;
|
||||
conf.stop_bits = UART_STOP_BITS_1;
|
||||
conf.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
conf.rx_flow_ctrl_thresh = 0;
|
||||
conf.use_ref_tick = false;
|
||||
assert(uart_param_config(uart_port_, &conf) == ESP_OK);
|
||||
assert(uart_driver_install(uart_port_, 32000, 32000, 0, NULL, 0) == ESP_OK);
|
||||
}
|
||||
|
||||
int UartStream::peek() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int UartStream::available() {
|
||||
size_t size = 0;
|
||||
assert(uart_get_buffered_data_len(uart_port_, &size) == ESP_OK);
|
||||
return size;
|
||||
}
|
||||
|
||||
int UartStream::read() {
|
||||
uint8_t b;
|
||||
int res = uart_read_bytes(uart_port_, &b, 1, 0);
|
||||
return res != 1 ? -1 : b;
|
||||
}
|
||||
|
||||
void UartStream::flush() {
|
||||
|
||||
}
|
||||
|
||||
size_t UartStream::write(uint8_t b) {
|
||||
return uart_write_bytes(uart_port_, (char*)&b, 1);
|
||||
}
|
||||
|
||||
size_t UartStream::write(const uint8_t *buffer, size_t size) {
|
||||
return uart_write_bytes(uart_port_, (const char*)buffer, size);
|
||||
}
|
47
firmware/src/serial/uart_stream.h
Normal file
47
firmware/src/serial/uart_stream.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2021 Scott Bezek and the splitflap contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <driver/uart.h>
|
||||
|
||||
/**
|
||||
* Implementation of an Arduino Stream for UART serial communications using the esp uart driver
|
||||
* directly, rather than the Arduino HAL which has a small fixed underlying rx FIFO size and
|
||||
* potentially other issues that cause dropped bytes at high speeds/bursts.
|
||||
*
|
||||
* This is not a full or optimized implementation; just the minimal necessary for this project.
|
||||
*/
|
||||
class UartStream : public Stream {
|
||||
public:
|
||||
UartStream();
|
||||
|
||||
void begin();
|
||||
|
||||
// Stream methods
|
||||
int available() override;
|
||||
int read() override;
|
||||
int peek() override;
|
||||
void flush() override;
|
||||
|
||||
// Print methods
|
||||
size_t write(uint8_t b) override;
|
||||
size_t write(const uint8_t *buffer, size_t size) override;
|
||||
|
||||
private:
|
||||
const uart_port_t uart_port_ = UART_NUM_0;
|
||||
};
|
@ -35,7 +35,7 @@ float TlvSensor::getSensorAngle() {
|
||||
}
|
||||
}
|
||||
if (all_same) {
|
||||
Serial.println("LOCKED!");
|
||||
error_ = true;
|
||||
init(wire_, invert_);
|
||||
// Force unique frame counts to avoid reset loop
|
||||
for (uint8_t i = 1; i < sizeof(frame_counts_); i++) {
|
||||
@ -49,3 +49,9 @@ float TlvSensor::getSensorAngle() {
|
||||
}
|
||||
return rad;
|
||||
}
|
||||
|
||||
bool TlvSensor::getAndClearError() {
|
||||
bool error = error_;
|
||||
error_ = false;
|
||||
return error;
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ class TlvSensor : public Sensor {
|
||||
// Calling this method directly does not update the base-class internal fields.
|
||||
// Use update() when calling from outside code.
|
||||
float getSensorAngle();
|
||||
|
||||
bool getAndClearError();
|
||||
private:
|
||||
Tlv493d tlv_ = Tlv493d();
|
||||
float x_;
|
||||
@ -24,6 +26,8 @@ class TlvSensor : public Sensor {
|
||||
TwoWire* wire_;
|
||||
bool invert_;
|
||||
|
||||
bool error_ = false;
|
||||
|
||||
uint8_t frame_counts_[3] = {};
|
||||
uint8_t cur_frame_count_index_ = 0;
|
||||
};
|
||||
|
@ -10,11 +10,16 @@
|
||||
|
||||
[platformio]
|
||||
default_envs = view
|
||||
src_dir = firmware/src
|
||||
lib_dir = firmware/lib
|
||||
include_dir = firmware/include
|
||||
test_dir = firmware/test
|
||||
data_dir = firmware/data
|
||||
|
||||
[base_config]
|
||||
platform = espressif32@3.4
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_speed = 921600
|
||||
monitor_flags =
|
||||
--eol=CRLF
|
||||
--echo
|
||||
@ -23,9 +28,9 @@ lib_deps =
|
||||
askuric/Simple FOC @ 2.2.0
|
||||
infineon/TLV493D-Magnetic-Sensor @ 1.0.3
|
||||
bxparks/AceButton @ 1.9.1
|
||||
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
-DMONITOR_SPEED=921600
|
||||
|
||||
[env:view]
|
||||
extends = base_config
|
||||
@ -36,6 +41,12 @@ lib_deps =
|
||||
fastled/FastLED @ 3.5.0
|
||||
bogde/HX711 @ 0.7.5
|
||||
adafruit/Adafruit VEML7700 Library @ 1.1.1
|
||||
bakercp/PacketSerial @ 1.4.0
|
||||
nanopb/Nanopb @ 0.4.6 ; Ideally this would reference the nanopb submodule, but that would require
|
||||
; everyone to check out submodules to just compile, so we use the library
|
||||
; registry for the runtime. The submodule is available for manually updating
|
||||
; the pre-compiled (checked in) .pb.h/c files when proto files change, but is
|
||||
; otherwise not used during application firmware compilation.
|
||||
|
||||
build_flags =
|
||||
${base_config.build_flags}
|
34
proto/generate_protobuf.py
Executable file
34
proto/generate_protobuf.py
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def run():
|
||||
SCRIPT_PATH = Path(__file__).absolute().parent
|
||||
REPO_ROOT = SCRIPT_PATH.parent
|
||||
|
||||
proto_path = REPO_ROOT / 'proto'
|
||||
|
||||
nanopb_path = REPO_ROOT / 'thirdparty' / 'nanopb'
|
||||
|
||||
# Make sure nanopb submodule is available
|
||||
if not os.path.isdir(nanopb_path):
|
||||
print(f'Nanopb checkout not found! Make sure you have inited/updated the submodule located at {nanopb_path}', file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
nanopb_generator_path = nanopb_path / 'generator' / 'nanopb_generator.py'
|
||||
c_generated_output_path = REPO_ROOT / 'firmware' / 'src' / 'proto_gen'
|
||||
|
||||
proto_files = [f for f in os.listdir(proto_path) if f.endswith('.proto')]
|
||||
assert len(proto_files) > 0, 'No proto files found!'
|
||||
|
||||
# Generate C files via nanopb
|
||||
subprocess.check_call(['python3', nanopb_generator_path, '-D', c_generated_output_path] + proto_files, cwd=proto_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
55
proto/smartknob.proto
Normal file
55
proto/smartknob.proto
Normal file
@ -0,0 +1,55 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "nanopb.proto";
|
||||
|
||||
package PB;
|
||||
|
||||
/*
|
||||
* Message FROM the SmartKnob to USB host
|
||||
*/
|
||||
message FromSmartKnob {
|
||||
oneof payload {
|
||||
Ack ack = 1;
|
||||
Log log = 2;
|
||||
SmartKnobState smartknob_state = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Ack {
|
||||
uint32 nonce = 1;
|
||||
}
|
||||
|
||||
message Log {
|
||||
string msg = 1 [(nanopb).max_length = 255];
|
||||
}
|
||||
|
||||
message SmartKnobState {
|
||||
int32 current_position = 1;
|
||||
float sub_position_unit = 2;
|
||||
SmartKnobConfig config = 3;
|
||||
}
|
||||
|
||||
/*
|
||||
* Message TO the Smartknob from the USB host
|
||||
*/
|
||||
message ToSmartknob {
|
||||
uint32 nonce = 1;
|
||||
|
||||
oneof payload {
|
||||
RequestState request_state = 2;
|
||||
SmartKnobConfig smartknob_config = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message SmartKnobConfig {
|
||||
int32 num_positions = 1;
|
||||
int32 position = 2;
|
||||
float position_width_radians = 3;
|
||||
float detent_strength_unit = 4;
|
||||
float endstop_strength_unit = 5;
|
||||
float snap_point = 6;
|
||||
string text = 7 [(nanopb).max_length = 50];
|
||||
}
|
||||
|
||||
message RequestState {}
|
||||
|
2
software/js/.gitignore
vendored
Normal file
2
software/js/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist
|
1
software/js/.npmrc
Normal file
1
software/js/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
21
software/js/README.md
Normal file
21
software/js/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Typescript SmartKnob protobuf interface library
|
||||
|
||||
### Requirements (nvm is recommended)
|
||||
|
||||
- node >= 18.11.0
|
||||
- npm >= 8.19.2
|
||||
|
||||
### Setup
|
||||
|
||||
```
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
Connect the SmartKnob via USB, then run the example:
|
||||
|
||||
```
|
||||
npm run example
|
||||
```
|
4315
software/js/package-lock.json
generated
Normal file
4315
software/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
software/js/package.json
Normal file
24
software/js/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"npm": ">=8.19.2",
|
||||
"node": ">=18.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"example": "npm run -w example main"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/smartknobjs-proto",
|
||||
"packages/smartknobjs",
|
||||
"packages/example"
|
||||
],
|
||||
"devDependencies": {
|
||||
"eslint-config-prettier": "^8.5.0"
|
||||
}
|
||||
}
|
19
software/js/packages/example/.eslintrc
Normal file
19
software/js/packages/example/.eslintrc
Normal file
@ -0,0 +1,19 @@
|
||||
// .eslintrc
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
|
||||
},
|
||||
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
10
software/js/packages/example/.prettierrc
Normal file
10
software/js/packages/example/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
28
software/js/packages/example/package.json
Normal file
28
software/js/packages/example/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "example",
|
||||
"version": "0.1.0",
|
||||
"description": "SmartKnob Interface Library",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"**/*.+(js|ts|json)\"",
|
||||
"lint": "eslint --ext .js,.ts .",
|
||||
"main": "ts-node src/index.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"serialport": "^9.2.4",
|
||||
"smartknobjs": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/serialport": "^8.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.25.0",
|
||||
"prettier": "^2.4.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
60
software/js/packages/example/src/index.ts
Normal file
60
software/js/packages/example/src/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import SerialPort = require('serialport')
|
||||
import {SmartKnob} from 'smartknobjs'
|
||||
import {PB} from 'smartknobjs-proto'
|
||||
|
||||
const main = async () => {
|
||||
const ports = await SerialPort.list()
|
||||
|
||||
const matchingPorts = ports.filter((portInfo) => {
|
||||
// Implement a check for your device's vendor+product+serial
|
||||
// (this is more robust than the alternative of just hardcoding a "path" like "/dev/ttyUSB0")
|
||||
return (
|
||||
portInfo.vendorId === '1a86' && portInfo.productId === '7523'
|
||||
// && portInfo.serialNumber === 'DEADBEEF'
|
||||
)
|
||||
})
|
||||
|
||||
if (matchingPorts.length < 1) {
|
||||
console.error(`No smartknob usb serial port found! ${JSON.stringify(ports, undefined, 4)}`)
|
||||
return
|
||||
} else if (matchingPorts.length > 1) {
|
||||
console.error(`Multiple smartknob usb serial ports found: ${JSON.stringify(matchingPorts, undefined, 4)}`)
|
||||
return
|
||||
}
|
||||
|
||||
const portInfo = matchingPorts[0]
|
||||
|
||||
let lastLoggedState: PB.ISmartKnobState | undefined
|
||||
const smartknob = new SmartKnob(portInfo.path, (message: PB.FromSmartKnob) => {
|
||||
if (message.payload === 'log' && message.log) {
|
||||
console.log('LOG', message.log.msg)
|
||||
} else if (message.payload === 'smartknobState' && message.smartknobState) {
|
||||
if (
|
||||
message.smartknobState.currentPosition !== lastLoggedState?.currentPosition ||
|
||||
Math.abs((message.smartknobState.subPositionUnit ?? 0) - (lastLoggedState?.subPositionUnit ?? 0)) > 1
|
||||
) {
|
||||
console.log(
|
||||
`State:\n${JSON.stringify(
|
||||
PB.SmartKnobState.toObject(message.smartknobState as PB.SmartKnobState, {defaults: true}),
|
||||
undefined,
|
||||
4,
|
||||
)}`,
|
||||
)
|
||||
lastLoggedState = message.smartknobState
|
||||
}
|
||||
}
|
||||
})
|
||||
smartknob.sendConfig(
|
||||
PB.SmartKnobConfig.create({
|
||||
detentStrengthUnit: 1,
|
||||
endstopStrengthUnit: 1,
|
||||
numPositions: 5,
|
||||
position: 0,
|
||||
positionWidthRadians: (10 * Math.PI) / 180,
|
||||
snapPoint: 1.1,
|
||||
text: 'From TS!',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
105
software/js/packages/example/tsconfig.json
Normal file
105
software/js/packages/example/tsconfig.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "paths" :{
|
||||
// "*": ["./src/typings/*", "./*"]
|
||||
// },
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": ["src/typings"], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
"noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnussedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
201
software/js/packages/smartknobjs-proto/package-lock.json
generated
Normal file
201
software/js/packages/smartknobjs-proto/package-lock.json
generated
Normal file
@ -0,0 +1,201 @@
|
||||
{
|
||||
"name": "smartknobjs-proto",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "smartknobjs-proto",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "6.11.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
|
||||
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "bin/pbjs",
|
||||
"pbts": "bin/pbts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"protobufjs": {
|
||||
"version": "6.11.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
|
||||
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
software/js/packages/smartknobjs-proto/package.json
Normal file
15
software/js/packages/smartknobjs-proto/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "smartknobjs-proto",
|
||||
"version": "0.1.0",
|
||||
"description": "SmartKnob Protobuf Generated Code",
|
||||
"main": "dist/smartknob_proto.js",
|
||||
"types": "dist/smartknob_proto.d.ts",
|
||||
"scripts": {
|
||||
"build": "mkdir -p dist && pbjs --target static-module --out dist/smartknob_proto.js ../../../../proto/*.proto && pbts -o dist/smartknob_proto.d.ts dist/smartknob_proto.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.11.2"
|
||||
}
|
||||
}
|
19
software/js/packages/smartknobjs/.eslintrc
Normal file
19
software/js/packages/smartknobjs/.eslintrc
Normal file
@ -0,0 +1,19 @@
|
||||
// .eslintrc
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
|
||||
},
|
||||
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
10
software/js/packages/smartknobjs/.prettierrc
Normal file
10
software/js/packages/smartknobjs/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
29
software/js/packages/smartknobjs/package.json
Normal file
29
software/js/packages/smartknobjs/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "smartknobjs",
|
||||
"version": "0.1.0",
|
||||
"description": "SmartKnob Interface Library",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"**/*.+(js|ts|json)\"",
|
||||
"lint": "eslint --ext .js,.ts ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cobs": "^0.2.1",
|
||||
"crc-32": "^1.2.0",
|
||||
"serialport": "^9.2.4",
|
||||
"smartknobjs-proto": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/serialport": "^8.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.25.0",
|
||||
"prettier": "^2.4.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
195
software/js/packages/smartknobjs/src/index.ts
Normal file
195
software/js/packages/smartknobjs/src/index.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import SerialPort = require('serialport')
|
||||
import {decode, encode} from 'cobs'
|
||||
import * as CRC32 from 'crc-32'
|
||||
|
||||
import {PB} from 'smartknobjs-proto'
|
||||
|
||||
export type MessageCallback = (message: PB.FromSmartKnob) => void
|
||||
|
||||
type QueueEntry = {
|
||||
nonce: number
|
||||
encodedToSmartknobPayload: Uint8Array
|
||||
}
|
||||
|
||||
const sleep = (millis: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, millis)
|
||||
})
|
||||
}
|
||||
|
||||
export class SmartKnob {
|
||||
private static readonly RETRY_MILLIS = 250
|
||||
private static readonly BAUD = 921600
|
||||
|
||||
private port: SerialPort | null
|
||||
private onMessage: MessageCallback
|
||||
private buffer: Buffer
|
||||
|
||||
private outgoingQueue: QueueEntry[] = []
|
||||
|
||||
private lastNonce = 1
|
||||
private retryTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
private currentConfig: PB.SmartKnobConfig
|
||||
|
||||
constructor(serialPath: string | null, onMessage: MessageCallback) {
|
||||
this.onMessage = onMessage
|
||||
|
||||
this.buffer = Buffer.alloc(0)
|
||||
|
||||
if (serialPath !== null) {
|
||||
this.port = new SerialPort(serialPath, {
|
||||
baudRate: SmartKnob.BAUD,
|
||||
})
|
||||
this.port.on('data', (data: Buffer) => {
|
||||
this.buffer = Buffer.concat([this.buffer, data])
|
||||
this.processBuffer()
|
||||
})
|
||||
} else {
|
||||
this.port = null
|
||||
}
|
||||
|
||||
this.currentConfig = PB.SmartKnobConfig.create({
|
||||
detentStrengthUnit: 1,
|
||||
endstopStrengthUnit: 1,
|
||||
numPositions: 5,
|
||||
position: 0,
|
||||
positionWidthRadians: (10 * Math.PI) / 180,
|
||||
snapPoint: 1.1,
|
||||
text: 'From TS!',
|
||||
})
|
||||
|
||||
this.lastNonce = Math.floor(Math.random() * (2 ^ (32 - 1)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a hard reset of the MCU. Takes a few seconds.
|
||||
*/
|
||||
public async hardReset(): Promise<void> {
|
||||
if (this.port === null) {
|
||||
console.warn("Not connected to SmartKnob, so hard reset isn't possible")
|
||||
return
|
||||
}
|
||||
this.outgoingQueue = []
|
||||
this.port.set({rts: true, dtr: false})
|
||||
await sleep(200)
|
||||
this.port.set({rts: true, dtr: true})
|
||||
await sleep(200)
|
||||
return
|
||||
}
|
||||
|
||||
public sendConfig(config: PB.SmartKnobConfig): void {
|
||||
this.sendMessage(
|
||||
PB.ToSmartknob.create({
|
||||
smartknobConfig: config,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private processBuffer(): void {
|
||||
let i: number
|
||||
// Iterate 0-delimited packets
|
||||
while ((i = this.buffer.indexOf(0)) != -1) {
|
||||
const raw_buffer = this.buffer.slice(0, i)
|
||||
const packet = decode(raw_buffer) as Buffer
|
||||
this.buffer = this.buffer.slice(i + 1)
|
||||
if (packet.length <= 4) {
|
||||
console.debug(`Received short packet ${this.buffer.slice(0, i)}`)
|
||||
continue
|
||||
}
|
||||
const payload = packet.slice(0, packet.length - 4)
|
||||
|
||||
// Validate CRC32
|
||||
const crc_buf = packet.slice(packet.length - 4, packet.length)
|
||||
const provided_crc = crc_buf[0] | (crc_buf[1] << 8) | (crc_buf[2] << 16) | (crc_buf[3] << 24)
|
||||
const crc = CRC32.buf(payload)
|
||||
if (crc !== provided_crc) {
|
||||
console.debug(`Bad CRC. Expected ${crc} but received ${provided_crc}`)
|
||||
console.debug(raw_buffer.toString())
|
||||
continue
|
||||
}
|
||||
|
||||
let message: PB.FromSmartKnob
|
||||
try {
|
||||
message = PB.FromSmartKnob.decode(payload)
|
||||
} catch (err) {
|
||||
console.warn(`Invalid protobuf message ${payload}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (message.payload === 'ack') {
|
||||
const nonce = message.ack?.nonce ?? undefined
|
||||
if (nonce === undefined) {
|
||||
console.warn('Received ack without nonce')
|
||||
} else {
|
||||
this.handleAck(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
this.onMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(message: PB.ToSmartknob) {
|
||||
if (this.port === null) {
|
||||
return
|
||||
}
|
||||
message.nonce = this.lastNonce++
|
||||
|
||||
// Encode before enqueueing to ensure messages don't change once they're queued
|
||||
const payload = PB.ToSmartknob.encode(message).finish()
|
||||
|
||||
if (this.outgoingQueue.length > 10) {
|
||||
console.warn(`SmartKnob outgoing queue overflowed! Dropping ${this.outgoingQueue.length} pending messages!`)
|
||||
this.outgoingQueue.length = 0
|
||||
}
|
||||
this.outgoingQueue.push({
|
||||
nonce: message.nonce,
|
||||
encodedToSmartknobPayload: payload,
|
||||
})
|
||||
this.serviceQueue()
|
||||
}
|
||||
|
||||
private handleAck(nonce: number): void {
|
||||
if (this.outgoingQueue.length > 0 && nonce === this.outgoingQueue[0].nonce) {
|
||||
if (this.retryTimeout !== null) {
|
||||
clearTimeout(this.retryTimeout)
|
||||
this.retryTimeout = null
|
||||
}
|
||||
this.outgoingQueue.shift()
|
||||
this.serviceQueue()
|
||||
} else {
|
||||
console.debug(`Ignoring unexpected ack for nonce ${nonce}`)
|
||||
}
|
||||
}
|
||||
|
||||
private serviceQueue(): void {
|
||||
if (this.port === null) {
|
||||
return
|
||||
}
|
||||
if (this.retryTimeout !== null) {
|
||||
// Retry is pending; let the pending timeout handle the next step
|
||||
return
|
||||
}
|
||||
if (this.outgoingQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const {encodedToSmartknobPayload: payload} = this.outgoingQueue[0]
|
||||
|
||||
const crc = CRC32.buf(payload)
|
||||
const crcBuffer = Buffer.from([crc & 0xff, (crc >>> 8) & 0xff, (crc >>> 16) & 0xff, (crc >>> 24) & 0xff])
|
||||
const packet = Buffer.concat([payload, crcBuffer])
|
||||
|
||||
const encodedDelimitedPacket = Buffer.concat([encode(packet), Buffer.from([0])])
|
||||
|
||||
this.retryTimeout = setTimeout(() => {
|
||||
this.retryTimeout = null
|
||||
console.log(`Retrying ToSmartknob...`)
|
||||
this.serviceQueue()
|
||||
}, SmartKnob.RETRY_MILLIS)
|
||||
|
||||
console.debug(`Sent ${payload.length} byte packet with CRC ${(crc >>> 0).toString(16)}`)
|
||||
this.port.write(encodedDelimitedPacket)
|
||||
}
|
||||
}
|
4
software/js/packages/smartknobjs/src/typings/cobs.d.ts
vendored
Normal file
4
software/js/packages/smartknobjs/src/typings/cobs.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'cobs' {
|
||||
export function decode(buf: Buffer): Buffer
|
||||
export function encode(buf: Buffer, zeroFrame?: boolean): Buffer
|
||||
}
|
105
software/js/packages/smartknobjs/tsconfig.json
Normal file
105
software/js/packages/smartknobjs/tsconfig.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "paths" :{
|
||||
// "*": ["./src/typings/*", "./*"]
|
||||
// },
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": ["src/typings"], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
"noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnussedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
1
thirdparty/nanopb
vendored
Submodule
1
thirdparty/nanopb
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 80f9d5bcbc0da72c95d3411b642c109a14298d75
|
Loading…
Reference in New Issue
Block a user