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:
Scott Bezek 2022-10-22 17:27:35 -07:00 committed by GitHub
parent aec9799e10
commit ae28d523e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 6299 additions and 338 deletions

View File

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

@ -0,0 +1,3 @@
[submodule "thirdparty/nanopb"]
path = thirdparty/nanopb
url = git@github.com:nanopb/nanopb.git

View File

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

@ -0,0 +1,5 @@
{
"files.associations": {
"functional": "cpp"
}
}

View File

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

View File

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

View File

@ -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, &current_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
}

View File

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

View File

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

View File

@ -1,7 +1,8 @@
#pragma once
class Logger {
public:
Logger() {};
virtual ~Logger() {};
virtual void log(const char* msg) = 0;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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_ = {};
};

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

View 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

View 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;
}

View 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;
}

View 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);

View 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_;
};

View 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.");
}

View 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_;
};

View 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);
}

View 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);
};

View 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);
}

View 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;
};

View File

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

View File

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

View File

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

@ -0,0 +1,2 @@
node_modules/
dist

1
software/js/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

21
software/js/README.md Normal file
View 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

File diff suppressed because it is too large Load Diff

24
software/js/package.json Normal file
View 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"
}
}

View 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
}
}

View File

@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "always"
}

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

View 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()

View 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"]
}

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

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

View 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
}
}

View File

@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "always"
}

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

View 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)
}
}

View File

@ -0,0 +1,4 @@
declare module 'cobs' {
export function decode(buf: Buffer): Buffer
export function encode(buf: Buffer, zeroFrame?: boolean): Buffer
}

View 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

@ -0,0 +1 @@
Subproject commit 80f9d5bcbc0da72c95d3411b642c109a14298d75