mirror of
https://github.com/scottbez1/smartknob.git
synced 2025-11-04 17:19:40 +08:00
Compare commits
24 Commits
releases/v
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb988399c | ||
|
|
8700af8a24 | ||
|
|
370b8ca92b | ||
|
|
6d946c49e5 | ||
|
|
dafc5ca1e2 | ||
|
|
406dbb08ec | ||
|
|
2eddc13528 | ||
|
|
590ba8850c | ||
|
|
3cff3533d7 | ||
|
|
1e2b1983c2 | ||
|
|
d1495a8334 | ||
|
|
1d2458b28a | ||
|
|
948297fa82 | ||
|
|
0ebbcc8c23 | ||
|
|
231b75eca9 | ||
|
|
d64cc55432 | ||
|
|
1d73af17cc | ||
|
|
2afc38f7f9 | ||
|
|
08146e292d | ||
|
|
3fdd9fa601 | ||
|
|
9903ac63e6 | ||
|
|
4a1d2bee77 | ||
|
|
acfc85f72b | ||
|
|
9431ed3971 |
10
.github/workflows/electronics.yml
vendored
10
.github/workflows/electronics.yml
vendored
@ -2,7 +2,17 @@ name: Export Electronics
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'electronics/**'
|
||||
- 'util/**'
|
||||
- 'scripts/**'
|
||||
- '.github/workflows/electronics.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'electronics/**'
|
||||
- 'util/**'
|
||||
- 'scripts/**'
|
||||
- '.github/workflows/electronics.yml'
|
||||
|
||||
jobs:
|
||||
export-electronics:
|
||||
|
||||
62
.github/workflows/js.yml
vendored
Normal file
62
.github/workflows/js.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: JS
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'software/js/**'
|
||||
- 'proto/**'
|
||||
- '.github/workflows/js.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'software/js/**'
|
||||
- 'proto/**'
|
||||
- '.github/workflows/js.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
working-directory: software/js
|
||||
run: npm ci
|
||||
- name: Build
|
||||
working-directory: software/js
|
||||
run: PUBLIC_URL="/smartknob" npm run build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: './software/js/packages/example-webserial-timeline/build'
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
if: github.repository == 'scottbez1/smartknob' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
28
.github/workflows/pio.yml
vendored
28
.github/workflows/pio.yml
vendored
@ -2,7 +2,19 @@ name: PlatformIO CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'firmware/**'
|
||||
- 'proto/**'
|
||||
- 'thirdparty/nanopb/**'
|
||||
- 'platformio.ini'
|
||||
- '.github/workflows/pio.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'firmware/**'
|
||||
- 'proto/**'
|
||||
- 'thirdparty/nanopb/**'
|
||||
- 'platformio.ini'
|
||||
- '.github/workflows/pio.yml'
|
||||
|
||||
jobs:
|
||||
pio-build:
|
||||
@ -40,4 +52,18 @@ jobs:
|
||||
if: always() && steps.pio_install.outcome == 'success'
|
||||
run: |
|
||||
pio run \
|
||||
-e view \
|
||||
-e view
|
||||
|
||||
- name: Build Firmware (nanofoc)
|
||||
# Run regardless of other build step failures, as long as setup steps completed
|
||||
if: always() && steps.pio_install.outcome == 'success'
|
||||
run: |
|
||||
pio run \
|
||||
-e nanofoc
|
||||
|
||||
- name: Build Firmware (brushknight_esp32s3)
|
||||
# Run regardless of other build step failures, as long as setup steps completed
|
||||
if: always() && steps.pio_install.outcome == 'success'
|
||||
run: |
|
||||
pio run \
|
||||
-e brushknight_esp32s3
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -9,4 +9,8 @@
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"files.associations": {
|
||||
"cstddef": "cpp",
|
||||
"limits": "cpp"
|
||||
},
|
||||
}
|
||||
109
README.md
109
README.md
@ -12,27 +12,27 @@ Join the [Discord community](https://discord.gg/5jyhjcbTnR) to discuss the proje
|
||||
# Designs
|
||||
|
||||
## SmartKnob View
|
||||
Premium SmartKnob experience. Under active development.
|
||||
The "SmartKnob View" is the premium SmartKnob experience with an integrated display shown in my [demo video](https://www.youtube.com/watch?v=ip641WmY4pA). Under active development.
|
||||
|
||||
🎉 **Motors are [now available](https://www.sparkfun.com/products/20441)!** If you've been following this project,
|
||||
you'll know that the recommended motors went out of stock nearly immediately after it was published.
|
||||
Thanks to [the community](https://github.com/scottbez1/smartknob/issues/16#issuecomment-1094482805%5D), we were able to
|
||||
identify the likely original manufacturer, and recently SparkFun Electronics funded a new production run and are now
|
||||
[selling them](https://www.sparkfun.com/products/20441)! Thanks to everyone who helped search and investigate different
|
||||
identify the likely original manufacturer, and recently SparkFun Electronics has been getting them produced and regularly
|
||||
[stocking them](https://www.sparkfun.com/products/20441)! (However, they've been selling out quickly each time they restock, so definitely sign up for backorder notifications if they're out of stock when you check). Thanks to everyone who helped search and investigate different
|
||||
motor options along the way!
|
||||
|
||||
Features:
|
||||
- 240x240 round LCD ("GC9A01"), protected by 39.5mm watch glass on rotor
|
||||
- BLDC gimbal motor, with a hollow shaft for mechanically & electrically connecting the LCD
|
||||
- Powered by ESP32-PICO-V3-02 (Lilygo TMicro32 Plus module)
|
||||
- PCB flexure and strain gauges used for press detection (haptic feedback provided via the motor)
|
||||
- PCB flexure and SMD resistors used as strain gauges for press detection (haptic feedback provided via the motor)
|
||||
- 8 side-firing RGB LEDs (SK6812-SIDE-A) illuminate ring around the knob
|
||||
- USB-C (2.0) connector for 5V power and serial data/programming (CH340)
|
||||
- VEML7700 ambient light sensor for automatic backlight & LED intensity adjustment
|
||||
- Versatile back plate for mounting - use either 4x screws, or 2x 3M medium Command strips (with cutouts for accessing removal tabs after installation)
|
||||
- Front cover snaps on for easy access to the PCB
|
||||
|
||||
**Current status:** Not recommended for general use (mechanical and electrical revisions may be needed depending on motor/electronics availability)
|
||||
**Current status:** Not recommended for general use, but may be a fun project for an advanced electronics hobbyist.
|
||||
|
||||
### Demo video
|
||||
|
||||
@ -51,6 +51,17 @@ Features:
|
||||
|
||||
Latest Fusion 360 Model: https://a360.co/3BzkU0n
|
||||
|
||||
|
||||
#### DEMO: video editor timeline control
|
||||
|
||||
If you edit audio/video, imagine having a jog wheel where you could _feel_ the clip boundaries as you scrolled through the timeline. Then switch to playback speed control, with a wheel that's spring-loaded to return to "paused," with snaps at 1x, 2x, 4x, etc. The SmartKnob can do this; it just needs some software integrations to be written. Check out a demo using a SmartKnob prototype to control a mock video timeline:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=J9192DfZplk">
|
||||
<img src="https://img.youtube.com/vi/J9192DfZplk/maxresdefault.jpg" width="480" />
|
||||
</a>
|
||||
|
||||
(you can actually try this demo yourself if you've already built a SmartKnob; plug it in over USB and go to https://scottbez1.github.io/smartknob/)
|
||||
|
||||
### Build your own?
|
||||
|
||||
While this is a "DIY" open-source project, it is not yet a mature plug-and-play project. If you intend to build your own, note that it requires advanced soldering experience to build - very small-pitch surface-mount soldering is required (reflow or hot air recommended), and assembly is quite time-consuming and delicate. Please go into it with the expectation that you will almost certainly need to be able to troubleshoot some hardware and firmware issues yourself - I recommend reviewing/understanding the schematics and basic firmware before jumping in!
|
||||
@ -61,25 +72,28 @@ View the latest auto-generated (untested) [Base PCB Interactive BOM](https://sma
|
||||
|
||||
A few miscellaneous notes in the meantime:
|
||||
|
||||
- This can _probably_ be FDM 3D printed with a well-tuned printer, but the parts shown in videos/photos were MJF printed in nylon for tight tolerances and better surface finish
|
||||
- If you wanted a simpler build, you could omit the LCD and just merge the knob + glass from the model into a single STL to get a closed-top knob
|
||||
- There's limited space inside the LCD mount for wiring, and 8 wires need to fit through the hole in the center. I used 30 AWG wire-wrapping wire. Enamel-coated wire would probably work too.
|
||||
- Strain gauges are BF350-3AA, and glued in place with CA glue (I'll include video of this process in the future, but essentially I used kapton tape to pick up the strain gauge and hold it in place during curing). This has to be done after reflow soldering, and would be hard to remove/fix in case of a mistake, so MAKE SURE TO PRACTICE GLUING strain gauges to other items before attempting on the PCB!
|
||||
- Older versions of the design (as seen in the videos) used glued-on BF350-3AA strain gauges to detect presses; in v0.5 these were replaced with footprints for SMD resistors, which exhibit similar properties when stretched/compressed and are much easier to assemble.
|
||||
- The TMC6300 is _tiny_ and has a bottom pad, so I would seriously consider getting a stencil along with the PCB order. Even with the stencil I needed to manually clean up some bridging afterward; I _highly_ recommend Chip Quik NC191 gel flux, available on [Amazon](https://amzn.to/3MGDSr5) (or use this [non-affiliate link](https://www.amazon.com/Smooth-Flow-No-Clean-syringe-plunger/dp/B08KJPG3NZ) instead) or from your electronics distributor of choice. Flux is also very helpful when soldering the LCD ribbon cable to to screen PCB.
|
||||
- For breadboard prototyping, the [TMC6300-BOB](https://www.trinamic.com/support/eval-kits/details/tmc6300-bob/) is awesome and way easier to work with than the bare chip if you just want to play around with low current BLDC motors
|
||||
- For breadboard prototyping, the [TMC6300-BOB](https://www.trinamic.com/support/eval-kits/details/tmc6300-bob/) or SparkFun's [TMC6300 driver board](https://www.sparkfun.com/products/21867) are awesome and way easier to work with than the bare chip if you just want to play around with low current BLDC motors
|
||||
- For AliExpress purchases: I highly recommend **only** using AliExpress Standard Shipping (purchasing in the US). I have had multiple purchases take months or never get delivered when purchased with Cainiao or other low cost shipping options, whereas AliExpress Standard is very reliable and generally faster in my experience.
|
||||
- Make sure to check the [open issues](https://github.com/scottbez1/smartknob/issues) - this design is not yet "stable", so beware that everything may not go smoothly. I would not recommend ordering these parts yourself until the [stable release v1.0 milestone](https://github.com/scottbez1/smartknob/milestone/1) is complete, as there are some mechanical interference issues in the current revision.
|
||||
- Make sure to check the [open issues](https://github.com/scottbez1/smartknob/issues) - this design is not yet "stable", so beware that everything may not go smoothly.
|
||||
|
||||
Future plans:
|
||||
- consider switch to using an ESP32-S3-MINI-1 module (once Arduino core support is complete), as that would allow for direct USB HID support (for joystick/macro-pad type input to a computer)
|
||||
- Bluetooth HID support?
|
||||
- consider switch to using an ESP32-S3-MINI-1 module
|
||||
- get wifi configured and working (probably MQTT?). Currently memory is an issue with the full display framebuffer sprite. PSRAM might fix this (requires newer ESP-IDF & unreleased Arduino core, and from a brief test I got horrible performance with PSRAM enabled), or the next item might help reduce memory:
|
||||
- migrate to LVGL, for better display rendering and easy support for menus, etc. Shouldn't require a full 240x240x24b framebuffer in memory, freeing some for wifi, etc.
|
||||
- integrate nanopb for structured serial data (see [splitflap protobuf protocol](https://github.com/scottbez1/splitflap/blob/1440aba54d5b0d822ec5da68762431879988d7ef/arduino/splitflap/esp32/splitflap/serial_proto_protocol.cpp) for example)
|
||||
- Home Assistant integration, or other real-world applications
|
||||
- ???
|
||||
- [Profit](https://github.com/sponsors/scottbez1/) 😉
|
||||
|
||||
#### Already built one?
|
||||
Check out the [firmware/software documentation](https://paper.dropbox.com/doc/SmartKnob-firmware-and-software--B_oWj~L1dXqHgAqqYmhwwuqzAg-VUb9nq7btuhnHw5KlgJIH)
|
||||
for tips on getting it programmed and hooking it up to your own software integrations.
|
||||
|
||||
If you've already uploaded the standard firmware and gone through the calibration process (see guide linked above if you don't know what this means),
|
||||
go to https://scottbez1.github.io/smartknob/ to try out an interactive web-based demo that uses Web Serial to talk to a SmartKnob that's plugged into your
|
||||
computer's USB port!
|
||||
|
||||
#### Base PCB
|
||||
|
||||
@ -119,7 +133,7 @@ Latest auto-generated (untested and likely broken!) artifacts⚠️:
|
||||
<img src="https://smartknob-artifacts.s3.us-west-1.amazonaws.com/master/electronics/view_screen-back-3d.png" width="300" />
|
||||
</a>
|
||||
|
||||
Ordering notes: Must be 1.2mm thick (not "standard" 1.6mm) per mechanical design.
|
||||
Ordering notes: Must be 1.2mm thick (not "standard" 1.6mm) per mechanical design. (The PCB silkscreen mentions 0.6mm thickness - this is the thickness of the **VHB tape** for adhering the LCD; the PCB should be 1.2mm thick)
|
||||
|
||||
There are few enough components on the Screen PCB that I chose to hand-solder them rather than reflow with solder paste
|
||||
and a stencil, but if you order a stencil, see the note above about selecting a "Customized size" to be easier to
|
||||
@ -138,28 +152,70 @@ Latest auto-generated (untested and likely broken!) artifacts⚠️:
|
||||
|
||||
⚠️ For tested/stable/recommended artifacts, use a [release](https://github.com/scottbez1/smartknob/releases) instead.
|
||||
|
||||
#### Printed parts
|
||||
You will need six printed parts. The current stable designs can be found as STL assets in the [v185 mechanical release](https://github.com/scottbez1/smartknob/releases/tag/releases%2Fmechanical%2Fv185-dummy-tag) or you can export the latest untested designs from the CAD model by opening it in Fusion 360: https://a360.co/3BzkU0n
|
||||
|
||||
## SmartKnob Mini
|
||||
Planned for the future.
|
||||
The parts can _probably_ be FDM 3D printed with a well-tuned printer, but the parts shown in videos/photos were MJF printed in nylon for tight tolerances and better surface finish.
|
||||
|
||||
If you wanted a simpler build, you could omit the LCD and just merge the knob + glass from the model into a single STL to get a closed-top knob.
|
||||
|
||||
Export and print the following 6 parts:
|
||||
* `Enclosure`
|
||||
* `Knob` (RotorAssembly->KnobAssembly->Knob)
|
||||
* `ScreenPlatform`
|
||||
* `RotorSpacer` (RotorAssembly->RotorSpacer)
|
||||
* `MountBase`
|
||||
* `BackPlate`
|
||||
|
||||
⚠️ If you opt to go for MJF Nylon printing from JLCPCB, their system may complain about `< 0.8mm wall thickness detected` on the `MountBase`, `ScreenPlatform` and `RotorSpacer` parts. This appears to be a false positive, possibly on some of the print tapers. You should be able to ignore this, as all print orders at JLCPCB are subject to a final manual review, and they will contact you by email if there are any job-stopping issues or risks with your prints.
|
||||
|
||||
## NanoFOC (3rd party)
|
||||
If you're looking to tinker with FOC/haptic feedback, but don't want to build a full SmartKnob View yourself, I can
|
||||
recommend the NanoFOC DevKit++, an [open-source design](https://github.com/katbinaris/nanofoc_devkit) made and
|
||||
[sold](https://store.binaris.io/products/nanofoc-devkit) by a member of the SmartKnob community! It's super compact
|
||||
and is a great testbed or core for building your own BLDC-based haptic input device.
|
||||
|
||||

|
||||
|
||||
|
||||
The NanoFOC uses an ESP32-S3, and the SmartKnob firmware works on it out of the box; just select the `nanofoc`
|
||||
environment in PlatformIO rather than the `view` environment when uploading.
|
||||
|
||||
# Frequently Asked Questions (FAQ)
|
||||
|
||||
**How much does it cost?**
|
||||
### Can I use this cheap gimbal BLDC motor I found on AliExpress?
|
||||
|
||||
I wish I could tell you now, but I don't actually know off the top of my head. Check back soon - I've only built 1 so far, which was the result of a bunch of tinkering and prototyping over an extended period, so I don't have all the expenses tallied up yet. Certainly less than $200 in parts, and maybe closer to $100?
|
||||
I can't stop you, but I will caution that **you probably won't be super happy with it** depending on your intended application.
|
||||
|
||||
**Does it work with XYZ?**
|
||||
Nearly every off-the-shelf BLDC gimbal motor that the community has tested (which is pretty much ALL the cheap ones you'll find on AliExpress, trust me) has
|
||||
_moderate to severge cogging_. That means the motor wants to snap to certain positions by itself even when unpowered. This means you won't be able to get
|
||||
completely smooth rotation when virtual detents are turned off, and the motor's cogging may interfere with or overpower the virtual detents, particularly if
|
||||
you set them to lower strength or make then very fine-grained.
|
||||
|
||||
Not yet. So far I've only implemented enough firmware for the demo shown in the video, so you can't actually use it for anything productive yet. The basic detent configuration API is there, but not much else. Lots of firmware work remains to be done. If you build one, I'd love your help adding support for XYZ though!
|
||||
The recommended motor is definitely the best (i.e. lowest cogging) off-the-shelf motor we've come across so far, and is the only readily-available motor
|
||||
that I would consider "good" for this application.
|
||||
|
||||
**Can I buy one as a kit or already assembled?**
|
||||
If you've found another motor that is completely smooth when unpowered, we'd LOVE to hear about it in the discord; if you're wondering about some cheap motor
|
||||
you saw online, either just order one and try it for yourself, or ask about it in discord but don't be surprised (or offended) if the response you get is
|
||||
something terse like "nope, no good".
|
||||
|
||||
### How much does it cost?
|
||||
|
||||
I wish I could tell you now, but I don't actually know off the top of my head. Check back soon - I've only built a few so far, which was the result of a bunch of tinkering and prototyping over an extended period, so I don't have all the expenses tallied up yet. Probably less than $200 in parts? But some items have gotten more expensive, and you may be limited by minimum order quantities or shipping charges from multiple separate suppliers.
|
||||
|
||||
### Does it work with XYZ?
|
||||
|
||||
Not yet, regardless of whatever "XYZ" you're thinking of. So far I've only implemented enough firmware for the demo shown in the video, so you can't actually use it for anything productive yet. The basic detent configuration API is there, but not much else. Lots of firmware work remains to be done. If you build one, I'd love your help adding support for "XYZ" though!
|
||||
|
||||
### Can I buy one as a kit or already assembled?
|
||||
|
||||
Probably not? Or at least, I don't have any immediate plans to sell them myself. It's not that I don't want you to be happy, but hardware is a hard business and I just work on this stuff in my free time.
|
||||
|
||||
It's open source with a fairly permissive license though, so in theory anyone could start offering kits/assemblies. If someone does go down that route of selling them, note that attribution is
|
||||
_required_ (and I wouldn't say no to [royalties/tips/thanks](https://github.com/sponsors/scottbez1/) if you're in a giving mood 🙂).
|
||||
|
||||
# Firmware and Software
|
||||
More extensive documentation of the firmware and software (and how to get started) can be found in the dedicated [SmartKnob Firmware and Software Guide](https://paper.dropbox.com/doc/SmartKnob-firmware-and-software--Byho6npe9XvZLZLxJ_8bK5TqAg-VUb9nq7btuhnHw5KlgJIH#:h2=Calibration)
|
||||
|
||||
## General Component Info
|
||||
|
||||
@ -178,12 +234,12 @@ Excellent sensor at a reasonable price - highly recommended. Less noisy than TLV
|
||||
[Ordering (LCSC)](https://lcsc.com/product-detail/Angle-Linear-Position-Sensors_Magn-Tek-MT6701CT-STD_C2856764.html)
|
||||
|
||||
#### TLV493D (Infineon)
|
||||
A mediocre choice. Easy to prototype with using [Adafruit's QWIIC breakout board](https://www.adafruit.com/product/4366).
|
||||
Not used in the SmartKnob view, but a common/popular magnetic encoder in general. It's a mediocre choice for a haptic feedback implementation. Easy to prototype with using [Adafruit's QWIIC breakout board](https://www.adafruit.com/product/4366).
|
||||
|
||||
In my testing, it is a little noisy, requiring filtering/smoothing that can slow responsiveness, hurting control loop stability. Or, with less filtering, the noise
|
||||
In my testing, it's noisy, requiring filtering/smoothing that can slow responsiveness, hurting control loop stability. Or, with less filtering, the noise
|
||||
can easily be "amplified" by the derivative component in the PID motor torque controller, causing audible (and tactile) humming/buzzing.
|
||||
|
||||
There is also apparently a known silicon issue that causes the internal ADC to sometimes completely lock up, requiring a full reset and re-configuration. See section
|
||||
But the bigger issue is that there is apparently a known silicon issue that causes the internal ADC to sometimes completely lock up, requiring a full reset and re-configuration, which can cause delays/gaps in data! See section
|
||||
5.6 in the [User Manual](https://www.infineon.com/dgdl/Infineon-TLV493D-A1B6_3DMagnetic-UM-v01_03-EN.pdf?fileId=5546d46261d5e6820161e75721903ddd)
|
||||
|
||||
In the Master Controlled Mode (MCM) or the Fast Mode (FM) the ADC conversion may hang up. A hang up can
|
||||
@ -232,11 +288,6 @@ This is overall the easiest motor to get started with. Low cogging and a built-i
|
||||
|
||||
Available [from SparkFun](https://www.sparkfun.com/products/20441)!
|
||||
|
||||
# Firmware
|
||||
TODO: document this
|
||||
|
||||
Also TODO: implement a lot more of the firmware
|
||||
|
||||
# Acknowledgements
|
||||
This project was greatly inspired by Jesse Schoch's video "[haptic textures and virtual detents](https://www.youtube.com/watch?v=1gPQfDkX3BU)" and the
|
||||
corresponding [discussion in the SimpleFOC community](https://community.simplefoc.com/t/haptic-textures/301). Seriously, this project wouldn't exist if not for that video - thank you Jesse!
|
||||
|
||||
@ -2122,7 +2122,6 @@
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(junction (at 200.66 250.19) (diameter 0) (color 0 0 0 0)
|
||||
(uuid 00e38d63-5436-49db-81f5-697421f168fc)
|
||||
)
|
||||
@ -5608,7 +5607,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 158.75 184.15 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 158.75 184.15 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 158.75 184.15 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 158.75 184.15 0)
|
||||
@ -5778,7 +5777,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 22.86 33.02 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 22.86 33.02 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 22.86 33.02 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 22.86 33.02 0)
|
||||
@ -5806,7 +5805,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 36.83 33.02 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 36.83 33.02 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 36.83 33.02 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 36.83 33.02 0)
|
||||
@ -5866,7 +5865,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 172.72 165.1 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 172.72 165.1 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 172.72 165.1 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 172.72 165.1 0)
|
||||
@ -7422,7 +7421,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 317.5 30.48 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 317.5 30.48 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 317.5 30.48 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 317.5 30.48 0)
|
||||
@ -7610,7 +7609,7 @@
|
||||
(property "Datasheet" "~" (id 3) (at 224.79 73.66 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "LCSC" "C2922480" (id 4) (at 224.79 73.66 0)
|
||||
(property "LCSC" "C15850" (id 4) (at 224.79 73.66 0)
|
||||
(effects (font (size 1.27 1.27)) hide)
|
||||
)
|
||||
(property "Digikey" "1276-2891-1-ND" (id 5) (at 224.79 73.66 0)
|
||||
|
||||
11
firmware/partitions-4MB-fat.csv
Normal file
11
firmware/partitions-4MB-fat.csv
Normal file
@ -0,0 +1,11 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table, 0x8000, 4K
|
||||
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, 0, ota_0, 0x10000, 1408K,
|
||||
ota_1, 0, ota_1, 0x170000, 1408K,
|
||||
uf2, app, factory,0x2d0000, 256K,
|
||||
ffat, data, fat, 0x310000, 960K,
|
||||
|
140
firmware/src/configuration.cpp
Normal file
140
firmware/src/configuration.cpp
Normal file
@ -0,0 +1,140 @@
|
||||
#include <FFat.h>
|
||||
|
||||
#include "pb_decode.h"
|
||||
#include "pb_encode.h"
|
||||
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
#include "semaphore_guard.h"
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
static const char* CONFIG_PATH = "/config.pb";
|
||||
|
||||
Configuration::Configuration() {
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
assert(mutex_ != NULL);
|
||||
}
|
||||
|
||||
Configuration::~Configuration() {
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
bool Configuration::loadFromDisk() {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
FatGuard fatGuard(logger_);
|
||||
if (!fatGuard.mounted_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File f = FFat.open(CONFIG_PATH);
|
||||
if (!f) {
|
||||
log("Failed to read config file");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t read = f.readBytes((char*)buffer_, sizeof(buffer_));
|
||||
f.close();
|
||||
|
||||
pb_istream_t stream = pb_istream_from_buffer(buffer_, read);
|
||||
if (!pb_decode(&stream, PB_PersistentConfiguration_fields, &pb_buffer_)) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Decoding failed: %s", PB_GET_ERROR(&stream));
|
||||
log(buf);
|
||||
pb_buffer_ = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pb_buffer_.version != PERSISTENT_CONFIGURATION_VERSION) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Invalid config version. Expected %u, received %u", PERSISTENT_CONFIGURATION_VERSION, pb_buffer_.version);
|
||||
log(buf);
|
||||
pb_buffer_ = {};
|
||||
return false;
|
||||
}
|
||||
loaded_ = true;
|
||||
|
||||
char buf[200];
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"Motor calibration: calib=%u, pole_pairs=%u, zero_offset=%.2f, cw=%u",
|
||||
pb_buffer_.motor.calibrated,
|
||||
pb_buffer_.motor.pole_pairs,
|
||||
pb_buffer_.motor.zero_electrical_offset,
|
||||
pb_buffer_.motor.direction_cw
|
||||
);
|
||||
log(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Configuration::saveToDisk() {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer_, sizeof(buffer_));
|
||||
pb_buffer_.version = PERSISTENT_CONFIGURATION_VERSION;
|
||||
if (!pb_encode(&stream, PB_PersistentConfiguration_fields, &pb_buffer_)) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Encoding failed: %s", PB_GET_ERROR(&stream));
|
||||
log(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
FatGuard fatGuard(logger_);
|
||||
if (!fatGuard.mounted_) {
|
||||
return false;
|
||||
}
|
||||
File f = FFat.open(CONFIG_PATH, FILE_WRITE);
|
||||
if (!f) {
|
||||
log("Failed to read config file");
|
||||
return false;
|
||||
}
|
||||
size_t written = f.write(buffer_, stream.bytes_written);
|
||||
f.close();
|
||||
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Wrote %d bytes", written);
|
||||
log(buf);
|
||||
|
||||
if (written != stream.bytes_written) {
|
||||
log("Failed to write all bytes to file");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PB_PersistentConfiguration Configuration::get() {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
if (!loaded_) {
|
||||
return PB_PersistentConfiguration();
|
||||
}
|
||||
return pb_buffer_;
|
||||
}
|
||||
|
||||
bool Configuration::setMotorCalibrationAndSave(PB_MotorCalibration& motor_calibration) {
|
||||
{
|
||||
SemaphoreGuard lock(mutex_);
|
||||
pb_buffer_.motor = motor_calibration;
|
||||
pb_buffer_.has_motor = true;
|
||||
}
|
||||
return saveToDisk();
|
||||
}
|
||||
|
||||
bool Configuration::setStrainCalibrationAndSave(PB_StrainCalibration& strain_calibration) {
|
||||
{
|
||||
SemaphoreGuard lock(mutex_);
|
||||
pb_buffer_.strain = strain_calibration;
|
||||
pb_buffer_.has_strain = true;
|
||||
}
|
||||
return saveToDisk();
|
||||
}
|
||||
|
||||
void Configuration::setLogger(Logger* logger) {
|
||||
logger_ = logger;
|
||||
}
|
||||
|
||||
void Configuration::log(const char* msg) {
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log(msg);
|
||||
}
|
||||
}
|
||||
64
firmware/src/configuration.h
Normal file
64
firmware/src/configuration.h
Normal file
@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <FFat.h>
|
||||
#include <PacketSerial.h>
|
||||
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
const uint32_t PERSISTENT_CONFIGURATION_VERSION = 1;
|
||||
|
||||
class Configuration {
|
||||
public:
|
||||
Configuration();
|
||||
~Configuration();
|
||||
|
||||
void setLogger(Logger* logger);
|
||||
bool loadFromDisk();
|
||||
bool saveToDisk();
|
||||
PB_PersistentConfiguration get();
|
||||
bool setMotorCalibrationAndSave(PB_MotorCalibration& motor_calibration);
|
||||
bool setStrainCalibrationAndSave(PB_StrainCalibration& strain_calibration);
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t mutex_;
|
||||
|
||||
Logger* logger_ = nullptr;
|
||||
bool loaded_ = false;
|
||||
PB_PersistentConfiguration pb_buffer_ = {};
|
||||
|
||||
uint8_t buffer_[PB_PersistentConfiguration_size];
|
||||
|
||||
void log(const char* msg);
|
||||
};
|
||||
class FatGuard {
|
||||
public:
|
||||
FatGuard(Logger* logger) : logger_(logger) {
|
||||
if (!FFat.begin(true)) {
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log("Failed to mount FFat");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log("Mounted FFat");
|
||||
}
|
||||
mounted_ = true;
|
||||
}
|
||||
~FatGuard() {
|
||||
if (mounted_) {
|
||||
FFat.end();
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log("Unmounted FFat");
|
||||
}
|
||||
}
|
||||
}
|
||||
FatGuard(FatGuard const&)=delete;
|
||||
FatGuard& operator=(FatGuard const&)=delete;
|
||||
|
||||
bool mounted_ = false;
|
||||
|
||||
private:
|
||||
Logger* logger_;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
#if SK_DISPLAY
|
||||
#include "display_task.h"
|
||||
#include "semaphore_guard.h"
|
||||
#include "util.h"
|
||||
|
||||
#include "font/roboto_light_60.h"
|
||||
|
||||
@ -19,15 +20,24 @@ DisplayTask::~DisplayTask() {
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
static void drawPlayButton(TFT_eSprite& spr, int x, int y, int width, int height, uint16_t color) {
|
||||
spr.fillTriangle(
|
||||
x, y - height / 2,
|
||||
x, y + height / 2,
|
||||
x + width, y,
|
||||
color
|
||||
);
|
||||
}
|
||||
|
||||
void DisplayTask::run() {
|
||||
tft_.begin();
|
||||
tft_.invertDisplay(1);
|
||||
tft_.setRotation(0);
|
||||
tft_.setRotation(SK_DISPLAY_ROTATION);
|
||||
tft_.fillScreen(TFT_DARKGREEN);
|
||||
|
||||
ledcSetup(LEDC_CHANNEL_LCD_BACKLIGHT, 5000, 16);
|
||||
ledcSetup(LEDC_CHANNEL_LCD_BACKLIGHT, 5000, SK_BACKLIGHT_BIT_DEPTH);
|
||||
ledcAttachPin(PIN_LCD_BACKLIGHT, LEDC_CHANNEL_LCD_BACKLIGHT);
|
||||
ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, UINT16_MAX);
|
||||
ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, (1 << SK_BACKLIGHT_BIT_DEPTH) - 1);
|
||||
|
||||
spr_.setColorDepth(8);
|
||||
|
||||
@ -54,71 +64,125 @@ void DisplayTask::run() {
|
||||
}
|
||||
|
||||
spr_.fillSprite(TFT_BLACK);
|
||||
if (state.config.num_positions > 1) {
|
||||
int32_t height = state.current_position * TFT_HEIGHT / (state.config.num_positions - 1);
|
||||
spr_.fillRect(0, TFT_HEIGHT - height, TFT_WIDTH, height, FILL_COLOR);
|
||||
}
|
||||
|
||||
spr_.setFreeFont(&Roboto_Light_60);
|
||||
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.text;
|
||||
char* end = start + strlen(state.config.text);
|
||||
while (start < end) {
|
||||
char* newline = strchr(start, '\n');
|
||||
if (newline == nullptr) {
|
||||
newline = end;
|
||||
}
|
||||
|
||||
char buf[sizeof(state.config.text)] = {};
|
||||
strncat(buf, start, min(sizeof(buf) - 1, (size_t)(newline - start)));
|
||||
spr_.drawString(String(buf), TFT_WIDTH / 2, line_y, 1);
|
||||
start = newline + 1;
|
||||
line_y += spr_.fontHeight(1);
|
||||
}
|
||||
|
||||
float left_bound = PI / 2;
|
||||
|
||||
if (state.config.num_positions > 0) {
|
||||
float range_radians = (state.config.num_positions - 1) * state.config.position_width_radians;
|
||||
left_bound = PI / 2 + range_radians / 2;
|
||||
float right_bound = PI / 2 - range_radians / 2;
|
||||
spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(left_bound), TFT_HEIGHT/2 - RADIUS * sinf(left_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound), TFT_WHITE);
|
||||
spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(right_bound), TFT_HEIGHT/2 - RADIUS * sinf(right_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound), TFT_WHITE);
|
||||
}
|
||||
if (DRAW_ARC) {
|
||||
spr_.drawCircle(TFT_WIDTH/2, TFT_HEIGHT/2, RADIUS, TFT_DARKGREY);
|
||||
}
|
||||
|
||||
int32_t num_positions = state.config.max_position - state.config.min_position + 1;
|
||||
float adjusted_sub_position = state.sub_position_unit * state.config.position_width_radians;
|
||||
if (state.config.num_positions > 0) {
|
||||
if (state.current_position == 0 && state.sub_position_unit < 0) {
|
||||
if (num_positions > 0) {
|
||||
if (state.current_position == state.config.min_position && state.sub_position_unit < 0) {
|
||||
adjusted_sub_position = -logf(1 - state.sub_position_unit * state.config.position_width_radians / 5 / PI * 180) * 5 * PI / 180;
|
||||
} else if (state.current_position == state.config.num_positions - 1 && state.sub_position_unit > 0) {
|
||||
} else if (state.current_position == state.config.max_position && state.sub_position_unit > 0) {
|
||||
adjusted_sub_position = logf(1 + state.sub_position_unit * state.config.position_width_radians / 5 / PI * 180) * 5 * PI / 180;
|
||||
}
|
||||
}
|
||||
|
||||
float raw_angle = left_bound - state.current_position * state.config.position_width_radians;
|
||||
float left_bound = PI / 2;
|
||||
float right_bound = 0;
|
||||
if (num_positions > 0) {
|
||||
float range_radians = (state.config.max_position - state.config.min_position) * state.config.position_width_radians;
|
||||
left_bound = PI / 2 + range_radians / 2;
|
||||
right_bound = PI / 2 - range_radians / 2;
|
||||
}
|
||||
float raw_angle = left_bound - (state.current_position - state.config.min_position) * state.config.position_width_radians;
|
||||
float adjusted_angle = raw_angle - adjusted_sub_position;
|
||||
|
||||
bool sk_demo_mode = strncmp(state.config.text, "SKDEMO_", 7) == 0;
|
||||
|
||||
if (state.config.num_positions > 0 && ((state.current_position == 0 && state.sub_position_unit < 0) || (state.current_position == state.config.num_positions - 1 && state.sub_position_unit > 0))) {
|
||||
if (!sk_demo_mode) {
|
||||
if (num_positions > 1) {
|
||||
int32_t height = (state.current_position - state.config.min_position) * TFT_HEIGHT / (state.config.max_position - state.config.min_position);
|
||||
spr_.fillRect(0, TFT_HEIGHT - height, TFT_WIDTH, height, FILL_COLOR);
|
||||
}
|
||||
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(raw_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(raw_angle), 5, DOT_COLOR);
|
||||
if (raw_angle < adjusted_angle) {
|
||||
for (float r = raw_angle; r <= adjusted_angle; r += 2 * PI / 180) {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
|
||||
spr_.setFreeFont(&Roboto_Light_60);
|
||||
spr_.drawNumber(state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2 - VALUE_OFFSET, 1);
|
||||
spr_.setFreeFont(&DESCRIPTION_FONT);
|
||||
int32_t line_y = TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET;
|
||||
char* start = state.config.text;
|
||||
char* end = start + strlen(state.config.text);
|
||||
while (start < end) {
|
||||
char* newline = strchr(start, '\n');
|
||||
if (newline == nullptr) {
|
||||
newline = end;
|
||||
}
|
||||
|
||||
char buf[sizeof(state.config.text)] = {};
|
||||
strncat(buf, start, min(sizeof(buf) - 1, (size_t)(newline - start)));
|
||||
spr_.drawString(String(buf), TFT_WIDTH / 2, line_y, 1);
|
||||
start = newline + 1;
|
||||
line_y += spr_.fontHeight(1);
|
||||
}
|
||||
|
||||
if (num_positions > 0) {
|
||||
spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(left_bound), TFT_HEIGHT/2 - RADIUS * sinf(left_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound), TFT_WHITE);
|
||||
spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(right_bound), TFT_HEIGHT/2 - RADIUS * sinf(right_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound), TFT_WHITE);
|
||||
}
|
||||
if (DRAW_ARC) {
|
||||
spr_.drawCircle(TFT_WIDTH/2, TFT_HEIGHT/2, RADIUS, TFT_DARKGREY);
|
||||
}
|
||||
|
||||
if (num_positions > 0 && ((state.current_position == state.config.min_position && state.sub_position_unit < 0) || (state.current_position == state.config.max_position && state.sub_position_unit > 0))) {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(raw_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(raw_angle), 5, DOT_COLOR);
|
||||
if (raw_angle < adjusted_angle) {
|
||||
for (float r = raw_angle; r <= adjusted_angle; r += 2 * PI / 180) {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
|
||||
}
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
|
||||
} else {
|
||||
for (float r = raw_angle; r >= adjusted_angle; r -= 2 * PI / 180) {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
|
||||
}
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
|
||||
}
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
|
||||
} else {
|
||||
for (float r = raw_angle; r >= adjusted_angle; r -= 2 * PI / 180) {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR);
|
||||
}
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR);
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 5, DOT_COLOR);
|
||||
}
|
||||
} else {
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 5, DOT_COLOR);
|
||||
if (strncmp(state.config.text, "SKDEMO_Scroll", 13) == 0) {
|
||||
spr_.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, spr_.color565(150, 0, 0));
|
||||
spr_.setFreeFont(&Roboto_Thin_24);
|
||||
spr_.drawString("Scroll", TFT_WIDTH / 2, TFT_HEIGHT / 2, 1);
|
||||
bool detent = false;
|
||||
for (uint8_t i = 0; i < state.config.detent_positions_count; i++) {
|
||||
if (state.config.detent_positions[i] == state.current_position) {
|
||||
detent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 16) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 16) * sinf(adjusted_angle), detent ? 8 : 5, TFT_WHITE);
|
||||
} else if (strncmp(state.config.text, "SKDEMO_Frames", 13) == 0) {
|
||||
int32_t width = (state.current_position - state.config.min_position) * TFT_WIDTH / (state.config.max_position - state.config.min_position);
|
||||
spr_.fillRect(0, 0, width, TFT_HEIGHT, spr_.color565(0, 150, 0));
|
||||
spr_.setFreeFont(&Roboto_Light_60);
|
||||
spr_.drawNumber(state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2, 1);
|
||||
spr_.setFreeFont(&Roboto_Thin_24);
|
||||
spr_.drawString("Frame", TFT_WIDTH / 2, TFT_HEIGHT / 2 - DESCRIPTION_Y_OFFSET - VALUE_OFFSET, 1);
|
||||
} else if (strncmp(state.config.text, "SKDEMO_Speed", 12) == 0) {
|
||||
spr_.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, spr_.color565(0, 0, 150));
|
||||
|
||||
float normalizedFractional = sgn(state.sub_position_unit) *
|
||||
CLAMP(lerp(state.sub_position_unit * sgn(state.sub_position_unit), 0.1, 0.9, 0, 1), (float)0, (float)1);
|
||||
float normalized = state.current_position + normalizedFractional;
|
||||
float speed = sgn(normalized) * powf(2, fabsf(normalized) - 1);
|
||||
float roundedSpeed = truncf(speed * 10) / 10;
|
||||
|
||||
spr_.setFreeFont(&Roboto_Thin_24);
|
||||
if (roundedSpeed == 0) {
|
||||
spr_.drawString("Paused", TFT_WIDTH / 2, TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET + VALUE_OFFSET, 1);
|
||||
|
||||
spr_.fillRect(TFT_WIDTH / 2 + 5, TFT_HEIGHT / 2 - 20, 10, 40, TFT_WHITE);
|
||||
spr_.fillRect(TFT_WIDTH / 2 - 5 - 10, TFT_HEIGHT / 2 - 20, 10, 40, TFT_WHITE);
|
||||
} else {
|
||||
char buf[10];
|
||||
snprintf(buf, sizeof(buf), "%0.1fx", roundedSpeed);
|
||||
spr_.drawString(buf, TFT_WIDTH / 2, TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET + VALUE_OFFSET, 1);
|
||||
|
||||
uint16_t x = TFT_WIDTH / 2;
|
||||
for (uint8_t i = 0; i < max(1, abs(state.current_position)); i++) {
|
||||
drawPlayButton(spr_, x, TFT_HEIGHT / 2, sgn(roundedSpeed) * 20, 40, TFT_WHITE);
|
||||
x += sgn(roundedSpeed) * 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spr_.pushSprite(0, 0);
|
||||
@ -127,7 +191,7 @@ void DisplayTask::run() {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, brightness_);
|
||||
}
|
||||
delay(2);
|
||||
delay(5);
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +201,7 @@ QueueHandle_t DisplayTask::getKnobStateQueue() {
|
||||
|
||||
void DisplayTask::setBrightness(uint16_t brightness) {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
brightness_ = brightness;
|
||||
brightness_ = brightness >> (16 - SK_BACKLIGHT_BIT_DEPTH);
|
||||
}
|
||||
|
||||
void DisplayTask::setLogger(Logger* logger) {
|
||||
|
||||
8
firmware/src/interface_callbacks.h
Normal file
8
firmware/src/interface_callbacks.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
|
||||
typedef std::function<void(PB_SmartKnobConfig&)> ConfigCallback;
|
||||
typedef std::function<void(void)> MotorCalibrationCallback;
|
||||
@ -11,10 +11,9 @@
|
||||
#endif
|
||||
|
||||
#include "interface_task.h"
|
||||
#include "semaphore_guard.h"
|
||||
#include "util.h"
|
||||
|
||||
#define COUNT_OF(A) (sizeof(A) / sizeof(A[0]))
|
||||
|
||||
#if SK_LEDS
|
||||
CRGB leds[NUM_LEDS];
|
||||
#endif
|
||||
@ -28,113 +27,227 @@ Adafruit_VEML7700 veml = Adafruit_VEML7700();
|
||||
#endif
|
||||
|
||||
static PB_SmartKnobConfig configs[] = {
|
||||
// int32_t num_positions;
|
||||
// int32_t position;
|
||||
// float sub_position_unit;
|
||||
// uint8_t position_nonce;
|
||||
// int32_t min_position;
|
||||
// int32_t max_position;
|
||||
// float position_width_radians;
|
||||
// float detent_strength_unit;
|
||||
// float endstop_strength_unit;
|
||||
// float snap_point;
|
||||
// char text[51];
|
||||
// pb_size_t detent_positions_count;
|
||||
// int32_t detent_positions[5];
|
||||
// float snap_point_bias;
|
||||
// int8_t led_hue;
|
||||
|
||||
{
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-1, // max position < min position indicates no bounds
|
||||
10 * PI / 180,
|
||||
0,
|
||||
1,
|
||||
1.1,
|
||||
"Unbounded\nNo detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
200,
|
||||
},
|
||||
{
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
10,
|
||||
10 * PI / 180,
|
||||
0,
|
||||
1,
|
||||
1.1,
|
||||
"Bounded 0-10\nNo detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
73,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
72,
|
||||
10 * PI / 180,
|
||||
0,
|
||||
1,
|
||||
1.1,
|
||||
"Multi-rev\nNo detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
73,
|
||||
},
|
||||
{
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
60 * PI / 180,
|
||||
1,
|
||||
1,
|
||||
0.55, // Note the snap point is slightly past the midpoint (0.5); compare to normal detents which use a snap point *past* the next value (i.e. > 1)
|
||||
"On/off\nStrong detent",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
157,
|
||||
},
|
||||
{
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
60 * PI / 180,
|
||||
0.01,
|
||||
0.6,
|
||||
1.1,
|
||||
"Return-to-center",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
45,
|
||||
},
|
||||
{
|
||||
256,
|
||||
127,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
255,
|
||||
1 * PI / 180,
|
||||
0,
|
||||
1,
|
||||
1.1,
|
||||
"Fine values\nNo detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
219,
|
||||
},
|
||||
{
|
||||
256,
|
||||
127,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
255,
|
||||
1 * PI / 180,
|
||||
1,
|
||||
1,
|
||||
1.1,
|
||||
"Fine values\nWith detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
25,
|
||||
},
|
||||
{
|
||||
32,
|
||||
0,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
31,
|
||||
8.225806452 * PI / 180,
|
||||
2,
|
||||
1,
|
||||
1.1,
|
||||
"Coarse values\nStrong detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
200,
|
||||
},
|
||||
{
|
||||
32,
|
||||
0,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
31,
|
||||
8.225806452 * PI / 180,
|
||||
0.2,
|
||||
1,
|
||||
1.1,
|
||||
"Coarse values\nWeak detents",
|
||||
0,
|
||||
{},
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
31,
|
||||
7 * PI / 180,
|
||||
2.5,
|
||||
1,
|
||||
0.7,
|
||||
"Magnetic detents",
|
||||
4,
|
||||
{2, 10, 21, 22},
|
||||
0,
|
||||
73,
|
||||
},
|
||||
{
|
||||
0,
|
||||
0,
|
||||
8,
|
||||
-6,
|
||||
6,
|
||||
60 * PI / 180,
|
||||
1,
|
||||
1,
|
||||
0.55,
|
||||
"Return-to-center\nwith detents",
|
||||
0,
|
||||
{},
|
||||
0.4,
|
||||
157,
|
||||
},
|
||||
};
|
||||
|
||||
InterfaceTask::InterfaceTask(const uint8_t task_core, MotorTask& motor_task, DisplayTask* display_task) :
|
||||
Task("Interface", 3000, 1, task_core),
|
||||
Task("Interface", 3400, 1, task_core),
|
||||
stream_(),
|
||||
motor_task_(motor_task),
|
||||
display_task_(display_task),
|
||||
plaintext_protocol_(stream_, motor_task_),
|
||||
proto_protocol_(stream_, motor_task_) {
|
||||
plaintext_protocol_(stream_, [this] () {
|
||||
motor_task_.runCalibration();
|
||||
}),
|
||||
proto_protocol_(stream_, [this] (PB_SmartKnobConfig& config) {
|
||||
applyConfig(config, true);
|
||||
}) {
|
||||
#if SK_DISPLAY
|
||||
assert(display_task != nullptr);
|
||||
#endif
|
||||
|
||||
|
||||
log_queue_ = xQueueCreate(10, sizeof(std::string *));
|
||||
assert(log_queue_ != NULL);
|
||||
|
||||
knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState));
|
||||
assert(knob_state_queue_ != NULL);
|
||||
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
assert(mutex_ != NULL);
|
||||
}
|
||||
|
||||
InterfaceTask::~InterfaceTask() {
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
void InterfaceTask::run() {
|
||||
@ -149,7 +262,7 @@ void InterfaceTask::run() {
|
||||
Wire.setClock(400000);
|
||||
#endif
|
||||
#if SK_STRAIN
|
||||
scale.begin(38, 2);
|
||||
scale.begin(PIN_STRAIN_DO, PIN_STRAIN_SCK);
|
||||
#endif
|
||||
|
||||
#if SK_ALS
|
||||
@ -161,23 +274,49 @@ void InterfaceTask::run() {
|
||||
}
|
||||
#endif
|
||||
|
||||
motor_task_.setConfig(configs[0]);
|
||||
applyConfig(configs[0], false);
|
||||
motor_task_.addListener(knob_state_queue_);
|
||||
|
||||
|
||||
// Start in legacy protocol mode
|
||||
plaintext_protocol_.init([this] () {
|
||||
changeConfig(true);
|
||||
}, [this] () {
|
||||
if (!configuration_loaded_) {
|
||||
return;
|
||||
}
|
||||
if (strain_calibration_step_ == 0) {
|
||||
log("Strain calibration step 1: Don't touch the knob, then press 'S' again");
|
||||
strain_calibration_step_ = 1;
|
||||
} else if (strain_calibration_step_ == 1) {
|
||||
configuration_value_.strain.idle_value = strain_reading_;
|
||||
snprintf(buf_, sizeof(buf_), " idle_value=%d", configuration_value_.strain.idle_value);
|
||||
log(buf_);
|
||||
log("Strain calibration step 2: Push and hold down the knob with medium pressure, and press 'S' again");
|
||||
strain_calibration_step_ = 2;
|
||||
} else if (strain_calibration_step_ == 2) {
|
||||
configuration_value_.strain.press_delta = strain_reading_ - configuration_value_.strain.idle_value;
|
||||
configuration_value_.has_strain = true;
|
||||
snprintf(buf_, sizeof(buf_), " press_delta=%d", configuration_value_.strain.press_delta);
|
||||
log(buf_);
|
||||
log("Strain calibration complete! Saving...");
|
||||
strain_calibration_step_ = 0;
|
||||
if (configuration_->setStrainCalibrationAndSave(configuration_value_.strain)) {
|
||||
log(" Saved!");
|
||||
} else {
|
||||
log(" FAILED to save config!!!");
|
||||
}
|
||||
}
|
||||
});
|
||||
SerialProtocol* current_protocol = &plaintext_protocol_;
|
||||
|
||||
ProtocolChangeCallback protocol_change_callback = [this, ¤t_protocol] (uint8_t protocol) {
|
||||
// Start in legacy protocol mode
|
||||
current_protocol_ = &plaintext_protocol_;
|
||||
|
||||
ProtocolChangeCallback protocol_change_callback = [this] (uint8_t protocol) {
|
||||
switch (protocol) {
|
||||
case SERIAL_PROTOCOL_LEGACY:
|
||||
current_protocol = &plaintext_protocol_;
|
||||
current_protocol_ = &plaintext_protocol_;
|
||||
break;
|
||||
case SERIAL_PROTOCOL_PROTO:
|
||||
current_protocol = &proto_protocol_;
|
||||
current_protocol_ = &proto_protocol_;
|
||||
break;
|
||||
default:
|
||||
log("Unknown protocol requested");
|
||||
@ -190,21 +329,28 @@ void InterfaceTask::run() {
|
||||
|
||||
// Interface loop:
|
||||
while (1) {
|
||||
PB_SmartKnobState state;
|
||||
if (xQueueReceive(knob_state_queue_, &state, 0) == pdTRUE) {
|
||||
current_protocol->handleState(state);
|
||||
if (xQueueReceive(knob_state_queue_, &latest_state_, 0) == pdTRUE) {
|
||||
publishState();
|
||||
}
|
||||
|
||||
current_protocol->loop();
|
||||
current_protocol_->loop();
|
||||
|
||||
std::string* log_string;
|
||||
while (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) {
|
||||
current_protocol->log(log_string->c_str());
|
||||
current_protocol_->log(log_string->c_str());
|
||||
delete log_string;
|
||||
}
|
||||
|
||||
updateHardware();
|
||||
|
||||
if (!configuration_loaded_) {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
if (configuration_ != nullptr) {
|
||||
configuration_value_ = configuration_->get();
|
||||
configuration_loaded_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
@ -228,10 +374,9 @@ void InterfaceTask::changeConfig(bool next) {
|
||||
}
|
||||
}
|
||||
|
||||
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_]);
|
||||
applyConfig(configs[current_config_], false);
|
||||
}
|
||||
|
||||
void InterfaceTask::updateHardware() {
|
||||
@ -244,40 +389,51 @@ void InterfaceTask::updateHardware() {
|
||||
float lux = veml.readLux();
|
||||
lux_avg = lux * LUX_ALPHA + lux_avg * (1 - LUX_ALPHA);
|
||||
static uint32_t last_als;
|
||||
if (millis() - last_als > 1000) {
|
||||
if (millis() - last_als > 1000 && strain_calibration_step_ == 0) {
|
||||
snprintf(buf_, sizeof(buf_), "millilux: %.2f", lux*1000);
|
||||
log(buf_);
|
||||
last_als = millis();
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool pressed;
|
||||
#if SK_STRAIN
|
||||
if (scale.wait_ready_timeout(100)) {
|
||||
int32_t reading = scale.read();
|
||||
strain_reading_ = scale.read();
|
||||
|
||||
static uint32_t last_reading_display;
|
||||
if (millis() - last_reading_display > 1000) {
|
||||
snprintf(buf_, sizeof(buf_), "HX711 reading: %d", reading);
|
||||
if (millis() - last_reading_display > 1000 && strain_calibration_step_ == 0) {
|
||||
snprintf(buf_, sizeof(buf_), "HX711 reading: %d", strain_reading_);
|
||||
log(buf_);
|
||||
last_reading_display = millis();
|
||||
}
|
||||
if (configuration_loaded_ && configuration_value_.has_strain && strain_calibration_step_ == 0) {
|
||||
// TODO: calibrate and track (long term moving average) idle point (lower)
|
||||
press_value_unit = lerp(strain_reading_, configuration_value_.strain.idle_value, configuration_value_.strain.idle_value + configuration_value_.strain.press_delta, 0, 1);
|
||||
|
||||
// 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;
|
||||
// Ignore readings that are way out of expected bounds
|
||||
if (-1 < press_value_unit && press_value_unit < 2) {
|
||||
static uint8_t press_readings;
|
||||
if (!pressed && press_value_unit > 1) {
|
||||
press_readings++;
|
||||
if (press_readings > 2) {
|
||||
motor_task_.playHaptic(true);
|
||||
pressed = true;
|
||||
press_count_++;
|
||||
publishState();
|
||||
if (!remote_controlled_) {
|
||||
changeConfig(true);
|
||||
}
|
||||
}
|
||||
} else if (pressed && press_value_unit < 0.5) {
|
||||
press_readings++;
|
||||
if (press_readings > 2) {
|
||||
motor_task_.playHaptic(false);
|
||||
pressed = false;
|
||||
}
|
||||
} else {
|
||||
press_readings = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -304,7 +460,7 @@ void InterfaceTask::updateHardware() {
|
||||
|
||||
#if SK_LEDS
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
leds[i].setHSV(200 * press_value_unit, 255, brightness >> 8);
|
||||
leds[i].setHSV(latest_config_.led_hue, 255 - 180*CLAMP(press_value_unit, (float)0, (float)1) - 75*pressed, brightness >> 8);
|
||||
|
||||
// Gamma adjustment
|
||||
leds[i].r = dim8_video(leds[i].r);
|
||||
@ -314,3 +470,20 @@ void InterfaceTask::updateHardware() {
|
||||
FastLED.show();
|
||||
#endif
|
||||
}
|
||||
|
||||
void InterfaceTask::setConfiguration(Configuration* configuration) {
|
||||
SemaphoreGuard lock(mutex_);
|
||||
configuration_ = configuration;
|
||||
}
|
||||
|
||||
void InterfaceTask::publishState() {
|
||||
// Apply local state before publishing to serial
|
||||
latest_state_.press_nonce = press_count_;
|
||||
current_protocol_->handleState(latest_state_);
|
||||
}
|
||||
|
||||
void InterfaceTask::applyConfig(PB_SmartKnobConfig& config, bool from_remote) {
|
||||
remote_controlled_ = from_remote;
|
||||
latest_config_ = config;
|
||||
motor_task_.setConfig(config);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <AceButton.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "configuration.h"
|
||||
#include "display_task.h"
|
||||
#include "logger.h"
|
||||
#include "motor_task.h"
|
||||
@ -11,25 +12,49 @@
|
||||
#include "serial/uart_stream.h"
|
||||
#include "task.h"
|
||||
|
||||
#ifndef SK_FORCE_UART_STREAM
|
||||
#define SK_FORCE_UART_STREAM 0
|
||||
#endif
|
||||
|
||||
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);
|
||||
virtual ~InterfaceTask() {};
|
||||
virtual ~InterfaceTask();
|
||||
|
||||
void log(const char* msg) override;
|
||||
void setConfiguration(Configuration* configuration);
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) && !SK_FORCE_UART_STREAM
|
||||
HWCDC stream_;
|
||||
#else
|
||||
UartStream stream_;
|
||||
#endif
|
||||
MotorTask& motor_task_;
|
||||
DisplayTask* display_task_;
|
||||
char buf_[64];
|
||||
char buf_[128];
|
||||
|
||||
SemaphoreHandle_t mutex_;
|
||||
Configuration* configuration_ = nullptr; // protected by mutex_
|
||||
|
||||
PB_PersistentConfiguration configuration_value_;
|
||||
bool configuration_loaded_ = false;
|
||||
|
||||
uint8_t strain_calibration_step_ = 0;
|
||||
int32_t strain_reading_ = 0;
|
||||
|
||||
SerialProtocol* current_protocol_ = nullptr;
|
||||
bool remote_controlled_ = false;
|
||||
int current_config_ = 0;
|
||||
uint8_t press_count_ = 1;
|
||||
|
||||
PB_SmartKnobState latest_state_ = {};
|
||||
PB_SmartKnobConfig latest_config_ = {};
|
||||
|
||||
QueueHandle_t log_queue_;
|
||||
QueueHandle_t knob_state_queue_;
|
||||
@ -38,4 +63,6 @@ class InterfaceTask : public Task<InterfaceTask>, public Logger {
|
||||
|
||||
void changeConfig(bool next);
|
||||
void updateHardware();
|
||||
void publishState();
|
||||
void applyConfig(PB_SmartKnobConfig& config, bool from_remote);
|
||||
};
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "configuration.h"
|
||||
#include "display_task.h"
|
||||
#include "interface_task.h"
|
||||
#include "motor_task.h"
|
||||
|
||||
Configuration config;
|
||||
|
||||
#if SK_DISPLAY
|
||||
static DisplayTask display_task(0);
|
||||
static DisplayTask* display_task_p = &display_task;
|
||||
#else
|
||||
static DisplayTask* display_task_p = nullptr;
|
||||
#endif
|
||||
static MotorTask motor_task(1);
|
||||
static MotorTask motor_task(1, config);
|
||||
|
||||
|
||||
InterfaceTask interface_task(0, motor_task, display_task_p);
|
||||
@ -24,9 +27,15 @@ void setup() {
|
||||
motor_task.addListener(display_task.getKnobStateQueue());
|
||||
#endif
|
||||
|
||||
interface_task.begin();
|
||||
|
||||
config.setLogger(&interface_task);
|
||||
config.loadFromDisk();
|
||||
|
||||
interface_task.setConfiguration(&config);
|
||||
|
||||
motor_task.setLogger(&interface_task);
|
||||
motor_task.begin();
|
||||
interface_task.begin();
|
||||
|
||||
// Free up the Arduino loop task
|
||||
vTaskDelete(NULL);
|
||||
|
||||
13
firmware/src/maq430_sensor.h
Normal file
13
firmware/src/maq430_sensor.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <sensors/MagneticSensorSPI.h>
|
||||
|
||||
/** Configured fro 12bit MA710 and MAQ430 magnetic sensor over SPI interface*/
|
||||
MagneticSensorSPIConfig_s MAQ430_SPI = {
|
||||
.spi_mode = SPI_MODE3,
|
||||
.clock_speed = 1000000,
|
||||
.bit_resolution = 12,
|
||||
.angle_register = 0x0000,
|
||||
.data_start_bit = 15,
|
||||
.command_rw_bit = 0, // not required
|
||||
.command_parity_bit = 17 // parity not implemented
|
||||
};
|
||||
@ -3,22 +3,15 @@
|
||||
#include "motor_task.h"
|
||||
#if SENSOR_MT6701
|
||||
#include "mt6701_sensor.h"
|
||||
#endif
|
||||
#if SENSOR_TLV
|
||||
#elif SENSOR_TLV
|
||||
#include "tlv_sensor.h"
|
||||
#elif SENSOR_MAQ430
|
||||
#include "maq430_sensor.h"
|
||||
#endif
|
||||
|
||||
#include "motors/motor_config.h"
|
||||
#include "util.h"
|
||||
|
||||
|
||||
// ####
|
||||
// Hardware-specific motor calibration constants.
|
||||
// Run calibration once at startup, then update these constants with the calibration results.
|
||||
static const float ZERO_ELECTRICAL_OFFSET = 2.77;
|
||||
static const Direction FOC_DIRECTION = Direction::CW;
|
||||
static const int MOTOR_POLE_PAIRS = 7;
|
||||
// ####
|
||||
|
||||
|
||||
static const float DEAD_ZONE_DETENT_PERCENT = 0.2;
|
||||
static const float DEAD_ZONE_RAD = 1 * _PI / 180;
|
||||
|
||||
@ -29,7 +22,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", 2500, 1, task_core) {
|
||||
MotorTask::MotorTask(const uint8_t task_core, Configuration& configuration) : Task("Motor", 4000, 1, task_core), configuration_(configuration) {
|
||||
queue_ = xQueueCreate(5, sizeof(Command));
|
||||
assert(queue_ != NULL);
|
||||
}
|
||||
@ -41,6 +34,8 @@ MotorTask::~MotorTask() {}
|
||||
TlvSensor encoder = TlvSensor();
|
||||
#elif SENSOR_MT6701
|
||||
MT6701Sensor encoder = MT6701Sensor();
|
||||
#elif SENSOR_MAQ430
|
||||
MagneticSensorSPI encoder = MagneticSensorSPI(MAQ430_SPI, PIN_MAQ_SS);
|
||||
#endif
|
||||
|
||||
void MotorTask::run() {
|
||||
@ -50,34 +45,41 @@ void MotorTask::run() {
|
||||
|
||||
#if SENSOR_TLV
|
||||
encoder.init(&Wire, false);
|
||||
#endif
|
||||
|
||||
#if SENSOR_MT6701
|
||||
#elif SENSOR_MT6701
|
||||
encoder.init();
|
||||
#elif SENSOR_MAQ430
|
||||
SPIClass* spi = new SPIClass(HSPI);
|
||||
spi->begin(PIN_MAQ_SCK, PIN_MAQ_MISO, PIN_MAQ_MOSI, PIN_MAQ_SS);
|
||||
encoder.init(spi);
|
||||
#endif
|
||||
|
||||
motor.linkDriver(&driver);
|
||||
|
||||
motor.controller = MotionControlType::torque;
|
||||
motor.voltage_limit = 5;
|
||||
motor.voltage_limit = FOC_VOLTAGE_LIMIT;
|
||||
motor.velocity_limit = 10000;
|
||||
motor.linkSensor(&encoder);
|
||||
|
||||
// Not actually using the velocity loop built into SimpleFOC; but I'm using those PID variables
|
||||
// to run PID for torque (and SimpleFOC studio supports updating them easily over serial for tuning)
|
||||
motor.PID_velocity.P = 4;
|
||||
motor.PID_velocity.I = 0;
|
||||
motor.PID_velocity.D = 0.04;
|
||||
motor.PID_velocity.output_ramp = 10000;
|
||||
motor.PID_velocity.limit = 10;
|
||||
motor.PID_velocity.P = FOC_PID_P;
|
||||
motor.PID_velocity.I = FOC_PID_I;
|
||||
motor.PID_velocity.D = FOC_PID_D;
|
||||
motor.PID_velocity.output_ramp = FOC_PID_OUTPUT_RAMP;
|
||||
motor.PID_velocity.limit = FOC_PID_LIMIT;
|
||||
|
||||
#ifdef FOC_LPF
|
||||
motor.LPF_angle.Tf = FOC_LPF;
|
||||
#endif
|
||||
|
||||
motor.init();
|
||||
|
||||
encoder.update();
|
||||
delay(10);
|
||||
|
||||
motor.pole_pairs = MOTOR_POLE_PAIRS;
|
||||
motor.initFOC(ZERO_ELECTRICAL_OFFSET, FOC_DIRECTION);
|
||||
PB_PersistentConfiguration c = configuration_.get();
|
||||
motor.pole_pairs = c.motor.calibrated ? c.motor.pole_pairs : 7;
|
||||
motor.initFOC(c.motor.zero_electrical_offset, c.motor.direction_cw ? Direction::CW : Direction::CCW);
|
||||
|
||||
motor.monitor_downsample = 0; // disable monitor at first - optional
|
||||
|
||||
@ -85,18 +87,21 @@ void MotorTask::run() {
|
||||
|
||||
float current_detent_center = motor.shaft_angle;
|
||||
PB_SmartKnobConfig config = {
|
||||
.num_positions = 2,
|
||||
.position = 0,
|
||||
.sub_position_unit = 0,
|
||||
.position_nonce = 0,
|
||||
.min_position = 0,
|
||||
.max_position = 1,
|
||||
.position_width_radians = 60 * _PI / 180,
|
||||
.detent_strength_unit = 0,
|
||||
};
|
||||
int32_t current_position = 0;
|
||||
float latest_sub_position_unit = 0;
|
||||
|
||||
float idle_check_velocity_ewma = 0;
|
||||
uint32_t last_idle_start = 0;
|
||||
uint32_t last_publish = 0;
|
||||
|
||||
PB_SmartKnobConfig latest_config = config;
|
||||
|
||||
while (1) {
|
||||
motor.loopFOC();
|
||||
|
||||
@ -108,14 +113,62 @@ void MotorTask::run() {
|
||||
calibrate();
|
||||
break;
|
||||
case CommandType::CONFIG: {
|
||||
// Check new config for validity
|
||||
PB_SmartKnobConfig& new_config = command.data.config;
|
||||
if (new_config.detent_strength_unit < 0) {
|
||||
log("Ignoring invalid config: detent_strength_unit cannot be negative");
|
||||
break;
|
||||
}
|
||||
if (new_config.endstop_strength_unit < 0) {
|
||||
log("Ignoring invalid config: endstop_strength_unit cannot be negative");
|
||||
break;
|
||||
}
|
||||
if (new_config.snap_point < 0.5) {
|
||||
log("Ignoring invalid config: snap_point must be >= 0.5 for stability");
|
||||
break;
|
||||
}
|
||||
if (new_config.detent_positions_count > COUNT_OF(new_config.detent_positions)) {
|
||||
log("Ignoring invalid config: detent_positions_count is too large");
|
||||
break;
|
||||
}
|
||||
if (new_config.snap_point_bias < 0) {
|
||||
log("Ignoring invalid config: snap_point_bias cannot be negative or there is risk of instability");
|
||||
break;
|
||||
}
|
||||
|
||||
// Change haptic input mode
|
||||
config = command.data.config;
|
||||
latest_config = config;
|
||||
bool position_updated = false;
|
||||
if (new_config.position != config.position
|
||||
|| new_config.sub_position_unit != config.sub_position_unit
|
||||
|| new_config.position_nonce != config.position_nonce) {
|
||||
log("applying position change");
|
||||
current_position = new_config.position;
|
||||
position_updated = true;
|
||||
}
|
||||
|
||||
if (new_config.min_position <= new_config.max_position) {
|
||||
// Only check bounds if min/max indicate bounds are active (min >= max)
|
||||
if (current_position < new_config.min_position) {
|
||||
current_position = new_config.min_position;
|
||||
log("adjusting position to min");
|
||||
} else if (current_position > new_config.max_position) {
|
||||
current_position = new_config.max_position;
|
||||
log("adjusting position to max");
|
||||
}
|
||||
}
|
||||
|
||||
if (position_updated || new_config.position_width_radians != config.position_width_radians) {
|
||||
log("adjusting detent center");
|
||||
float new_sub_position = position_updated ? new_config.sub_position_unit : latest_sub_position_unit;
|
||||
#if SK_INVERT_ROTATION
|
||||
float shaft_angle = -motor.shaft_angle;
|
||||
#else
|
||||
float shaft_angle = motor.shaft_angle;
|
||||
#endif
|
||||
current_detent_center = shaft_angle + new_sub_position * new_config.position_width_radians;
|
||||
}
|
||||
config = new_config;
|
||||
log("Got new config");
|
||||
current_detent_center = motor.shaft_angle;
|
||||
#if SK_INVERT_ROTATION
|
||||
current_detent_center = -motor.shaft_angle;
|
||||
#endif
|
||||
|
||||
// Update derivative factor of torque controller based on detent width.
|
||||
// If the D factor is large on coarse detents, the motor ends up making noise because the P&D factors amplify the noise from the sensor.
|
||||
@ -130,7 +183,9 @@ void MotorTask::run() {
|
||||
const float derivative_position_width_lower = radians(3);
|
||||
const float derivative_position_width_upper = radians(8);
|
||||
const float raw = derivative_lower_strength + (derivative_upper_strength - derivative_lower_strength)/(derivative_position_width_upper - derivative_position_width_lower)*(config.position_width_radians - derivative_position_width_lower);
|
||||
motor.PID_velocity.D = CLAMP(
|
||||
// When there are intermittent detents (set via detent_positions), disable derivative factor as this adds extra "clicks" when nearing
|
||||
// a detent.
|
||||
motor.PID_velocity.D = config.detent_positions_count > 0 ? 0 : CLAMP(
|
||||
raw,
|
||||
min(derivative_lower_strength, derivative_upper_strength),
|
||||
max(derivative_lower_strength, derivative_upper_strength)
|
||||
@ -175,22 +230,31 @@ void MotorTask::run() {
|
||||
#if SK_INVERT_ROTATION
|
||||
angle_to_detent_center = -motor.shaft_angle - current_detent_center;
|
||||
#endif
|
||||
if (angle_to_detent_center > config.position_width_radians * config.snap_point && (config.num_positions <= 0 || config.position > 0)) {
|
||||
|
||||
float snap_point_radians = config.position_width_radians * config.snap_point;
|
||||
float bias_radians = config.position_width_radians * config.snap_point_bias;
|
||||
float snap_point_radians_decrease = snap_point_radians + (current_position <= 0 ? bias_radians : -bias_radians);
|
||||
float snap_point_radians_increase = -snap_point_radians + (current_position >= 0 ? -bias_radians : bias_radians);
|
||||
|
||||
int32_t num_positions = config.max_position - config.min_position + 1;
|
||||
if (angle_to_detent_center > snap_point_radians_decrease && (num_positions <= 0 || current_position > config.min_position)) {
|
||||
current_detent_center += config.position_width_radians;
|
||||
angle_to_detent_center -= config.position_width_radians;
|
||||
config.position--;
|
||||
} else if (angle_to_detent_center < -config.position_width_radians * config.snap_point && (config.num_positions <= 0 || config.position < config.num_positions - 1)) {
|
||||
current_position--;
|
||||
} else if (angle_to_detent_center < snap_point_radians_increase && (num_positions <= 0 || current_position < config.max_position)) {
|
||||
current_detent_center -= config.position_width_radians;
|
||||
angle_to_detent_center += config.position_width_radians;
|
||||
config.position++;
|
||||
current_position++;
|
||||
}
|
||||
|
||||
latest_sub_position_unit = -angle_to_detent_center / config.position_width_radians;
|
||||
|
||||
float dead_zone_adjustment = CLAMP(
|
||||
angle_to_detent_center,
|
||||
fmaxf(-config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, -DEAD_ZONE_RAD),
|
||||
fminf(config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, DEAD_ZONE_RAD));
|
||||
|
||||
bool out_of_bounds = config.num_positions > 0 && ((angle_to_detent_center > 0 && config.position == 0) || (angle_to_detent_center < 0 && config.position == config.num_positions - 1));
|
||||
bool out_of_bounds = num_positions > 0 && ((angle_to_detent_center > 0 && current_position == config.min_position) || (angle_to_detent_center < 0 && current_position == config.max_position));
|
||||
motor.PID_velocity.limit = 10; //out_of_bounds ? 10 : 3;
|
||||
motor.PID_velocity.P = out_of_bounds ? config.endstop_strength_unit * 4 : config.detent_strength_unit * 4;
|
||||
|
||||
@ -200,7 +264,20 @@ void MotorTask::run() {
|
||||
// Don't apply torque if velocity is too high (helps avoid positive feedback loop/runaway)
|
||||
motor.move(0);
|
||||
} else {
|
||||
float torque = motor.PID_velocity(-angle_to_detent_center + dead_zone_adjustment);
|
||||
float input = -angle_to_detent_center + dead_zone_adjustment;
|
||||
if (!out_of_bounds && config.detent_positions_count > 0) {
|
||||
bool in_detent = false;
|
||||
for (uint8_t i = 0; i < config.detent_positions_count; i++) {
|
||||
if (config.detent_positions[i] == current_position) {
|
||||
in_detent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!in_detent) {
|
||||
input = 0;
|
||||
}
|
||||
}
|
||||
float torque = motor.PID_velocity(input);
|
||||
#if SK_INVERT_ROTATION
|
||||
torque = -torque;
|
||||
#endif
|
||||
@ -210,16 +287,14 @@ void MotorTask::run() {
|
||||
// Publish current status to other registered tasks periodically
|
||||
if (millis() - last_publish > 5) {
|
||||
publish({
|
||||
.current_position = config.position,
|
||||
.sub_position_unit = -angle_to_detent_center / config.position_width_radians,
|
||||
.current_position = current_position,
|
||||
.sub_position_unit = latest_sub_position_unit,
|
||||
.has_config = true,
|
||||
.config = config,
|
||||
});
|
||||
last_publish = millis();
|
||||
}
|
||||
|
||||
motor.monitor();
|
||||
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
@ -275,6 +350,7 @@ void MotorTask::calibrate() {
|
||||
// TODO: dig into SimpleFOC calibration and find/fix the issue
|
||||
|
||||
log("\n\n\nStarting calibration, please DO NOT TOUCH MOTOR until complete!");
|
||||
delay(1000);
|
||||
|
||||
motor.controller = MotionControlType::angle_openloop;
|
||||
motor.pole_pairs = 1;
|
||||
@ -282,6 +358,9 @@ void MotorTask::calibrate() {
|
||||
|
||||
float a = 0;
|
||||
|
||||
motor.voltage_limit = FOC_VOLTAGE_LIMIT;
|
||||
motor.move(a);
|
||||
|
||||
// #### Determine direction motor rotates relative to angle sensor
|
||||
for (uint8_t i = 0; i < 200; i++) {
|
||||
encoder.update();
|
||||
@ -308,7 +387,12 @@ void MotorTask::calibrate() {
|
||||
|
||||
log("");
|
||||
|
||||
// TODO: check for no motor movement!
|
||||
float movement_angle = fabsf(end_sensor - start_sensor);
|
||||
if (movement_angle < radians(30) || movement_angle > radians(180)) {
|
||||
snprintf(buf_, sizeof(buf_), "ERROR! Unexpected sensor change: start=%.2f end=%.2f", start_sensor, end_sensor);
|
||||
log(buf_);
|
||||
return;
|
||||
}
|
||||
|
||||
log("Sensor measures positive for positive motor rotation:");
|
||||
if (end_sensor > start_sensor) {
|
||||
@ -318,6 +402,8 @@ void MotorTask::calibrate() {
|
||||
log("NO, Direction=CCW");
|
||||
motor.initFOC(0, Direction::CCW);
|
||||
}
|
||||
snprintf(buf_, sizeof(buf_), " (start was %.1f, end was %.1f)", start_sensor, end_sensor);
|
||||
log(buf_);
|
||||
|
||||
|
||||
// #### Determine pole-pairs
|
||||
@ -325,7 +411,7 @@ void MotorTask::calibrate() {
|
||||
uint8_t electrical_revolutions = 20;
|
||||
snprintf(buf_, sizeof(buf_), "Going to measure %d electrical revolutions...", electrical_revolutions);
|
||||
log(buf_);
|
||||
motor.voltage_limit = 5;
|
||||
motor.voltage_limit = FOC_VOLTAGE_LIMIT;
|
||||
motor.move(a);
|
||||
log("Going to electrical zero...");
|
||||
float destination = a + _2PI;
|
||||
@ -366,6 +452,12 @@ void MotorTask::calibrate() {
|
||||
snprintf(buf_, sizeof(buf_), "Electrical angle / mechanical angle (i.e. pole pairs) = %.2f", electrical_per_mechanical);
|
||||
log(buf_);
|
||||
|
||||
if (electrical_per_mechanical < 3 || electrical_per_mechanical > 12) {
|
||||
snprintf(buf_, sizeof(buf_), "ERROR! Unexpected calculated pole pairs: %.2f", electrical_per_mechanical);
|
||||
log(buf_);
|
||||
return;
|
||||
}
|
||||
|
||||
int measured_pole_pairs = (int)round(electrical_per_mechanical);
|
||||
snprintf(buf_, sizeof(buf_), "Pole pairs set to %d", measured_pole_pairs);
|
||||
log(buf_);
|
||||
@ -375,7 +467,7 @@ void MotorTask::calibrate() {
|
||||
|
||||
// #### Determine mechanical offset to electrical zero
|
||||
// Measure mechanical angle at every electrical zero for several revolutions
|
||||
motor.voltage_limit = 5;
|
||||
motor.voltage_limit = FOC_VOLTAGE_LIMIT;
|
||||
motor.move(a);
|
||||
float offset_x = 0;
|
||||
float offset_y = 0;
|
||||
@ -422,13 +514,13 @@ void MotorTask::calibrate() {
|
||||
|
||||
|
||||
// #### Apply settings
|
||||
// TODO: save to non-volatile storage
|
||||
motor.pole_pairs = measured_pole_pairs;
|
||||
motor.zero_electric_angle = avg_offset_angle + _3PI_2;
|
||||
motor.voltage_limit = 5;
|
||||
motor.voltage_limit = FOC_VOLTAGE_LIMIT;
|
||||
motor.controller = MotionControlType::torque;
|
||||
|
||||
log("\n\nRESULTS:\n Update these constants at the top of " __FILE__);
|
||||
log("");
|
||||
log("RESULTS:");
|
||||
snprintf(buf_, sizeof(buf_), " ZERO_ELECTRICAL_OFFSET: %.2f", motor.zero_electric_angle);
|
||||
log(buf_);
|
||||
if (motor.sensor_direction == Direction::CW) {
|
||||
@ -438,7 +530,18 @@ void MotorTask::calibrate() {
|
||||
}
|
||||
snprintf(buf_, sizeof(buf_), " MOTOR_POLE_PAIRS: %d", motor.pole_pairs);
|
||||
log(buf_);
|
||||
delay(2000);
|
||||
|
||||
log("");
|
||||
log("Saving to persistent configuration...");
|
||||
PB_MotorCalibration calibration = {
|
||||
.calibrated = true,
|
||||
.zero_electrical_offset = motor.zero_electric_angle,
|
||||
.direction_cw = motor.sensor_direction == Direction::CW,
|
||||
.pole_pairs = motor.pole_pairs,
|
||||
};
|
||||
if (configuration_.setMotorCalibrationAndSave(calibration)) {
|
||||
log("Success!");
|
||||
}
|
||||
}
|
||||
|
||||
void MotorTask::checkSensorError() {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <SimpleFOC.h>
|
||||
#include <vector>
|
||||
|
||||
#include "configuration.h"
|
||||
#include "logger.h"
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
#include "task.h"
|
||||
@ -33,7 +34,7 @@ class MotorTask : public Task<MotorTask> {
|
||||
friend class Task<MotorTask>; // Allow base Task to invoke protected run()
|
||||
|
||||
public:
|
||||
MotorTask(const uint8_t task_core);
|
||||
MotorTask(const uint8_t task_core, Configuration& configuration);
|
||||
~MotorTask();
|
||||
|
||||
void setConfig(const PB_SmartKnobConfig& config);
|
||||
@ -47,6 +48,7 @@ class MotorTask : public Task<MotorTask> {
|
||||
void run();
|
||||
|
||||
private:
|
||||
Configuration& configuration_;
|
||||
QueueHandle_t queue_;
|
||||
Logger* logger_;
|
||||
std::vector<QueueHandle_t> listeners_;
|
||||
|
||||
12
firmware/src/motors/mad2804.h
Normal file
12
firmware/src/motors/mad2804.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Tuning parameters for the MAD2804 motor (orange stator).
|
||||
|
||||
#define FOC_PID_P 1
|
||||
#define FOC_PID_I 0
|
||||
#define FOC_PID_D 0.148
|
||||
#define FOC_PID_OUTPUT_RAMP 5000
|
||||
#define FOC_PID_LIMIT 3
|
||||
|
||||
#define FOC_VOLTAGE_LIMIT 3
|
||||
#define FOC_LPF 0.0075
|
||||
9
firmware/src/motors/motor_config.h
Normal file
9
firmware/src/motors/motor_config.h
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#if MOTOR_WANZHIDA_ONCE_TOP
|
||||
#include "motors/wanzhida_once_top.h"
|
||||
#elif MOTOR_MAD2804
|
||||
#include "motors/mad2804.h"
|
||||
#else
|
||||
#error "No motor configuration specified!"
|
||||
#endif
|
||||
12
firmware/src/motors/wanzhida_once_top.h
Normal file
12
firmware/src/motors/wanzhida_once_top.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Tuning parameters for the Wanzhida/Oncetop OT-EM3215D2450Y1R,
|
||||
// sold by SparkFun (ROB-20441).
|
||||
|
||||
#define FOC_PID_P 4
|
||||
#define FOC_PID_I 0
|
||||
#define FOC_PID_D 0.04
|
||||
#define FOC_PID_OUTPUT_RAMP 10000
|
||||
#define FOC_PID_LIMIT 10
|
||||
|
||||
#define FOC_VOLTAGE_LIMIT 5
|
||||
@ -51,7 +51,13 @@ void MT6701Sensor::init() {
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 1000,
|
||||
};
|
||||
esp_err_t ret = spi_bus_initialize(HSPI_HOST, &tx_bus_config, 1);
|
||||
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
esp_err_t ret = spi_bus_initialize(SPI3_HOST, &tx_bus_config, SPI_DMA_CH_AUTO);
|
||||
#else
|
||||
esp_err_t ret = spi_bus_initialize(HSPI_HOST, &tx_bus_config, 1);
|
||||
#endif
|
||||
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
spi_device_interface_config_t tx_device_config = {
|
||||
@ -70,7 +76,12 @@ void MT6701Sensor::init() {
|
||||
.pre_cb=NULL,
|
||||
.post_cb=NULL,
|
||||
};
|
||||
ret=spi_bus_add_device(HSPI_HOST, &tx_device_config, &spi_device_);
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
ret=spi_bus_add_device(SPI3_HOST, &tx_device_config, &spi_device_);
|
||||
#else
|
||||
ret=spi_bus_add_device(HSPI_HOST, &tx_device_config, &spi_device_);
|
||||
#endif
|
||||
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* Automatically generated nanopb constant definitions */
|
||||
/* Generated by nanopb-0.4.7-dev */
|
||||
/* Generated by nanopb-0.4.7 */
|
||||
|
||||
#include "smartknob.pb.h"
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
@ -9,6 +9,9 @@
|
||||
PB_BIND(PB_FromSmartKnob, PB_FromSmartKnob, 2)
|
||||
|
||||
|
||||
PB_BIND(PB_ToSmartknob, PB_ToSmartknob, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_Ack, PB_Ack, AUTO)
|
||||
|
||||
|
||||
@ -18,13 +21,19 @@ 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)
|
||||
|
||||
|
||||
PB_BIND(PB_PersistentConfiguration, PB_PersistentConfiguration, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_MotorCalibration, PB_MotorCalibration, AUTO)
|
||||
|
||||
|
||||
PB_BIND(PB_StrainCalibration, PB_StrainCalibration, AUTO)
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* Automatically generated nanopb header */
|
||||
/* Generated by nanopb-0.4.7-dev */
|
||||
/* Generated by nanopb-0.4.7 */
|
||||
|
||||
#ifndef PB_PB_SMARTKNOB_PB_H_INCLUDED
|
||||
#define PB_PB_SMARTKNOB_PB_H_INCLUDED
|
||||
@ -10,10 +10,7 @@
|
||||
#endif
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _PB_RequestState {
|
||||
char dummy_field;
|
||||
} PB_RequestState;
|
||||
|
||||
/* * Lets the host know that a ToSmartknob message was received and should not be retried. */
|
||||
typedef struct _PB_Ack {
|
||||
uint32_t nonce;
|
||||
} PB_Ack;
|
||||
@ -23,34 +20,142 @@ typedef struct _PB_Log {
|
||||
} PB_Log;
|
||||
|
||||
typedef struct _PB_SmartKnobConfig {
|
||||
int32_t num_positions;
|
||||
/* *
|
||||
Set the integer position.
|
||||
|
||||
Note: in order to make SmartKnobConfig apply idempotently, the current position
|
||||
will only be set to this value when it changes compared to a previous config (and
|
||||
NOT compared to the current state!). So by default, if you send a config position
|
||||
of 5 and the current position is 3, the position may remain at 3 if the config
|
||||
change to 5 was previously handled. If you need to force a position update, see
|
||||
position_nonce. */
|
||||
int32_t position;
|
||||
/* *
|
||||
Set the fractional position. Typical range: (-snap_point, snap_point).
|
||||
|
||||
Actual range is technically unbounded, but in practice this value will be compared
|
||||
against snap_point on the next control loop, so any value beyond the snap_point will
|
||||
generally result in an integer position change (unless position is already at a
|
||||
limit).
|
||||
|
||||
Note: idempotency implications noted in the documentation for `position` apply here
|
||||
as well */
|
||||
float sub_position_unit;
|
||||
/* *
|
||||
Position is normally only applied when it changes, but sometimes it's desirable
|
||||
to reset the position to the same value, so a nonce change can be used to force
|
||||
the position values to be applied as well.
|
||||
|
||||
NOTE: Must be < 256 */
|
||||
uint8_t position_nonce;
|
||||
/* * Minimum position allowed. */
|
||||
int32_t min_position;
|
||||
/* *
|
||||
Maximum position allowed.
|
||||
|
||||
If this is the same as min_position, there will only be one allowed position.
|
||||
|
||||
If this is less than min_position, bounds will be disabled. */
|
||||
int32_t max_position;
|
||||
/* * The angular "width" of each position/detent, in radians. */
|
||||
float position_width_radians;
|
||||
/* *
|
||||
Strength of detents to apply. Typical range: [0, 1].
|
||||
|
||||
A value of 0 disables detents.
|
||||
|
||||
Values greater than 1 are not recommended and may lead to unstable behavior. */
|
||||
float detent_strength_unit;
|
||||
/* *
|
||||
Strength of endstop torque to apply at min/max bounds. Typical range: [0, 1].
|
||||
|
||||
A value of 0 disables endstop torque, but does not make position unbounded, meaning
|
||||
the knob will not try to return to the valid region. For unbounded rotation, use
|
||||
min_position and max_position.
|
||||
|
||||
Values greater than 1 are not recommended and may lead to unstable behavior. */
|
||||
float endstop_strength_unit;
|
||||
/* *
|
||||
Fractional (sub-position) threshold where the position will increment/decrement.
|
||||
Typical range: (0.5, 1.5).
|
||||
|
||||
This defines how hysteresis is applied to positions, which is why values > */
|
||||
float snap_point;
|
||||
/* *
|
||||
Arbitrary 50-byte string representing this "config". This can be used to identify major
|
||||
config/mode changes. The value will be echoed back to the host via a future State's
|
||||
embedded config field so the host can use this value to determine the mode that was
|
||||
in effect at the time of the State snapshot instead of having to infer it from the
|
||||
other config fields. */
|
||||
char text[51];
|
||||
/* *
|
||||
For a "magnetic" detent mode - where not all positions should have detents - this
|
||||
specifies which positions (up to 5) have detents enabled. The knob will feel like it
|
||||
is "magnetically" attracted to those positions, and will rotate smoothy past all
|
||||
other positions.
|
||||
|
||||
If you want to have more than 5 magnetic detent positions, you will need to dynamically
|
||||
update this list as the knob is rotated. A recommended approach is to always send the
|
||||
_nearest_ 5 detent positions, and send a new Config message whenever the list of
|
||||
positions nearest the current position (as reported via State messages) changes.
|
||||
|
||||
This approach enables effectively unbounded detent positions while keeping Config
|
||||
bounded in size, and is resilient against tightly-packed detents with fast rotation
|
||||
since multiple detent positions can be sent in advance; a full round-trip Config-State
|
||||
isn't needed between each detent in order to keep up. */
|
||||
pb_size_t detent_positions_count;
|
||||
int32_t detent_positions[5];
|
||||
/* *
|
||||
Advanced feature for shifting the defined snap_point away from the center (position 0)
|
||||
for implementing asymmetric detents. Typical value: 0 (symmetric detent force).
|
||||
|
||||
This can be used to create detents that will hold the position when carefully released,
|
||||
but can be easily disturbed to return "home" towards position 0. */
|
||||
float snap_point_bias;
|
||||
/* *
|
||||
Hue (0-255) for all 8 ring LEDs, if supported. Note: this will likely be replaced
|
||||
with more configurability in a future protocol version. */
|
||||
int16_t led_hue;
|
||||
} PB_SmartKnobConfig;
|
||||
|
||||
typedef struct _PB_SmartKnobState {
|
||||
/* * Current integer position of the knob. (Detent resolution is at integer positions) */
|
||||
int32_t current_position;
|
||||
/* *
|
||||
Current fractional position. Typically will only range from (-snap_point, snap_point)
|
||||
since further rotation will result in the integer position changing, but may exceed
|
||||
those values if snap_point_bias is non-zero, or if the knob is at a bound. When the
|
||||
knob is at a bound, this value can grow endlessly as the knob is rotated further past
|
||||
the bound.
|
||||
|
||||
When visualizing sub_position_unit, you will likely want to apply a rubber-band easing
|
||||
function past the bounds; a sublinear relationship will help suggest that a bound has
|
||||
been reached. */
|
||||
float sub_position_unit;
|
||||
/* *
|
||||
Current SmartKnobConfig in effect at the time of this State snapshot.
|
||||
|
||||
Beware that this config contains position and sub_position_unit values, not to be
|
||||
confused with the top level current_position and sub_position_unit values in this State
|
||||
message. The position values in the embedded config message will almost never be useful
|
||||
to you; you probably want to be reading the top level values from the State message. */
|
||||
bool has_config;
|
||||
PB_SmartKnobConfig config;
|
||||
/* *
|
||||
Value that changes each time the knob is pressed. Does not change when a press is released.
|
||||
|
||||
Why this press state a "nonce" rather than a simple boolean representing the current
|
||||
"pressed" state? It makes the protocol more robust to dropped/lost State messages; if
|
||||
the knob was pressed/released quickly and State messages happened to be dropped during
|
||||
that time, the press would be completely lost. Using a nonce allows the host to recognize
|
||||
that a press has taken place at some point even if the State was lost during the press
|
||||
itself. Is this overkill? Probably, let's revisit in future protocol versions. */
|
||||
uint8_t press_nonce;
|
||||
} 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 */
|
||||
/* Message FROM the SmartKnob to the host */
|
||||
typedef struct _PB_FromSmartKnob {
|
||||
uint8_t protocol_version;
|
||||
pb_size_t which_payload;
|
||||
union {
|
||||
PB_Ack ack;
|
||||
@ -59,58 +164,128 @@ typedef struct _PB_FromSmartKnob {
|
||||
} payload;
|
||||
} PB_FromSmartKnob;
|
||||
|
||||
typedef struct _PB_RequestState {
|
||||
char dummy_field;
|
||||
} PB_RequestState;
|
||||
|
||||
/* Message TO the Smartknob from the host */
|
||||
typedef struct _PB_ToSmartknob {
|
||||
uint8_t protocol_version;
|
||||
uint32_t nonce;
|
||||
pb_size_t which_payload;
|
||||
union {
|
||||
PB_RequestState request_state;
|
||||
PB_SmartKnobConfig smartknob_config;
|
||||
} payload;
|
||||
} PB_ToSmartknob;
|
||||
|
||||
typedef struct _PB_MotorCalibration {
|
||||
bool calibrated;
|
||||
float zero_electrical_offset;
|
||||
bool direction_cw;
|
||||
uint32_t pole_pairs;
|
||||
} PB_MotorCalibration;
|
||||
|
||||
typedef struct _PB_StrainCalibration {
|
||||
int32_t idle_value;
|
||||
int32_t press_delta;
|
||||
} PB_StrainCalibration;
|
||||
|
||||
typedef struct _PB_PersistentConfiguration {
|
||||
uint32_t version;
|
||||
bool has_motor;
|
||||
PB_MotorCalibration motor;
|
||||
bool has_strain;
|
||||
PB_StrainCalibration strain;
|
||||
} PB_PersistentConfiguration;
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define PB_FromSmartKnob_init_default {0, {PB_Ack_init_default}}
|
||||
#define PB_FromSmartKnob_init_default {0, 0, {PB_Ack_init_default}}
|
||||
#define PB_ToSmartknob_init_default {0, 0, 0, {PB_RequestState_init_default}}
|
||||
#define PB_Ack_init_default {0}
|
||||
#define PB_Log_init_default {""}
|
||||
#define PB_SmartKnobState_init_default {0, 0, false, PB_SmartKnobConfig_init_default}
|
||||
#define PB_ToSmartknob_init_default {0, 0, {PB_RequestState_init_default}}
|
||||
#define PB_SmartKnobConfig_init_default {0, 0, 0, 0, 0, 0, ""}
|
||||
#define PB_SmartKnobState_init_default {0, 0, false, PB_SmartKnobConfig_init_default, 0}
|
||||
#define PB_SmartKnobConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0, 0}
|
||||
#define PB_RequestState_init_default {0}
|
||||
#define PB_FromSmartKnob_init_zero {0, {PB_Ack_init_zero}}
|
||||
#define PB_PersistentConfiguration_init_default {0, false, PB_MotorCalibration_init_default, false, PB_StrainCalibration_init_default}
|
||||
#define PB_MotorCalibration_init_default {0, 0, 0, 0}
|
||||
#define PB_StrainCalibration_init_default {0, 0}
|
||||
#define PB_FromSmartKnob_init_zero {0, 0, {PB_Ack_init_zero}}
|
||||
#define PB_ToSmartknob_init_zero {0, 0, 0, {PB_RequestState_init_zero}}
|
||||
#define PB_Ack_init_zero {0}
|
||||
#define PB_Log_init_zero {""}
|
||||
#define PB_SmartKnobState_init_zero {0, 0, false, PB_SmartKnobConfig_init_zero}
|
||||
#define PB_ToSmartknob_init_zero {0, 0, {PB_RequestState_init_zero}}
|
||||
#define PB_SmartKnobConfig_init_zero {0, 0, 0, 0, 0, 0, ""}
|
||||
#define PB_SmartKnobState_init_zero {0, 0, false, PB_SmartKnobConfig_init_zero, 0}
|
||||
#define PB_SmartKnobConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, {0, 0, 0, 0, 0}, 0, 0}
|
||||
#define PB_RequestState_init_zero {0}
|
||||
#define PB_PersistentConfiguration_init_zero {0, false, PB_MotorCalibration_init_zero, false, PB_StrainCalibration_init_zero}
|
||||
#define PB_MotorCalibration_init_zero {0, 0, 0, 0}
|
||||
#define PB_StrainCalibration_init_zero {0, 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_SmartKnobConfig_position_tag 1
|
||||
#define PB_SmartKnobConfig_sub_position_unit_tag 2
|
||||
#define PB_SmartKnobConfig_position_nonce_tag 3
|
||||
#define PB_SmartKnobConfig_min_position_tag 4
|
||||
#define PB_SmartKnobConfig_max_position_tag 5
|
||||
#define PB_SmartKnobConfig_position_width_radians_tag 6
|
||||
#define PB_SmartKnobConfig_detent_strength_unit_tag 7
|
||||
#define PB_SmartKnobConfig_endstop_strength_unit_tag 8
|
||||
#define PB_SmartKnobConfig_snap_point_tag 9
|
||||
#define PB_SmartKnobConfig_text_tag 10
|
||||
#define PB_SmartKnobConfig_detent_positions_tag 11
|
||||
#define PB_SmartKnobConfig_snap_point_bias_tag 12
|
||||
#define PB_SmartKnobConfig_led_hue_tag 13
|
||||
#define PB_SmartKnobState_current_position_tag 1
|
||||
#define PB_SmartKnobState_sub_position_unit_tag 2
|
||||
#define PB_SmartKnobState_config_tag 3
|
||||
#define PB_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
|
||||
#define PB_SmartKnobState_press_nonce_tag 4
|
||||
#define PB_FromSmartKnob_protocol_version_tag 1
|
||||
#define PB_FromSmartKnob_ack_tag 2
|
||||
#define PB_FromSmartKnob_log_tag 3
|
||||
#define PB_FromSmartKnob_smartknob_state_tag 4
|
||||
#define PB_ToSmartknob_protocol_version_tag 1
|
||||
#define PB_ToSmartknob_nonce_tag 2
|
||||
#define PB_ToSmartknob_request_state_tag 3
|
||||
#define PB_ToSmartknob_smartknob_config_tag 4
|
||||
#define PB_MotorCalibration_calibrated_tag 1
|
||||
#define PB_MotorCalibration_zero_electrical_offset_tag 2
|
||||
#define PB_MotorCalibration_direction_cw_tag 3
|
||||
#define PB_MotorCalibration_pole_pairs_tag 4
|
||||
#define PB_StrainCalibration_idle_value_tag 1
|
||||
#define PB_StrainCalibration_press_delta_tag 2
|
||||
#define PB_PersistentConfiguration_version_tag 1
|
||||
#define PB_PersistentConfiguration_motor_tag 2
|
||||
#define PB_PersistentConfiguration_strain_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)
|
||||
X(a, STATIC, SINGULAR, UINT32, protocol_version, 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,ack,payload.ack), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,log,payload.log), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,smartknob_state,payload.smartknob_state), 4)
|
||||
#define PB_FromSmartKnob_CALLBACK NULL
|
||||
#define PB_FromSmartKnob_DEFAULT NULL
|
||||
#define PB_FromSmartKnob_payload_ack_MSGTYPE PB_Ack
|
||||
#define PB_FromSmartKnob_payload_log_MSGTYPE PB_Log
|
||||
#define PB_FromSmartKnob_payload_smartknob_state_MSGTYPE PB_SmartKnobState
|
||||
|
||||
#define PB_ToSmartknob_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, protocol_version, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, nonce, 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,request_state,payload.request_state), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,smartknob_config,payload.smartknob_config), 4)
|
||||
#define PB_ToSmartknob_CALLBACK NULL
|
||||
#define PB_ToSmartknob_DEFAULT NULL
|
||||
#define PB_ToSmartknob_payload_request_state_MSGTYPE PB_RequestState
|
||||
#define PB_ToSmartknob_payload_smartknob_config_MSGTYPE PB_SmartKnobConfig
|
||||
|
||||
#define PB_Ack_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, nonce, 1)
|
||||
#define PB_Ack_CALLBACK NULL
|
||||
@ -124,28 +299,26 @@ X(a, STATIC, SINGULAR, STRING, msg, 1)
|
||||
#define PB_SmartKnobState_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, current_position, 1) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, sub_position_unit, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, config, 3)
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, config, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, press_nonce, 4)
|
||||
#define PB_SmartKnobState_CALLBACK NULL
|
||||
#define PB_SmartKnobState_DEFAULT NULL
|
||||
#define PB_SmartKnobState_config_MSGTYPE PB_SmartKnobConfig
|
||||
|
||||
#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)
|
||||
X(a, STATIC, SINGULAR, INT32, position, 1) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, sub_position_unit, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, position_nonce, 3) \
|
||||
X(a, STATIC, SINGULAR, INT32, min_position, 4) \
|
||||
X(a, STATIC, SINGULAR, INT32, max_position, 5) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, position_width_radians, 6) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, detent_strength_unit, 7) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, endstop_strength_unit, 8) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, snap_point, 9) \
|
||||
X(a, STATIC, SINGULAR, STRING, text, 10) \
|
||||
X(a, STATIC, REPEATED, INT32, detent_positions, 11) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, snap_point_bias, 12) \
|
||||
X(a, STATIC, SINGULAR, INT32, led_hue, 13)
|
||||
#define PB_SmartKnobConfig_CALLBACK NULL
|
||||
#define PB_SmartKnobConfig_DEFAULT NULL
|
||||
|
||||
@ -154,31 +327,63 @@ X(a, STATIC, SINGULAR, STRING, text, 7)
|
||||
#define PB_RequestState_CALLBACK NULL
|
||||
#define PB_RequestState_DEFAULT NULL
|
||||
|
||||
#define PB_PersistentConfiguration_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, motor, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, strain, 3)
|
||||
#define PB_PersistentConfiguration_CALLBACK NULL
|
||||
#define PB_PersistentConfiguration_DEFAULT NULL
|
||||
#define PB_PersistentConfiguration_motor_MSGTYPE PB_MotorCalibration
|
||||
#define PB_PersistentConfiguration_strain_MSGTYPE PB_StrainCalibration
|
||||
|
||||
#define PB_MotorCalibration_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, calibrated, 1) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, zero_electrical_offset, 2) \
|
||||
X(a, STATIC, SINGULAR, BOOL, direction_cw, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, pole_pairs, 4)
|
||||
#define PB_MotorCalibration_CALLBACK NULL
|
||||
#define PB_MotorCalibration_DEFAULT NULL
|
||||
|
||||
#define PB_StrainCalibration_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, idle_value, 1) \
|
||||
X(a, STATIC, SINGULAR, INT32, press_delta, 2)
|
||||
#define PB_StrainCalibration_CALLBACK NULL
|
||||
#define PB_StrainCalibration_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t PB_FromSmartKnob_msg;
|
||||
extern const pb_msgdesc_t PB_ToSmartknob_msg;
|
||||
extern const pb_msgdesc_t PB_Ack_msg;
|
||||
extern const pb_msgdesc_t PB_Log_msg;
|
||||
extern const pb_msgdesc_t PB_SmartKnobState_msg;
|
||||
extern const pb_msgdesc_t PB_ToSmartknob_msg;
|
||||
extern const pb_msgdesc_t PB_SmartKnobConfig_msg;
|
||||
extern const pb_msgdesc_t PB_RequestState_msg;
|
||||
extern const pb_msgdesc_t PB_PersistentConfiguration_msg;
|
||||
extern const pb_msgdesc_t PB_MotorCalibration_msg;
|
||||
extern const pb_msgdesc_t PB_StrainCalibration_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define PB_FromSmartKnob_fields &PB_FromSmartKnob_msg
|
||||
#define PB_ToSmartknob_fields &PB_ToSmartknob_msg
|
||||
#define PB_Ack_fields &PB_Ack_msg
|
||||
#define PB_Log_fields &PB_Log_msg
|
||||
#define PB_SmartKnobState_fields &PB_SmartKnobState_msg
|
||||
#define PB_ToSmartknob_fields &PB_ToSmartknob_msg
|
||||
#define PB_SmartKnobConfig_fields &PB_SmartKnobConfig_msg
|
||||
#define PB_RequestState_fields &PB_RequestState_msg
|
||||
#define PB_PersistentConfiguration_fields &PB_PersistentConfiguration_msg
|
||||
#define PB_MotorCalibration_fields &PB_MotorCalibration_msg
|
||||
#define PB_StrainCalibration_fields &PB_StrainCalibration_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define PB_Ack_size 6
|
||||
#define PB_FromSmartKnob_size 261
|
||||
#define PB_FromSmartKnob_size 264
|
||||
#define PB_Log_size 258
|
||||
#define PB_MotorCalibration_size 15
|
||||
#define PB_PersistentConfiguration_size 47
|
||||
#define PB_RequestState_size 0
|
||||
#define PB_SmartKnobConfig_size 94
|
||||
#define PB_SmartKnobState_size 112
|
||||
#define PB_ToSmartknob_size 102
|
||||
#define PB_SmartKnobConfig_size 184
|
||||
#define PB_SmartKnobState_size 206
|
||||
#define PB_StrainCalibration_size 22
|
||||
#define PB_ToSmartknob_size 196
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@ -2,14 +2,22 @@
|
||||
|
||||
#include "proto_gen/smartknob.pb.h"
|
||||
|
||||
#define PROTOBUF_PROTOCOL_VERSION (1)
|
||||
|
||||
bool config_eq(PB_SmartKnobConfig& first, PB_SmartKnobConfig& second) {
|
||||
return first.detent_strength_unit == second.detent_strength_unit
|
||||
&& first.endstop_strength_unit == second.endstop_strength_unit
|
||||
&& first.num_positions == second.num_positions
|
||||
&& first.position == second.position
|
||||
&& first.position_nonce == second.position_nonce
|
||||
&& first.min_position == second.min_position
|
||||
&& first.max_position == second.max_position
|
||||
&& first.position_width_radians == second.position_width_radians
|
||||
&& first.snap_point == second.snap_point
|
||||
&& strcmp(first.text, second.text) == 0;
|
||||
&& first.sub_position_unit == second.sub_position_unit
|
||||
&& strcmp(first.text, second.text) == 0
|
||||
&& first.detent_positions_count == second.detent_positions_count
|
||||
&& memcmp(first.detent_positions, second.detent_positions, first.detent_positions_count * sizeof(first.detent_positions[0]))
|
||||
&& first.snap_point_bias == second.snap_point_bias;
|
||||
}
|
||||
|
||||
bool state_eq(PB_SmartKnobState& first, PB_SmartKnobState& second) {
|
||||
@ -6,11 +6,18 @@ 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_.config.min_position != state.config.min_position)
|
||||
|| (latest_state_.config.max_position != state.config.max_position);
|
||||
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);
|
||||
stream_.printf("STATE: %d [%d, %d] (detent strength: %0.2f, width: %0.0f deg, endstop strength: %0.2f)\n",
|
||||
state.current_position,
|
||||
state.config.min_position,
|
||||
state.config.max_position,
|
||||
state.config.detent_strength_unit,
|
||||
degrees(state.config.position_width_radians),
|
||||
state.config.endstop_strength_unit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,12 +40,17 @@ void SerialProtocolPlaintext::loop() {
|
||||
demo_config_change_callback_();
|
||||
}
|
||||
} else if (b == 'C') {
|
||||
motor_task_.runCalibration();
|
||||
motor_calibration_callback_();
|
||||
} else if (b == 'S') {
|
||||
if (strain_calibration_callback_) {
|
||||
strain_calibration_callback_();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
void SerialProtocolPlaintext::init(DemoConfigChangeCallback demo_config_change_callback, StrainCalibrationCallback strain_calibration_callback) {
|
||||
demo_config_change_callback_ = demo_config_change_callback;
|
||||
strain_calibration_callback_ = strain_calibration_callback;
|
||||
stream_.println("SmartKnob starting!\n\nSerial mode: plaintext\nPress 'C' at any time to calibrate motor/sensor.\nPress 'S' at any time to calibrate strain sensors.\nPress <Space> to change haptic modes.");
|
||||
}
|
||||
|
||||
@ -2,25 +2,28 @@
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "interface_callbacks.h"
|
||||
#include "motor_task.h"
|
||||
#include "serial_protocol.h"
|
||||
#include "uart_stream.h"
|
||||
|
||||
typedef std::function<void(void)> DemoConfigChangeCallback;
|
||||
typedef std::function<void(void)> StrainCalibrationCallback;
|
||||
|
||||
class SerialProtocolPlaintext : public SerialProtocol {
|
||||
public:
|
||||
SerialProtocolPlaintext(Stream& stream, MotorTask& motor_task) : SerialProtocol(), stream_(stream), motor_task_(motor_task) {}
|
||||
SerialProtocolPlaintext(Stream& stream, MotorCalibrationCallback motor_calibration_callback) : SerialProtocol(), stream_(stream), motor_calibration_callback_(motor_calibration_callback) {}
|
||||
~SerialProtocolPlaintext(){}
|
||||
void log(const char* msg) override;
|
||||
void loop() override;
|
||||
void handleState(const PB_SmartKnobState& state) override;
|
||||
|
||||
void init(DemoConfigChangeCallback cb);
|
||||
void init(DemoConfigChangeCallback demo_config_change_callback, StrainCalibrationCallback strain_calibration_callback);
|
||||
|
||||
private:
|
||||
Stream& stream_;
|
||||
MotorTask& motor_task_;
|
||||
MotorCalibrationCallback motor_calibration_callback_;
|
||||
PB_SmartKnobState latest_state_ = {};
|
||||
DemoConfigChangeCallback demo_config_change_callback_;
|
||||
StrainCalibrationCallback strain_calibration_callback_;
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "../proto_helpers.h"
|
||||
#include "proto_helpers.h"
|
||||
|
||||
#include "crc32.h"
|
||||
#include "pb_encode.h"
|
||||
@ -14,10 +14,10 @@ static SerialProtocolProtobuf* singleton_for_packet_serial = 0;
|
||||
static const uint16_t MIN_STATE_INTERVAL_MILLIS = 5;
|
||||
static const uint16_t PERIODIC_STATE_INTERVAL_MILLIS = 5000;
|
||||
|
||||
SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task) :
|
||||
SerialProtocolProtobuf::SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback) :
|
||||
SerialProtocol(),
|
||||
stream_(stream),
|
||||
motor_task_(motor_task),
|
||||
config_callback_(config_callback),
|
||||
packet_serial_() {
|
||||
packet_serial_.setStream(&stream);
|
||||
|
||||
@ -105,6 +105,13 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pb_rx_buffer_.protocol_version != PROTOBUF_PROTOCOL_VERSION) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Invalid protocol version. Expected %u, received %u", PROTOBUF_PROTOCOL_VERSION, pb_rx_buffer_.protocol_version);
|
||||
log(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always ACK immediately
|
||||
ack(pb_rx_buffer_.nonce);
|
||||
if (pb_rx_buffer_.nonce == last_nonce_) {
|
||||
@ -118,9 +125,12 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
|
||||
|
||||
switch (pb_rx_buffer_.which_payload) {
|
||||
case PB_ToSmartknob_smartknob_config_tag: {
|
||||
motor_task_.setConfig(pb_rx_buffer_.payload.smartknob_config);
|
||||
config_callback_(pb_rx_buffer_.payload.smartknob_config);
|
||||
break;
|
||||
}
|
||||
case PB_ToSmartknob_request_state_tag:
|
||||
state_requested_ = true;
|
||||
break;
|
||||
default: {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Unknown payload type: %d", pb_rx_buffer_.which_payload);
|
||||
@ -133,6 +143,7 @@ void SerialProtocolProtobuf::handlePacket(const uint8_t* buffer, size_t size) {
|
||||
void SerialProtocolProtobuf::sendPbTxBuffer() {
|
||||
// Encode protobuf message to byte buffer
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(tx_buffer_, sizeof(tx_buffer_));
|
||||
pb_tx_buffer_.protocol_version = PROTOBUF_PROTOCOL_VERSION;
|
||||
if (!pb_encode(&stream, PB_FromSmartKnob_fields, &pb_tx_buffer_)) {
|
||||
stream_.println(stream.errmsg);
|
||||
stream_.flush();
|
||||
|
||||
@ -4,21 +4,22 @@
|
||||
|
||||
#include "../proto_gen/smartknob.pb.h"
|
||||
|
||||
#include "interface_callbacks.h"
|
||||
#include "motor_task.h"
|
||||
#include "serial_protocol.h"
|
||||
#include "uart_stream.h"
|
||||
|
||||
class SerialProtocolProtobuf : public SerialProtocol {
|
||||
public:
|
||||
SerialProtocolProtobuf(Stream& stream, MotorTask& motor_task);
|
||||
~SerialProtocolProtobuf(){}
|
||||
SerialProtocolProtobuf(Stream& stream, ConfigCallback config_callback);
|
||||
~SerialProtocolProtobuf(){};
|
||||
void log(const char* msg) override;
|
||||
void loop() override;
|
||||
void handleState(const PB_SmartKnobState& state) override;
|
||||
|
||||
private:
|
||||
Stream& stream_;
|
||||
MotorTask& motor_task_;
|
||||
ConfigCallback config_callback_;
|
||||
|
||||
PB_FromSmartKnob pb_tx_buffer_;
|
||||
PB_ToSmartknob pb_rx_buffer_;
|
||||
|
||||
6
firmware/src/util.cpp
Normal file
6
firmware/src/util.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
#include "util.h"
|
||||
|
||||
float lerp(const float value, const float inMin, const float inMax, const float min, const float max) {
|
||||
// Map the input value from the input range to the output range
|
||||
return ((value - inMin) / (inMax - inMin)) * (max - min) + min;
|
||||
}
|
||||
@ -5,3 +5,11 @@ template <typename T> T CLAMP(const T& value, const T& low, const T& high)
|
||||
{
|
||||
return value < low ? low : (value > high ? high : value);
|
||||
}
|
||||
|
||||
#define COUNT_OF(A) (sizeof(A) / sizeof(A[0]))
|
||||
|
||||
float lerp(const float value, const float inMin, const float inMax, const float min, const float max);
|
||||
|
||||
template <typename T> int sgn(T val) {
|
||||
return (T(0) < val) - (val < T(0));
|
||||
}
|
||||
|
||||
209
platformio.ini
209
platformio.ini
@ -24,40 +24,53 @@ monitor_flags =
|
||||
--eol=CRLF
|
||||
--echo
|
||||
--filter=esp32_exception_decoder
|
||||
upload_speed = 921600
|
||||
lib_deps =
|
||||
askuric/Simple FOC @ 2.2.0
|
||||
infineon/TLV493D-Magnetic-Sensor @ 1.0.3
|
||||
bxparks/AceButton @ 1.9.1
|
||||
bakercp/PacketSerial @ 1.4.0
|
||||
nanopb/Nanopb @ 0.4.7 ; 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 =
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
-DMONITOR_SPEED=921600
|
||||
-DMONITOR_SPEED=921600
|
||||
|
||||
[env:view]
|
||||
extends = base_config
|
||||
board = esp32doit-devkit-v1
|
||||
board_build.partitions = default_ffat.csv
|
||||
lib_deps =
|
||||
${base_config.lib_deps}
|
||||
askuric/Simple FOC @ 2.2.0
|
||||
bodmer/TFT_eSPI@2.4.25
|
||||
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}
|
||||
; Display enabled: 1=enable, 0=disable
|
||||
-DSK_DISPLAY=1
|
||||
; Display orientation: 0=usb bottom, 2=usb top
|
||||
-DSK_DISPLAY_ROTATION=0
|
||||
; LEDs enabled: 1=enable, 0=disable
|
||||
-DSK_LEDS=1
|
||||
; Number of LEDs
|
||||
-DNUM_LEDS=8
|
||||
-DSENSOR_MT6701=1
|
||||
; Strain-gauge press input enabled: 1=enable, 0=disable
|
||||
-DSK_STRAIN=1
|
||||
-DSK_INVERT_ROTATION=1
|
||||
; Ambient light sensor (VEML7700) enabled: 1=enable (display/LEDs match ambient brightness), 0=disable (100% brightness all the time)
|
||||
-DSK_ALS=1
|
||||
; Use MT6701 magnetic encoder
|
||||
-DSENSOR_MT6701=1
|
||||
; Invert direction of angle sensor (motor direction is detected relative to angle sensor as part of the calibration procedure)
|
||||
-DSK_INVERT_ROTATION=1
|
||||
|
||||
-DMOTOR_WANZHIDA_ONCE_TOP=1
|
||||
|
||||
; Pin configurations
|
||||
-DPIN_UH=26
|
||||
-DPIN_UL=25
|
||||
-DPIN_VH=27
|
||||
@ -73,12 +86,17 @@ build_flags =
|
||||
-DPIN_MT_CSN=14
|
||||
-DPIN_LED_DATA=7
|
||||
-DPIN_LCD_BACKLIGHT=19
|
||||
-DPIN_STRAIN_DO=38
|
||||
-DPIN_STRAIN_SCK=2
|
||||
|
||||
-DDESCRIPTION_FONT=Roboto_Thin_24
|
||||
-DDESCRIPTION_Y_OFFSET=20
|
||||
-DVALUE_OFFSET=30
|
||||
-DDRAW_ARC=0
|
||||
|
||||
-DSK_BACKLIGHT_BIT_DEPTH=16
|
||||
|
||||
; TFT_eSPI setup
|
||||
-DUSER_SETUP_LOADED=1
|
||||
-DGC9A01_DRIVER=1
|
||||
-DCGRAM_OFFSET=1
|
||||
@ -98,11 +116,178 @@ build_flags =
|
||||
; Reduce loop task stack size (only works on newer IDF Arduino core)
|
||||
; -DARDUINO_LOOP_STACK_SIZE=2048
|
||||
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
|
||||
; FastLED setup
|
||||
; Modify the default unusable pin mask to allow GPIO 7 (allowed to use on ESP32-PICO-V3-02)
|
||||
; Unusable bits: 6, 8, 9, 10, 20
|
||||
; (0ULL | _FL_BIT(6) | _FL_BIT(8) | _FL_BIT(9) | _FL_BIT(10) | _FL_BIT(20))
|
||||
-DFASTLED_UNUSABLE_PIN_MASK=0x100740LL
|
||||
; 0~39 except from 24, 28~31 are valid
|
||||
; (0xFFFFFFFFFFULL & ~(0ULL | _FL_BIT(24) | _FL_BIT(28) | _FL_BIT(29) | _FL_BIT(30) | _FL_BIT(31)))
|
||||
-DSOC_GPIO_VALID_GPIO_MASK=0xFF0EFFFFFF
|
||||
; GPIO >= 34 are input only
|
||||
; (SOC_GPIO_VALID_GPIO_MASK & ~(0ULL | _FL_BIT(34) | _FL_BIT(35) | _FL_BIT(36) | _FL_BIT(37) | _FL_BIT(38) | _FL_BIT(39)))
|
||||
-DSOC_GPIO_VALID_OUTPUT_GPIO_MASK=0x30EFFFFFF
|
||||
|
||||
|
||||
[env:nanofoc]
|
||||
extends = base_config
|
||||
platform = espressif32@6.3.1
|
||||
board = adafruit_feather_esp32s3
|
||||
board_build.partitions = firmware/partitions-4MB-fat.csv
|
||||
lib_deps =
|
||||
${base_config.lib_deps}
|
||||
askuric/Simple FOC@2.3.0
|
||||
bodmer/TFT_eSPI@2.5.0
|
||||
|
||||
build_flags =
|
||||
${base_config.build_flags}
|
||||
; Display enabled: 1=enable, 0=disable
|
||||
-DSK_DISPLAY=0
|
||||
; Display orientation: 0=usb bottom, 2=usb top
|
||||
-DSK_DISPLAY_ROTATION=0
|
||||
; LEDs enabled: 1=enable, 0=disable
|
||||
-DSK_LEDS=0
|
||||
; Number of LEDs
|
||||
-DNUM_LEDS=8
|
||||
; Strain-gauge press input enabled: 1=enable, 0=disable
|
||||
-DSK_STRAIN=0
|
||||
; Ambient light sensor (VEML7700) enabled: 1=enable (display/LEDs match ambient brightness), 0=disable (100% brightness all the time)
|
||||
-DSK_ALS=0
|
||||
|
||||
-DSENSOR_MAQ430=1
|
||||
-DPIN_MAQ_SCK=6
|
||||
-DPIN_MAQ_MISO=7
|
||||
-DPIN_MAQ_MOSI=5
|
||||
-DPIN_MAQ_SS=4
|
||||
; Invert direction of angle sensor (motor direction is detected relative to angle sensor as part of the calibration procedure)
|
||||
-DSK_INVERT_ROTATION=1
|
||||
|
||||
-DMOTOR_MAD2804=1
|
||||
|
||||
; Pin configurations
|
||||
-DPIN_UH=21
|
||||
-DPIN_UL=12
|
||||
-DPIN_VH=14
|
||||
-DPIN_VL=10
|
||||
-DPIN_WH=13
|
||||
-DPIN_WL=11
|
||||
-DPIN_BUTTON_NEXT=-1
|
||||
-DPIN_BUTTON_PREV=-1
|
||||
-DPIN_LED_DATA=7
|
||||
-DPIN_LCD_BACKLIGHT=08
|
||||
|
||||
-DPIO_FRAMEWORK_ARDUINO_ENABLE_CDC=1
|
||||
-DUSBCON=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DCORE_DEBUG_LEVEL=2
|
||||
-DHSPI_SPEED=100000 ; MA/MAQ Nominal SPI Speed in Mhz (HSPI)
|
||||
-DVSPI_SPEED=400000 ; TFt Nominal SPI Speed in Mhz (VSPI)
|
||||
|
||||
; Reduce loop task stack size (only works on newer IDF Arduino core)
|
||||
; -DARDUINO_LOOP_STACK_SIZE=2048
|
||||
|
||||
|
||||
[env:brushknight_esp32s3]
|
||||
extends = base_config
|
||||
platform = espressif32@6.3.1
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = default_ffat.csv
|
||||
|
||||
monitor_speed = 115200
|
||||
|
||||
lib_deps =
|
||||
${base_config.lib_deps}
|
||||
bodmer/TFT_eSPI@2.5.0
|
||||
fastled/FastLED @ 3.5.0
|
||||
bogde/HX711 @ 0.7.5
|
||||
adafruit/Adafruit VEML7700 Library @ 1.1.1
|
||||
askuric/Simple FOC@2.3.0
|
||||
|
||||
build_flags =
|
||||
${base_config.build_flags}
|
||||
; Use physical UART for the serial stream rather than the S3 default USB CDC
|
||||
-DSK_FORCE_UART_STREAM=1
|
||||
-DMONITOR_SPEED=115200
|
||||
; Display enabled: 1=enable, 0=disable
|
||||
-DSK_DISPLAY=1
|
||||
; PWM bit resolution (even esp32s3 claims that there are 13 bits, max is 12, after it panics)
|
||||
-DSK_BACKLIGHT_BIT_DEPTH=12
|
||||
; Display orientation: 0=usb bottom, 2=usb top
|
||||
-DSK_DISPLAY_ROTATION=0
|
||||
; LEDs enabled: 1=enable, 0=disable
|
||||
-DSK_LEDS=1
|
||||
; Number of LEDs
|
||||
-DNUM_LEDS=8
|
||||
; Strain-gauge press input enabled: 1=enable, 0=disable
|
||||
-DSK_STRAIN=1
|
||||
; Ambient light sensor (VEML7700) enabled: 1=enable (display/LEDs match ambient brightness), 0=disable (100% brightness all the time)
|
||||
-DSK_ALS=0
|
||||
; Use MT6701 magnetic encoder
|
||||
-DSENSOR_MT6701=1
|
||||
; Invert direction of angle sensor (motor direction is detected relative to angle sensor as part of the calibration procedure)
|
||||
-DSK_INVERT_ROTATION=1
|
||||
|
||||
-DMOTOR_WANZHIDA_ONCE_TOP=1
|
||||
|
||||
; Pin configurations
|
||||
; Motor
|
||||
-DPIN_UH=20
|
||||
-DPIN_UL=19
|
||||
-DPIN_VH=21
|
||||
-DPIN_VL=17
|
||||
-DPIN_WH=12
|
||||
-DPIN_WL=18
|
||||
|
||||
-DPIN_BUTTON_NEXT=-1
|
||||
-DPIN_BUTTON_PREV=-1
|
||||
-DPIN_LED_DATA=7
|
||||
-DPIN_LCD_BACKLIGHT=5
|
||||
|
||||
-DPIN_SDA=15
|
||||
-DPIN_SCL=8
|
||||
|
||||
-DPIN_MT_DATA=37
|
||||
-DPIN_MT_CLOCK=13
|
||||
-DPIN_MT_CSN=14
|
||||
|
||||
-DPIN_STRAIN_DO=38
|
||||
-DPIN_STRAIN_SCK=1
|
||||
|
||||
-DDESCRIPTION_FONT=Roboto_Thin_24
|
||||
-DDESCRIPTION_Y_OFFSET=20
|
||||
-DVALUE_OFFSET=30
|
||||
-DDRAW_ARC=0
|
||||
|
||||
; TFT_eSPI setup
|
||||
-DUSER_SETUP_LOADED=1
|
||||
-DGC9A01_DRIVER=1
|
||||
-DCGRAM_OFFSET=1
|
||||
-DTFT_WIDTH=240
|
||||
-DTFT_HEIGHT=240
|
||||
-DTFT_MISO=-1 # fake
|
||||
-DTFT_MOSI=3
|
||||
-DTFT_SCLK=4
|
||||
-DTFT_CS=9
|
||||
-DTFT_DC=2
|
||||
-DTFT_RST=6
|
||||
-DTFT_BL=-1
|
||||
-DLOAD_GLCD=1
|
||||
-DLOAD_GFXFF=1
|
||||
-DSPI_FREQUENCY=40000000
|
||||
|
||||
; Reduce loop task stack size (only works on newer IDF Arduino core)
|
||||
; -DARDUINO_LOOP_STACK_SIZE=2048
|
||||
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
|
||||
; FastLED setup
|
||||
; Modify the default unusable pin mask to allow GPIO 7 (allowed to use on ESP32-PICO-V3-02)
|
||||
; Unusable bits: 6, 8, 9, 10, 20
|
||||
; (0ULL | _FL_BIT(6) | _FL_BIT(8) | _FL_BIT(9) | _FL_BIT(10) | _FL_BIT(20))
|
||||
-DFASTLED_UNUSABLE_PIN_MASK=0x100740LL
|
||||
|
||||
; 0~39 except from 24, 28~31 are valid
|
||||
; (0xFFFFFFFFFFULL & ~(0ULL | _FL_BIT(24) | _FL_BIT(28) | _FL_BIT(29) | _FL_BIT(30) | _FL_BIT(31)))
|
||||
-DSOC_GPIO_VALID_GPIO_MASK=0xFF0EFFFFFF
|
||||
|
||||
13
proto/Pipfile
Normal file
13
proto/Pipfile
Normal file
@ -0,0 +1,13 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
grpcio-tools = "*"
|
||||
protobuf = "==3.20.3"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
160
proto/Pipfile.lock
generated
Normal file
160
proto/Pipfile.lock
generated
Normal file
@ -0,0 +1,160 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "81cf165543dd8620d62e1e5c86575e5245b7b76980809b0fbd484de2aba002cb"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.10"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:094e64236253590d9d4075665c77b329d707b6fca864dd62b144255e199b4f87",
|
||||
"sha256:0dc5354e38e5adf2498312f7241b14c7ce3484eefa0082db4297189dcbe272e6",
|
||||
"sha256:0e1a9e1b4a23808f1132aa35f968cd8e659f60af3ffd6fb00bcf9a65e7db279f",
|
||||
"sha256:0fb93051331acbb75b49a2a0fd9239c6ba9528f6bdc1dd400ad1cb66cf864292",
|
||||
"sha256:16c71740640ba3a882f50b01bf58154681d44b51f09a5728180a8fdc66c67bd5",
|
||||
"sha256:172405ca6bdfedd6054c74c62085946e45ad4d9cec9f3c42b4c9a02546c4c7e9",
|
||||
"sha256:17ec9b13cec4a286b9e606b48191e560ca2f3bbdf3986f91e480a95d1582e1a7",
|
||||
"sha256:22b011674090594f1f3245960ced7386f6af35485a38901f8afee8ad01541dbd",
|
||||
"sha256:24ac1154c4b2ab4a0c5326a76161547e70664cd2c39ba75f00fc8a2170964ea2",
|
||||
"sha256:257478300735ce3c98d65a930bbda3db172bd4e00968ba743e6a1154ea6edf10",
|
||||
"sha256:29cb97d41a4ead83b7bcad23bdb25bdd170b1e2cba16db6d3acbb090bc2de43c",
|
||||
"sha256:2b170eaf51518275c9b6b22ccb59450537c5a8555326fd96ff7391b5dd75303c",
|
||||
"sha256:31bb6bc7ff145e2771c9baf612f4b9ebbc9605ccdc5f3ff3d5553de7fc0e0d79",
|
||||
"sha256:3c2b3842dcf870912da31a503454a33a697392f60c5e2697c91d133130c2c85d",
|
||||
"sha256:3f9b0023c2c92bebd1be72cdfca23004ea748be1813a66d684d49d67d836adde",
|
||||
"sha256:471d39d3370ca923a316d49c8aac66356cea708a11e647e3bdc3d0b5de4f0a40",
|
||||
"sha256:49d680356a975d9c66a678eb2dde192d5dc427a7994fb977363634e781614f7c",
|
||||
"sha256:4c4423ea38a7825b8fed8934d6d9aeebdf646c97e3c608c3b0bcf23616f33877",
|
||||
"sha256:506b9b7a4cede87d7219bfb31014d7b471cfc77157da9e820a737ec1ea4b0663",
|
||||
"sha256:538d981818e49b6ed1e9c8d5e5adf29f71c4e334e7d459bf47e9b7abb3c30e09",
|
||||
"sha256:59dffade859f157bcc55243714d57b286da6ae16469bf1ac0614d281b5f49b67",
|
||||
"sha256:5a6ebcdef0ef12005d56d38be30f5156d1cb3373b52e96f147f4a24b0ddb3a9d",
|
||||
"sha256:5dca372268c6ab6372d37d6b9f9343e7e5b4bc09779f819f9470cd88b2ece3c3",
|
||||
"sha256:6df3b63538c362312bc5fa95fb965069c65c3ea91d7ce78ad9c47cab57226f54",
|
||||
"sha256:6f0b89967ee11f2b654c23b27086d88ad7bf08c0b3c2a280362f28c3698b2896",
|
||||
"sha256:75e29a90dc319f0ad4d87ba6d20083615a00d8276b51512e04ad7452b5c23b04",
|
||||
"sha256:7942b32a291421460d6a07883033e392167d30724aa84987e6956cd15f1a21b9",
|
||||
"sha256:9235dcd5144a83f9ca6f431bd0eccc46b90e2c22fe27b7f7d77cabb2fb515595",
|
||||
"sha256:97d67983189e2e45550eac194d6234fc38b8c3b5396c153821f2d906ed46e0ce",
|
||||
"sha256:9ff42c5620b4e4530609e11afefa4a62ca91fa0abb045a8957e509ef84e54d30",
|
||||
"sha256:a8a0b77e992c64880e6efbe0086fe54dfc0bbd56f72a92d9e48264dcd2a3db98",
|
||||
"sha256:aacb54f7789ede5cbf1d007637f792d3e87f1c9841f57dd51abf89337d1b8472",
|
||||
"sha256:bc59f7ba87972ab236f8669d8ca7400f02a0eadf273ca00e02af64d588046f02",
|
||||
"sha256:cc2bece1737b44d878cc1510ea04469a8073dbbcdd762175168937ae4742dfb3",
|
||||
"sha256:cd3baccea2bc5c38aeb14e5b00167bd4e2373a373a5e4d8d850bd193edad150c",
|
||||
"sha256:dad6533411d033b77f5369eafe87af8583178efd4039c41d7515d3336c53b4f1",
|
||||
"sha256:e223a9793522680beae44671b9ed8f6d25bbe5ddf8887e66aebad5e0686049ef",
|
||||
"sha256:e473525c28251558337b5c1ad3fa969511e42304524a4e404065e165b084c9e4",
|
||||
"sha256:e4ef09f8997c4be5f3504cefa6b5c6cc3cf648274ce3cede84d4342a35d76db6",
|
||||
"sha256:e6dfc2b6567b1c261739b43d9c59d201c1b89e017afd9e684d85aa7a186c9f7a",
|
||||
"sha256:eacad297ea60c72dd280d3353d93fb1dcca952ec11de6bb3c49d12a572ba31dd",
|
||||
"sha256:f1158bccbb919da42544a4d3af5d9296a3358539ffa01018307337365a9a0c64",
|
||||
"sha256:f1fec3abaf274cdb85bf3878167cfde5ad4a4d97c68421afda95174de85ba813",
|
||||
"sha256:f96ace1540223f26fbe7c4ebbf8a98e3929a6aa0290c8033d12526847b291c0f",
|
||||
"sha256:fbdbe9a849854fe484c00823f45b7baab159bdd4a46075302281998cb8719df5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.51.1"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:0119aabd9ceedfdf41b56b9fdc8284dd85a7f589d087f2694d743f346a368556",
|
||||
"sha256:072234859f6069dc43a6be8ad6b7d682f4ba1dc2e2db2ebf5c75f62eee0f6dfb",
|
||||
"sha256:0fb6c1c1e56eb26b224adc028a4204b6ad0f8b292efa28067dff273bbc8b27c4",
|
||||
"sha256:189be2a9b672300ca6845d94016bdacc052fdbe9d1ae9e85344425efae2ff8ef",
|
||||
"sha256:21ff50e321736eba22210bf9b94e05391a9ac345f26e7df16333dc75d63e74fb",
|
||||
"sha256:3c8749dca04a8d302862ceeb1dfbdd071ee13b281395975f24405a347e5baa57",
|
||||
"sha256:4fa4300b1be59b046492ed3c5fdb59760bc6433f44c08f50de900f9552ec7461",
|
||||
"sha256:516eedd5eb7af6326050bc2cfceb3a977b9cc1144f283c43cc4956905285c912",
|
||||
"sha256:51be91b7c7056ff9ee48b1eccd4a2840b0126230803a5e09dfc082a5b16a91c1",
|
||||
"sha256:5410d6b601d1404835e34466bd8aee37213489b36ee1aad2276366e265ff29d4",
|
||||
"sha256:55fdebc73fb580717656b1bafa4f8eca448726a7aa22726a6c0a7895d2f0f088",
|
||||
"sha256:6cc298fbfe584de8876a85355efbcf796dfbcfac5948c9560f5df82e79336e2a",
|
||||
"sha256:6d9753944e5a6b6b78b76ce9d2ae0fe3f748008c1849deb7fadcb64489d6553b",
|
||||
"sha256:70564521e86a0de35ea9ac6daecff10cb46860aec469af65869974807ce8e98b",
|
||||
"sha256:7307dd2408b82ea545ae63502ec03036b025f449568556ea9a056e06129a7a4e",
|
||||
"sha256:80f450272316ca0924545f488c8492649ca3aeb7044d4bf59c426dcdee527f7c",
|
||||
"sha256:84a84d601a238572d049d3108e04fe4c206536e81076d56e623bd525a1b38def",
|
||||
"sha256:8588819b22d0de3aa1951e1991cc3e4b9aa105eecf6e3e24eb0a2fc8ab958b3e",
|
||||
"sha256:8902a035708555cddbd61b5467cea127484362decc52de03f061a1a520fe90cd",
|
||||
"sha256:8a5614251c46da07549e24f417cf989710250385e9d80deeafc53a0ee7df6325",
|
||||
"sha256:8e0d74403484eb77e8df2566a64b8b0b484b5c87903678c381634dd72f252d5e",
|
||||
"sha256:92acc3e10ba2b0dcb90a88ae9fe1cc0ffba6868545207e4ff20ca95284f8e3c9",
|
||||
"sha256:9443f5c30bac449237c3cf99da125f8d6e6c01e17972bc683ee73b75dea95573",
|
||||
"sha256:9771d4d317dca029dfaca7ec9282d8afe731c18bc536ece37fd39b8a974cc331",
|
||||
"sha256:a415fbec67d4ff7efe88794cbe00cf548d0f0a5484cceffe0a0c89d47694c491",
|
||||
"sha256:a43d26714933f23de93ea0bf9c86c66a6ede709b8ca32e357f9e2181703e64ae",
|
||||
"sha256:ace0035766fe01a1b096aa050be9f0a9f98402317e7aeff8bfe55349be32a407",
|
||||
"sha256:ae56f133b05b7e5d780ef7e032dd762adad7f3dc8f64adb43ff5bfabd659f435",
|
||||
"sha256:bdbbe63f6190187de5946891941629912ac8196701ed2253fa91624a397822ec",
|
||||
"sha256:cabc8b0905cedbc3b2b7b2856334fa35cce3d4bc79ae241cacd8cca8940a5c85",
|
||||
"sha256:cb75bac0cd43858cb759ef103fe68f8c540cb58b63dda127e710228fec3007b8",
|
||||
"sha256:d18599ab572b2f15a8f3db49503272d1bb4fcabb4b4d1214ef03aca1816b20a0",
|
||||
"sha256:d18ef2adc05a8ef9e58ac46357f6d4ce7e43e077c7eda0a4425773461f9d0e6e",
|
||||
"sha256:d598ccde6338b2cfbb3124f34c95f03394209013f9b1ed4a5360a736853b1c27",
|
||||
"sha256:d77e8b1613876e0d8fd17709509d4ceba13492816426bd156f7e88a4c47e7158",
|
||||
"sha256:d886a9e052a038642b3af5d18e6f2085d1656d9788e202dc23258cf3a751e7ca",
|
||||
"sha256:d96e96ae7361aa51c9cd9c73b677b51f691f98df6086860fcc3c45852d96b0b0",
|
||||
"sha256:dcaaecdd5e847de5c1d533ea91522bf56c9e6b2dc98cdc0d45f0a1c26e846ea2",
|
||||
"sha256:e0403e095b343431195db1305248b50019ad55d3dd310254431af87e14ef83a2",
|
||||
"sha256:e20d7885a40e68a2bda92908acbabcdf3c14dd386c3845de73ba139e9df1f132",
|
||||
"sha256:e5bb396d63495667d4df42e506eed9d74fc9a51c99c173c04395fe7604c848f1",
|
||||
"sha256:e712a6d00606ad19abdeae852a7e521d6f6d0dcea843708fecf3a38be16a851e",
|
||||
"sha256:e7e7668f89fd598c5469bb58e16bfd12b511d9947ccc75aec94da31f62bc3758",
|
||||
"sha256:f0feb4f2b777fa6377e977faa89c26359d4f31953de15e035505b92f41aa6906",
|
||||
"sha256:f75973a42c710999acd419968bc79f00327e03e855bbe82c6529e003e49af660",
|
||||
"sha256:f766050e491d0b3203b6b85638015f543816a2eb7d089fc04e86e00f6de0e31d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.48.2"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
|
||||
"sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
|
||||
"sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
|
||||
"sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
|
||||
"sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
|
||||
"sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
|
||||
"sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
|
||||
"sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
|
||||
"sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
|
||||
"sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
|
||||
"sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
|
||||
"sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
|
||||
"sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
|
||||
"sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
|
||||
"sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
|
||||
"sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
|
||||
"sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
|
||||
"sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
|
||||
"sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
|
||||
"sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
|
||||
"sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
|
||||
"sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.20.3"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378",
|
||||
"sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==67.1.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
24
proto/generate_protobuf.py
Executable file → Normal file
24
proto/generate_protobuf.py
Executable file → Normal file
@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
if __name__ == '__main__':
|
||||
if 'PIPENV_ACTIVE' not in os.environ:
|
||||
sys.exit(f'This script should be run in a Pipenv.\n\nRun it as:\npipenv run python {os.path.basename(__file__)}')
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def run():
|
||||
SCRIPT_PATH = Path(__file__).absolute().parent
|
||||
@ -30,5 +31,20 @@ def run():
|
||||
subprocess.check_call(['python3', nanopb_generator_path, '-D', c_generated_output_path] + proto_files, cwd=proto_path)
|
||||
|
||||
|
||||
# Use nanopb's packaged protoc to generate python bindings
|
||||
protoc_path = nanopb_path / 'generator' / 'protoc'
|
||||
python_generated_output_path = REPO_ROOT / 'software' / 'python' / 'proto_gen'
|
||||
python_generated_output_path.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.check_call([protoc_path, '--version'])
|
||||
subprocess.check_call([
|
||||
protoc_path,
|
||||
'--python_out',
|
||||
python_generated_output_path,
|
||||
] + proto_files, cwd=proto_path)
|
||||
|
||||
# Copy nanopb's compiled options proto
|
||||
shutil.copy2(nanopb_path / 'generator' / 'proto' / 'nanopb_pb2.py', python_generated_output_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
|
||||
@ -5,16 +5,31 @@ import "nanopb.proto";
|
||||
package PB;
|
||||
|
||||
/*
|
||||
* Message FROM the SmartKnob to USB host
|
||||
* Message FROM the SmartKnob to the host
|
||||
*/
|
||||
message FromSmartKnob {
|
||||
uint32 protocol_version = 1 [(nanopb).int_size = IS_8];
|
||||
oneof payload {
|
||||
Ack ack = 1;
|
||||
Log log = 2;
|
||||
SmartKnobState smartknob_state = 3;
|
||||
Ack ack = 2;
|
||||
Log log = 3;
|
||||
SmartKnobState smartknob_state = 4;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Message TO the Smartknob from the host
|
||||
*/
|
||||
message ToSmartknob {
|
||||
uint32 protocol_version = 1 [(nanopb).int_size = IS_8];
|
||||
uint32 nonce = 2;
|
||||
|
||||
oneof payload {
|
||||
RequestState request_state = 3;
|
||||
SmartKnobConfig smartknob_config = 4;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lets the host know that a ToSmartknob message was received and should not be retried. */
|
||||
message Ack {
|
||||
uint32 nonce = 1;
|
||||
}
|
||||
@ -24,32 +39,183 @@ message Log {
|
||||
}
|
||||
|
||||
message SmartKnobState {
|
||||
/** Current integer position of the knob. (Detent resolution is at integer positions) */
|
||||
int32 current_position = 1;
|
||||
|
||||
/**
|
||||
* Current fractional position. Typically will only range from (-snap_point, snap_point)
|
||||
* since further rotation will result in the integer position changing, but may exceed
|
||||
* those values if snap_point_bias is non-zero, or if the knob is at a bound. When the
|
||||
* knob is at a bound, this value can grow endlessly as the knob is rotated further past
|
||||
* the bound.
|
||||
*
|
||||
* When visualizing sub_position_unit, you will likely want to apply a rubber-band easing
|
||||
* function past the bounds; a sublinear relationship will help suggest that a bound has
|
||||
* been reached.
|
||||
*/
|
||||
float sub_position_unit = 2;
|
||||
|
||||
/**
|
||||
* Current SmartKnobConfig in effect at the time of this State snapshot.
|
||||
*
|
||||
* Beware that this config contains position and sub_position_unit values, not to be
|
||||
* confused with the top level current_position and sub_position_unit values in this State
|
||||
* message. The position values in the embedded config message will almost never be useful
|
||||
* to you; you probably want to be reading the top level values from the State message.
|
||||
*/
|
||||
SmartKnobConfig config = 3;
|
||||
|
||||
/**
|
||||
* Value that changes each time the knob is pressed. Does not change when a press is released.
|
||||
*
|
||||
* Why this press state a "nonce" rather than a simple boolean representing the current
|
||||
* "pressed" state? It makes the protocol more robust to dropped/lost State messages; if
|
||||
* the knob was pressed/released quickly and State messages happened to be dropped during
|
||||
* that time, the press would be completely lost. Using a nonce allows the host to recognize
|
||||
* that a press has taken place at some point even if the State was lost during the press
|
||||
* itself. Is this overkill? Probably, let's revisit in future protocol versions.
|
||||
*/
|
||||
uint32 press_nonce = 4 [(nanopb).int_size = IS_8];
|
||||
}
|
||||
|
||||
/*
|
||||
* 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];
|
||||
/**
|
||||
* Set the integer position.
|
||||
*
|
||||
* Note: in order to make SmartKnobConfig apply idempotently, the current position
|
||||
* will only be set to this value when it changes compared to a previous config (and
|
||||
* NOT compared to the current state!). So by default, if you send a config position
|
||||
* of 5 and the current position is 3, the position may remain at 3 if the config
|
||||
* change to 5 was previously handled. If you need to force a position update, see
|
||||
* position_nonce.
|
||||
*/
|
||||
int32 position = 1;
|
||||
|
||||
/**
|
||||
* Set the fractional position. Typical range: (-snap_point, snap_point).
|
||||
*
|
||||
* Actual range is technically unbounded, but in practice this value will be compared
|
||||
* against snap_point on the next control loop, so any value beyond the snap_point will
|
||||
* generally result in an integer position change (unless position is already at a
|
||||
* limit).
|
||||
*
|
||||
* Note: idempotency implications noted in the documentation for `position` apply here
|
||||
* as well
|
||||
*/
|
||||
float sub_position_unit = 2;
|
||||
|
||||
/**
|
||||
* Position is normally only applied when it changes, but sometimes it's desirable
|
||||
* to reset the position to the same value, so a nonce change can be used to force
|
||||
* the position values to be applied as well.
|
||||
*
|
||||
* NOTE: Must be < 256
|
||||
*/
|
||||
uint32 position_nonce = 3 [(nanopb).int_size = IS_8];
|
||||
|
||||
/** Minimum position allowed. */
|
||||
int32 min_position = 4;
|
||||
|
||||
/**
|
||||
* Maximum position allowed.
|
||||
*
|
||||
* If this is the same as min_position, there will only be one allowed position.
|
||||
*
|
||||
* If this is less than min_position, bounds will be disabled.
|
||||
*/
|
||||
int32 max_position = 5;
|
||||
|
||||
/** The angular "width" of each position/detent, in radians. */
|
||||
float position_width_radians = 6;
|
||||
|
||||
/**
|
||||
* Strength of detents to apply. Typical range: [0, 1].
|
||||
*
|
||||
* A value of 0 disables detents.
|
||||
*
|
||||
* Values greater than 1 are not recommended and may lead to unstable behavior.
|
||||
*/
|
||||
float detent_strength_unit = 7;
|
||||
|
||||
/**
|
||||
* Strength of endstop torque to apply at min/max bounds. Typical range: [0, 1].
|
||||
*
|
||||
* A value of 0 disables endstop torque, but does not make position unbounded, meaning
|
||||
* the knob will not try to return to the valid region. For unbounded rotation, use
|
||||
* min_position and max_position.
|
||||
*
|
||||
* Values greater than 1 are not recommended and may lead to unstable behavior.
|
||||
*/
|
||||
float endstop_strength_unit = 8;
|
||||
|
||||
/**
|
||||
* Fractional (sub-position) threshold where the position will increment/decrement.
|
||||
* Typical range: (0.5, 1.5).
|
||||
*
|
||||
* This defines how hysteresis is applied to positions, which is why values >
|
||||
*/
|
||||
float snap_point = 9;
|
||||
|
||||
/**
|
||||
* Arbitrary 50-byte string representing this "config". This can be used to identify major
|
||||
* config/mode changes. The value will be echoed back to the host via a future State's
|
||||
* embedded config field so the host can use this value to determine the mode that was
|
||||
* in effect at the time of the State snapshot instead of having to infer it from the
|
||||
* other config fields.
|
||||
*/
|
||||
string text = 10 [(nanopb).max_length = 50];
|
||||
|
||||
/**
|
||||
* For a "magnetic" detent mode - where not all positions should have detents - this
|
||||
* specifies which positions (up to 5) have detents enabled. The knob will feel like it
|
||||
* is "magnetically" attracted to those positions, and will rotate smoothy past all
|
||||
* other positions.
|
||||
*
|
||||
* If you want to have more than 5 magnetic detent positions, you will need to dynamically
|
||||
* update this list as the knob is rotated. A recommended approach is to always send the
|
||||
* _nearest_ 5 detent positions, and send a new Config message whenever the list of
|
||||
* positions nearest the current position (as reported via State messages) changes.
|
||||
*
|
||||
* This approach enables effectively unbounded detent positions while keeping Config
|
||||
* bounded in size, and is resilient against tightly-packed detents with fast rotation
|
||||
* since multiple detent positions can be sent in advance; a full round-trip Config-State
|
||||
* isn't needed between each detent in order to keep up.
|
||||
*/
|
||||
repeated int32 detent_positions = 11 [(nanopb).max_count = 5];
|
||||
|
||||
/**
|
||||
* Advanced feature for shifting the defined snap_point away from the center (position 0)
|
||||
* for implementing asymmetric detents. Typical value: 0 (symmetric detent force).
|
||||
*
|
||||
* This can be used to create detents that will hold the position when carefully released,
|
||||
* but can be easily disturbed to return "home" towards position 0.
|
||||
*/
|
||||
float snap_point_bias = 12;
|
||||
|
||||
/**
|
||||
* Hue (0-255) for all 8 ring LEDs, if supported. Note: this will likely be replaced
|
||||
* with more configurability in a future protocol version.
|
||||
*/
|
||||
int32 led_hue = 13 [(nanopb).int_size = IS_16];
|
||||
}
|
||||
|
||||
message RequestState {}
|
||||
|
||||
message PersistentConfiguration {
|
||||
uint32 version = 1;
|
||||
MotorCalibration motor = 2;
|
||||
StrainCalibration strain = 3;
|
||||
}
|
||||
|
||||
message MotorCalibration {
|
||||
bool calibrated = 1;
|
||||
float zero_electrical_offset = 2;
|
||||
bool direction_cw = 3;
|
||||
uint32 pole_pairs = 4;
|
||||
}
|
||||
|
||||
message StrainCalibration {
|
||||
int32 idle_value = 1;
|
||||
int32 press_delta = 2;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Typescript SmartKnob protobuf interface library
|
||||
# Typescript SmartKnob protobuf interface library and examples
|
||||
|
||||
### Requirements (nvm is recommended)
|
||||
|
||||
@ -14,6 +14,7 @@ npm run build
|
||||
|
||||
### Example
|
||||
|
||||
A basic Node.js CLI example.
|
||||
Connect the SmartKnob via USB, then run the example:
|
||||
|
||||
```
|
||||
|
||||
33079
software/js/package-lock.json
generated
33079
software/js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,14 +9,21 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"example": "npm run -w example main"
|
||||
"example-cli": "npm -w example-cli run main",
|
||||
"example-webserial-basic": "npm -w example-webserial-basic run start",
|
||||
"example-webserial-timeline": "npm -w example-webserial-timeline run start"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"//": "NOTE: Workspaces listed in dependency order; intentionally not alphabetical!",
|
||||
"workspaces": [
|
||||
"packages/smartknobjs-proto",
|
||||
"packages/smartknobjs",
|
||||
"packages/example"
|
||||
"packages/smartknobjs-core",
|
||||
"packages/smartknobjs-node",
|
||||
"packages/smartknobjs-webserial",
|
||||
"packages/example-cli",
|
||||
"packages/example-webserial-basic",
|
||||
"packages/example-webserial-timeline"
|
||||
],
|
||||
"devDependencies": {
|
||||
"eslint-config-prettier": "^8.5.0"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "example",
|
||||
"name": "example-cli",
|
||||
"version": "0.1.0",
|
||||
"description": "SmartKnob Interface Library",
|
||||
"description": "SmartKnob Interface Example",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
@ -14,7 +14,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"serialport": "^9.2.4",
|
||||
"smartknobjs": "^0.1.0"
|
||||
"smartknobjs-proto": "^1.0.0",
|
||||
"smartknobjs-webserial": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/serialport": "^8.0.2",
|
||||
@ -1,16 +1,14 @@
|
||||
import SerialPort = require('serialport')
|
||||
import {SmartKnob} from 'smartknobjs'
|
||||
import {SmartKnobNode} from 'smartknobjs-node'
|
||||
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?.toLowerCase() === '1a86'.toLowerCase() && portInfo.productId?.toLowerCase() === '7523'.toLowerCase()
|
||||
// && portInfo.serialNumber === 'DEADBEEF'
|
||||
return SmartKnobNode.USB_DEVICE_FILTERS.some(
|
||||
(f) =>
|
||||
f.usbVendorId.toString(16) === portInfo.vendorId && f.usbProductId.toString(16) === portInfo.productId,
|
||||
)
|
||||
})
|
||||
|
||||
@ -18,20 +16,27 @@ const main = async () => {
|
||||
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)}`)
|
||||
console.error(
|
||||
`Multiple possible 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) => {
|
||||
const smartknob = new SmartKnobNode(portInfo.path, (message: PB.FromSmartKnob) => {
|
||||
if (message.payload === 'log' && message.log) {
|
||||
console.log('LOG', message.log.msg)
|
||||
} else if (message.payload === 'smartknobState' && message.smartknobState) {
|
||||
// Only log if it's a significant change (major position change, or at least 5 degrees)
|
||||
const radianChange =
|
||||
(message.smartknobState.subPositionUnit ?? 0) *
|
||||
(message.smartknobState.config?.positionWidthRadians ?? 0) -
|
||||
(lastLoggedState?.subPositionUnit ?? 0) * (lastLoggedState?.config?.positionWidthRadians ?? 0)
|
||||
if (
|
||||
message.smartknobState.currentPosition !== lastLoggedState?.currentPosition ||
|
||||
Math.abs((message.smartknobState.subPositionUnit ?? 0) - (lastLoggedState?.subPositionUnit ?? 0)) > 1
|
||||
(Math.abs(radianChange) * 180) / Math.PI > 5
|
||||
) {
|
||||
console.log(
|
||||
`State:\n${JSON.stringify(
|
||||
@ -48,8 +53,11 @@ const main = async () => {
|
||||
PB.SmartKnobConfig.create({
|
||||
detentStrengthUnit: 1,
|
||||
endstopStrengthUnit: 1,
|
||||
numPositions: 5,
|
||||
position: 0,
|
||||
subPositionUnit: 0,
|
||||
positionNonce: Math.floor(Math.random() * 255), // Pick a random nonce to force a position reset on start
|
||||
minPosition: 0,
|
||||
maxPosition: 4,
|
||||
positionWidthRadians: (10 * Math.PI) / 180,
|
||||
snapPoint: 1.1,
|
||||
text: 'From TS!',
|
||||
23
software/js/packages/example-webserial-basic/.gitignore
vendored
Normal file
23
software/js/packages/example-webserial-basic/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
46
software/js/packages/example-webserial-basic/README.md
Normal file
46
software/js/packages/example-webserial-basic/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
55
software/js/packages/example-webserial-basic/package.json
Normal file
55
software/js/packages/example-webserial-basic/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "example-webserial-basic",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@mui/material": "^5.10.16",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.4",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"smartknobjs-proto": "^1.0.0",
|
||||
"smartknobjs-webserial": "^1.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3000 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3001",
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/w3c-web-serial": "^1.0.3"
|
||||
}
|
||||
}
|
||||
BIN
software/js/packages/example-webserial-basic/public/favicon.ico
Normal file
BIN
software/js/packages/example-webserial-basic/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Interactive SmartKnob demo using Web Serial" />
|
||||
<title>SmartKnob Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
303
software/js/packages/example-webserial-basic/src/App.tsx
Normal file
303
software/js/packages/example-webserial-basic/src/App.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Container from '@mui/material/Container'
|
||||
import {PB} from 'smartknobjs-proto'
|
||||
import {Box, Button, CardActions, Paper, TextField} from '@mui/material'
|
||||
import {NoUndefinedField} from './util'
|
||||
import {SmartKnobWebSerial} from 'smartknobjs-webserial'
|
||||
|
||||
type Config = NoUndefinedField<PB.ISmartKnobConfig>
|
||||
|
||||
const defaultConfig: Config = {
|
||||
position: 0,
|
||||
subPositionUnit: 0,
|
||||
positionNonce: Math.floor(Math.random() * 255),
|
||||
minPosition: 0,
|
||||
maxPosition: 20,
|
||||
positionWidthRadians: (15 * Math.PI) / 180,
|
||||
detentStrengthUnit: 0.5,
|
||||
endstopStrengthUnit: 1,
|
||||
snapPoint: 0.7,
|
||||
text: 'Hello from\nweb serial!',
|
||||
detentPositions: [],
|
||||
snapPointBias: 0,
|
||||
ledHue: 0,
|
||||
}
|
||||
|
||||
export type AppProps = object
|
||||
export const App: React.FC<AppProps> = () => {
|
||||
const [smartKnob, setSmartKnob] = useState<SmartKnobWebSerial | null>(null)
|
||||
const [smartKnobState, setSmartKnobState] = useState<NoUndefinedField<PB.ISmartKnobState>>(
|
||||
PB.SmartKnobState.toObject(PB.SmartKnobState.create({config: PB.SmartKnobConfig.create()}), {
|
||||
defaults: true,
|
||||
}) as NoUndefinedField<PB.ISmartKnobState>,
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [smartKnobConfig, setSmartKnobConfig] = useState<Config>(defaultConfig)
|
||||
useEffect(() => {
|
||||
console.log('send config', smartKnobConfig)
|
||||
smartKnob?.sendConfig(PB.SmartKnobConfig.create(smartKnobConfig))
|
||||
}, [
|
||||
smartKnob,
|
||||
smartKnobConfig.position,
|
||||
smartKnobConfig.subPositionUnit,
|
||||
smartKnobConfig.positionNonce,
|
||||
smartKnobConfig.minPosition,
|
||||
smartKnobConfig.maxPosition,
|
||||
smartKnobConfig.positionWidthRadians,
|
||||
smartKnobConfig.detentStrengthUnit,
|
||||
smartKnobConfig.endstopStrengthUnit,
|
||||
smartKnobConfig.snapPoint,
|
||||
smartKnobConfig.text,
|
||||
smartKnobConfig.detentPositions,
|
||||
smartKnobConfig.snapPointBias,
|
||||
])
|
||||
const [pendingSmartKnobConfig, setPendingSmartKnobConfig] = useState<{[P in keyof Config]: string}>(() => {
|
||||
return Object.fromEntries(Object.entries(defaultConfig).map(([key, value]) => [key, String(value)])) as {
|
||||
[P in keyof Config]: string
|
||||
}
|
||||
})
|
||||
|
||||
const connectToSerial = async () => {
|
||||
try {
|
||||
if (navigator.serial) {
|
||||
const serialPort = await navigator.serial.requestPort({
|
||||
filters: SmartKnobWebSerial.USB_DEVICE_FILTERS,
|
||||
})
|
||||
serialPort.addEventListener('disconnect', () => {
|
||||
setSmartKnob(null)
|
||||
})
|
||||
const smartKnob = new SmartKnobWebSerial(serialPort, (message) => {
|
||||
if (message.payload === 'smartknobState' && message.smartknobState !== null) {
|
||||
const state = PB.SmartKnobState.create(message.smartknobState)
|
||||
const stateObj = PB.SmartKnobState.toObject(state, {
|
||||
defaults: true,
|
||||
}) as NoUndefinedField<PB.ISmartKnobState>
|
||||
setSmartKnobState(stateObj)
|
||||
} else if (message.payload === 'log' && message.log !== null) {
|
||||
console.log('LOG from smartknob', message.log?.msg)
|
||||
}
|
||||
})
|
||||
setSmartKnob(smartKnob)
|
||||
const loop = smartKnob.openAndLoop()
|
||||
console.log('FIXME')
|
||||
smartKnob.sendConfig(PB.SmartKnobConfig.create(smartKnobConfig))
|
||||
await loop
|
||||
} else {
|
||||
console.error('Web Serial API is not supported in this browser.')
|
||||
setSmartKnob(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with serial port:', error)
|
||||
setSmartKnob(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container component="main" maxWidth="lg">
|
||||
<Paper variant="outlined" sx={{my: {xs: 3, md: 6}, p: {xs: 2, md: 3}}}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Basic SmartKnob Web Serial Demo
|
||||
</Typography>
|
||||
{smartKnob !== null ? (
|
||||
<>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
'& .MuiTextField-root': {m: 1, width: '25ch'},
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
setSmartKnobConfig({
|
||||
position: parseInt(pendingSmartKnobConfig.position) || 0,
|
||||
subPositionUnit: parseFloat(pendingSmartKnobConfig.subPositionUnit) || 0,
|
||||
positionNonce: parseInt(pendingSmartKnobConfig.positionNonce) || 0,
|
||||
minPosition: parseInt(pendingSmartKnobConfig.minPosition) || 0,
|
||||
maxPosition: parseInt(pendingSmartKnobConfig.maxPosition) || 0,
|
||||
positionWidthRadians:
|
||||
parseFloat(pendingSmartKnobConfig.positionWidthRadians) || 0,
|
||||
detentStrengthUnit: parseFloat(pendingSmartKnobConfig.detentStrengthUnit) || 0,
|
||||
endstopStrengthUnit:
|
||||
parseFloat(pendingSmartKnobConfig.endstopStrengthUnit) || 0,
|
||||
snapPoint: parseFloat(pendingSmartKnobConfig.snapPoint) || 0,
|
||||
text: pendingSmartKnobConfig.text,
|
||||
detentPositions: [],
|
||||
snapPointBias: parseFloat(pendingSmartKnobConfig.snapPointBias) || 0,
|
||||
ledHue: 0,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Position"
|
||||
value={pendingSmartKnobConfig.position}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
position: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Sub-position unit"
|
||||
value={pendingSmartKnobConfig.subPositionUnit}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
subPositionUnit: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Position nonce"
|
||||
value={pendingSmartKnobConfig.positionNonce}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
positionNonce: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
label="Min position"
|
||||
value={pendingSmartKnobConfig.minPosition}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
minPosition: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Max position"
|
||||
value={pendingSmartKnobConfig.maxPosition}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
maxPosition: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
label="Position width (radians)"
|
||||
value={pendingSmartKnobConfig.positionWidthRadians}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
positionWidthRadians: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
label="Detent strength unit"
|
||||
value={pendingSmartKnobConfig.detentStrengthUnit}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
detentStrengthUnit: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Endstop strength unit"
|
||||
value={pendingSmartKnobConfig.endstopStrengthUnit}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
endstopStrengthUnit: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
label="Snap point"
|
||||
value={pendingSmartKnobConfig.snapPoint}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
snapPoint: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Snap point bias"
|
||||
value={pendingSmartKnobConfig.snapPointBias}
|
||||
type="number"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
snapPointBias: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
label="Text"
|
||||
value={pendingSmartKnobConfig.text}
|
||||
multiline
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPendingSmartKnobConfig((cur) => {
|
||||
return {
|
||||
...cur,
|
||||
text: event.target.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Button type="submit" variant="contained">
|
||||
Apply
|
||||
</Button>
|
||||
</Box>
|
||||
<pre>{JSON.stringify(smartKnobState, undefined, 2)}</pre>
|
||||
</>
|
||||
) : navigator.serial ? (
|
||||
<CardActions>
|
||||
<Button onClick={connectToSerial} variant="contained">
|
||||
Connect via Web Serial
|
||||
</Button>
|
||||
</CardActions>
|
||||
) : (
|
||||
<Typography>
|
||||
Sorry, Web Serial API isn't available in your browser. Try the latest version of Chrome.
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
22
software/js/packages/example-webserial-basic/src/index.tsx
Normal file
22
software/js/packages/example-webserial-basic/src/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {App} from './App'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/500.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import {createTheme, ThemeProvider} from '@mui/material/styles'
|
||||
|
||||
const theme = createTheme()
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
1
software/js/packages/example-webserial-basic/src/react-app-env.d.ts
vendored
Normal file
1
software/js/packages/example-webserial-basic/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
29
software/js/packages/example-webserial-basic/src/util.ts
Normal file
29
software/js/packages/example-webserial-basic/src/util.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export const exhaustiveCheck = (x: never): never => {
|
||||
throw new Error("Didn't expect to get here", x)
|
||||
}
|
||||
|
||||
export const isSome = <T>(v: T | null | undefined): v is T => {
|
||||
return v !== null && v !== undefined
|
||||
}
|
||||
|
||||
export const lerp = (value: number, inMin: number, inMax: number, min: number, max: number): number => {
|
||||
// Map the input value from the input range to the output range
|
||||
value = ((value - inMin) / (inMax - inMin)) * (max - min) + min
|
||||
|
||||
// Clamp the mapped value between the minimum and maximum range
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
export type NoUndefinedField<T> = {
|
||||
[P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>
|
||||
}
|
||||
|
||||
export function findNClosest(numbers: number[], target: number, n: number): number[] {
|
||||
// First, we sort the numbers in ascending order based on their absolute difference
|
||||
// from the target number. This means that the numbers that are closest to the target
|
||||
// will come first in the sorted array.
|
||||
const sortedNumbers = numbers.sort((a, b) => Math.abs(a - target) - Math.abs(b - target))
|
||||
|
||||
// Next, we return the first N numbers from the sorted array as the N closest numbers.
|
||||
return sortedNumbers.slice(0, n)
|
||||
}
|
||||
20
software/js/packages/example-webserial-basic/tsconfig.json
Normal file
20
software/js/packages/example-webserial-basic/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
19
software/js/packages/example-webserial-timeline/.eslintrc
Normal file
19
software/js/packages/example-webserial-timeline/.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
|
||||
}
|
||||
}
|
||||
23
software/js/packages/example-webserial-timeline/.gitignore
vendored
Normal file
23
software/js/packages/example-webserial-timeline/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
10
software/js/packages/example-webserial-timeline/.prettierrc
Normal file
10
software/js/packages/example-webserial-timeline/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
46
software/js/packages/example-webserial-timeline/README.md
Normal file
46
software/js/packages/example-webserial-timeline/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
55
software/js/packages/example-webserial-timeline/package.json
Normal file
55
software/js/packages/example-webserial-timeline/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "example-webserial-timeline",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@mui/material": "^5.10.16",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.4",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"smartknobjs-proto": "^1.0.0",
|
||||
"smartknobjs-webserial": "^1.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3000 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3001",
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/w3c-web-serial": "^1.0.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Interactive SmartKnob demo using Web Serial" />
|
||||
<title>SmartKnob Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
555
software/js/packages/example-webserial-timeline/src/App.tsx
Normal file
555
software/js/packages/example-webserial-timeline/src/App.tsx
Normal file
@ -0,0 +1,555 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Container from '@mui/material/Container'
|
||||
import ToggleButton from '@mui/material/ToggleButton'
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||
import {PB} from 'smartknobjs-proto'
|
||||
import {VideoInfo} from './types'
|
||||
import {Button, CardActions, Paper} from '@mui/material'
|
||||
import {exhaustiveCheck, findNClosest, lerp, NoUndefinedField} from './util'
|
||||
import {groupBy, parseInt} from 'lodash'
|
||||
import _ from 'lodash'
|
||||
import {SmartKnobWebSerial} from 'smartknobjs-webserial'
|
||||
|
||||
const MIN_ZOOM = 0.01
|
||||
const MAX_ZOOM = 60
|
||||
|
||||
const PIXELS_PER_POSITION = 5
|
||||
|
||||
enum Mode {
|
||||
Scroll = 'SKDEMO_Scroll',
|
||||
Frames = 'SKDEMO_Frames',
|
||||
Speed = 'SKDEMO_Speed',
|
||||
}
|
||||
|
||||
type PlaybackState = {
|
||||
speed: number
|
||||
currentFrame: number
|
||||
}
|
||||
|
||||
type InterfaceState = {
|
||||
zoomTimelinePixelsPerFrame: number
|
||||
}
|
||||
|
||||
type Config = NoUndefinedField<PB.ISmartKnobConfig> & {
|
||||
zoomTimelinePixelsPerFrame: number
|
||||
}
|
||||
|
||||
export type AppProps = {
|
||||
info: VideoInfo
|
||||
}
|
||||
export const App: React.FC<AppProps> = ({info}) => {
|
||||
const [smartKnob, setSmartKnob] = useState<SmartKnobWebSerial | null>(null)
|
||||
const [smartKnobState, setSmartKnobState] = useState<NoUndefinedField<PB.ISmartKnobState>>(
|
||||
PB.SmartKnobState.toObject(PB.SmartKnobState.create({config: PB.SmartKnobConfig.create()}), {
|
||||
defaults: true,
|
||||
}) as NoUndefinedField<PB.ISmartKnobState>,
|
||||
)
|
||||
const [smartKnobConfig, setSmartKnobConfig] = useState<Config>({
|
||||
position: 0,
|
||||
subPositionUnit: 0,
|
||||
positionNonce: Math.floor(Math.random() * 255),
|
||||
minPosition: 0,
|
||||
maxPosition: 0,
|
||||
positionWidthRadians: (15 * Math.PI) / 180,
|
||||
detentStrengthUnit: 0,
|
||||
endstopStrengthUnit: 1,
|
||||
snapPoint: 0.7,
|
||||
text: Mode.Scroll,
|
||||
detentPositions: [],
|
||||
snapPointBias: 0,
|
||||
ledHue: 0,
|
||||
|
||||
zoomTimelinePixelsPerFrame: 0.1,
|
||||
})
|
||||
useEffect(() => {
|
||||
console.log('send config', smartKnobConfig)
|
||||
smartKnob?.sendConfig(PB.SmartKnobConfig.create(smartKnobConfig))
|
||||
}, [
|
||||
smartKnob,
|
||||
smartKnobConfig.position,
|
||||
smartKnobConfig.subPositionUnit,
|
||||
smartKnobConfig.positionNonce,
|
||||
smartKnobConfig.minPosition,
|
||||
smartKnobConfig.maxPosition,
|
||||
smartKnobConfig.positionWidthRadians,
|
||||
smartKnobConfig.detentStrengthUnit,
|
||||
smartKnobConfig.endstopStrengthUnit,
|
||||
smartKnobConfig.snapPoint,
|
||||
smartKnobConfig.text,
|
||||
smartKnobConfig.detentPositions,
|
||||
smartKnobConfig.snapPointBias,
|
||||
smartKnobConfig.ledHue,
|
||||
])
|
||||
const [playbackState, setPlaybackState] = useState<PlaybackState>({
|
||||
speed: 0,
|
||||
currentFrame: 0,
|
||||
})
|
||||
const [interfaceState, setInterfaceState] = useState<InterfaceState>({
|
||||
zoomTimelinePixelsPerFrame: 0.3,
|
||||
})
|
||||
|
||||
const totalPositions = Math.ceil(
|
||||
(info.totalFrames * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
|
||||
)
|
||||
const detentPositions = useMemo(() => {
|
||||
// Always include the first and last positions at detents
|
||||
const positionsToFrames = groupBy([0, ...info.boundaryFrames, info.totalFrames - 1], (frame) =>
|
||||
Math.round((frame * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION),
|
||||
)
|
||||
console.log(JSON.stringify(positionsToFrames))
|
||||
return positionsToFrames
|
||||
}, [info.boundaryFrames, info.totalFrames, totalPositions, smartKnobConfig.zoomTimelinePixelsPerFrame])
|
||||
|
||||
const scrollPositionWholeMemo = useMemo(() => {
|
||||
const position = (playbackState.currentFrame * smartKnobConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
|
||||
return Math.round(position)
|
||||
}, [playbackState.currentFrame, smartKnobConfig.zoomTimelinePixelsPerFrame])
|
||||
const [nClosest, setNClosest] = useState<Array<number>>([])
|
||||
useEffect(() => {
|
||||
const calculated = findNClosest(Object.keys(detentPositions).map(parseInt), scrollPositionWholeMemo, 5).sort(
|
||||
(a, b) => a - b,
|
||||
)
|
||||
setNClosest((cur) => {
|
||||
if (_.isEqual(cur, calculated)) {
|
||||
return cur
|
||||
}
|
||||
return calculated
|
||||
})
|
||||
}, [scrollPositionWholeMemo])
|
||||
|
||||
const changeMode = useCallback(
|
||||
(newMode: Mode) => {
|
||||
if (newMode === Mode.Scroll) {
|
||||
setSmartKnobConfig((curConfig) => {
|
||||
const position =
|
||||
(playbackState.currentFrame * curConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
|
||||
const positionWhole = Math.round(position)
|
||||
const subPositionUnit = position - positionWhole
|
||||
return {
|
||||
position: positionWhole,
|
||||
subPositionUnit,
|
||||
positionNonce: (curConfig.positionNonce + 1) % 256,
|
||||
minPosition: 0,
|
||||
maxPosition: Math.round(
|
||||
((info.totalFrames - 1) * curConfig.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
|
||||
),
|
||||
positionWidthRadians: (8 * Math.PI) / 180,
|
||||
detentStrengthUnit: 3,
|
||||
endstopStrengthUnit: 1,
|
||||
snapPoint: 0.7,
|
||||
text: Mode.Scroll,
|
||||
detentPositions: findNClosest(Object.keys(detentPositions).map(parseInt), position, 5),
|
||||
snapPointBias: 0,
|
||||
ledHue: 0,
|
||||
|
||||
zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
|
||||
}
|
||||
})
|
||||
} else if (newMode === Mode.Frames) {
|
||||
setSmartKnobConfig((curConfig) => {
|
||||
return {
|
||||
position: Math.floor(playbackState.currentFrame),
|
||||
subPositionUnit: 0,
|
||||
positionNonce: (curConfig.positionNonce + 1) % 256,
|
||||
minPosition: 0,
|
||||
maxPosition: info.totalFrames - 1,
|
||||
positionWidthRadians: (1.8 * Math.PI) / 180,
|
||||
detentStrengthUnit: 1,
|
||||
endstopStrengthUnit: 1,
|
||||
snapPoint: 1.1,
|
||||
text: Mode.Frames,
|
||||
detentPositions: [],
|
||||
snapPointBias: 0,
|
||||
ledHue: (120 * 255) / 360,
|
||||
|
||||
zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
|
||||
}
|
||||
})
|
||||
} else if (newMode === Mode.Speed) {
|
||||
setSmartKnobConfig((curConfig) => {
|
||||
return {
|
||||
position: 0,
|
||||
subPositionUnit: 0,
|
||||
positionNonce: (curConfig.positionNonce + 1) % 256,
|
||||
minPosition: playbackState.currentFrame === 0 ? 0 : -6,
|
||||
maxPosition: playbackState.currentFrame === info.totalFrames - 1 ? 0 : 6,
|
||||
positionWidthRadians: (30 * Math.PI) / 180,
|
||||
detentStrengthUnit: 1,
|
||||
endstopStrengthUnit: 1,
|
||||
snapPoint: 0.5,
|
||||
text: Mode.Speed,
|
||||
detentPositions: [],
|
||||
snapPointBias: 0.4,
|
||||
ledHue: (240 * 255) / 360,
|
||||
|
||||
zoomTimelinePixelsPerFrame: curConfig.zoomTimelinePixelsPerFrame,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
exhaustiveCheck(newMode)
|
||||
}
|
||||
},
|
||||
[detentPositions, info.totalFrames, playbackState],
|
||||
)
|
||||
const nextMode = useCallback(() => {
|
||||
const curMode = smartKnobConfig.text as unknown as Mode
|
||||
console.log('nextMode', curMode)
|
||||
if (curMode === Mode.Scroll) {
|
||||
changeMode(Mode.Frames)
|
||||
} else if (curMode === Mode.Frames) {
|
||||
changeMode(Mode.Speed)
|
||||
} else if (curMode === Mode.Speed) {
|
||||
changeMode(Mode.Scroll)
|
||||
} else {
|
||||
exhaustiveCheck(curMode)
|
||||
}
|
||||
}, [smartKnobConfig.text, changeMode])
|
||||
|
||||
// Initialize to Scroll mode
|
||||
useEffect(() => {
|
||||
changeMode(Mode.Scroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (smartKnobState.config.text === '') {
|
||||
console.debug('No valid state yet')
|
||||
return
|
||||
}
|
||||
|
||||
const currentMode = smartKnobState.config.text as Mode
|
||||
if (currentMode !== smartKnobConfig.text) {
|
||||
console.debug('Mode mismatch, ignoring state', {configMode: smartKnobConfig.text, stateMode: currentMode})
|
||||
return
|
||||
}
|
||||
|
||||
// Update playbackState
|
||||
if (currentMode === Mode.Scroll) {
|
||||
// TODO: round input based on zoom level to avoid noise
|
||||
const rawFrame = Math.trunc(
|
||||
((smartKnobState.currentPosition + smartKnobState.subPositionUnit) * PIXELS_PER_POSITION) /
|
||||
smartKnobConfig.zoomTimelinePixelsPerFrame,
|
||||
)
|
||||
const frame =
|
||||
detentPositions[smartKnobState.currentPosition]?.[0] ??
|
||||
Math.min(Math.max(rawFrame, 0), info.totalFrames - 1)
|
||||
setPlaybackState({
|
||||
speed: 0,
|
||||
currentFrame: frame,
|
||||
})
|
||||
|
||||
// Update config with N nearest detents
|
||||
setSmartKnobConfig((curConfig) => {
|
||||
let positionInfo: Partial<Config> = {}
|
||||
if (interfaceState.zoomTimelinePixelsPerFrame !== curConfig.zoomTimelinePixelsPerFrame) {
|
||||
const position =
|
||||
(playbackState.currentFrame * interfaceState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION
|
||||
const positionWhole = Math.round(position)
|
||||
const subPositionUnit = position - positionWhole
|
||||
positionInfo = {
|
||||
position,
|
||||
subPositionUnit,
|
||||
positionNonce: (curConfig.positionNonce + 1) % 256,
|
||||
minPosition: 0,
|
||||
maxPosition: Math.round(
|
||||
((info.totalFrames - 1) * interfaceState.zoomTimelinePixelsPerFrame) / PIXELS_PER_POSITION,
|
||||
),
|
||||
zoomTimelinePixelsPerFrame: interfaceState.zoomTimelinePixelsPerFrame,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...curConfig,
|
||||
...positionInfo,
|
||||
detentPositions: nClosest,
|
||||
}
|
||||
})
|
||||
} else if (currentMode === Mode.Frames) {
|
||||
setPlaybackState({
|
||||
speed: 0,
|
||||
currentFrame: smartKnobState.currentPosition,
|
||||
})
|
||||
// No config updates needed
|
||||
} else if (currentMode === Mode.Speed) {
|
||||
const normalizedWholeValue = smartKnobState.currentPosition
|
||||
const normalizedFractional =
|
||||
Math.sign(smartKnobState.subPositionUnit) *
|
||||
lerp(smartKnobState.subPositionUnit * Math.sign(smartKnobState.subPositionUnit), 0.1, 0.9, 0, 1)
|
||||
const normalized = normalizedWholeValue + normalizedFractional
|
||||
const speed = Math.sign(normalized) * Math.pow(2, Math.abs(normalized) - 1)
|
||||
const roundedSpeed = Math.trunc(speed * 10) / 10
|
||||
setPlaybackState((cur) => {
|
||||
return {
|
||||
speed: roundedSpeed,
|
||||
currentFrame: cur.currentFrame,
|
||||
}
|
||||
})
|
||||
|
||||
// Update config with bounds depending on current frame
|
||||
setSmartKnobConfig((curConfig) => {
|
||||
return {
|
||||
...curConfig,
|
||||
minPosition: playbackState.currentFrame === 0 ? 0 : -6,
|
||||
maxPosition: playbackState.currentFrame === info.totalFrames - 1 ? 0 : 6,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
exhaustiveCheck(currentMode)
|
||||
}
|
||||
}, [
|
||||
detentPositions,
|
||||
nClosest,
|
||||
info.totalFrames,
|
||||
smartKnobState.config.text,
|
||||
smartKnobState.currentPosition,
|
||||
smartKnobState.subPositionUnit,
|
||||
smartKnobConfig.text,
|
||||
playbackState.currentFrame,
|
||||
playbackState.speed,
|
||||
interfaceState.zoomTimelinePixelsPerFrame,
|
||||
])
|
||||
|
||||
// Change mode when pressed
|
||||
const receivedPressNonceRef = useRef<boolean>(false)
|
||||
const previousPressNonceRef = useRef<number>(0)
|
||||
useEffect(() => {
|
||||
if (previousPressNonceRef.current !== smartKnobState.pressNonce) {
|
||||
if (!receivedPressNonceRef.current) {
|
||||
// Ignore first nonce change
|
||||
receivedPressNonceRef.current = true
|
||||
} else {
|
||||
nextMode()
|
||||
}
|
||||
}
|
||||
previousPressNonceRef.current = smartKnobState.pressNonce
|
||||
}, [smartKnobState.pressNonce, nextMode])
|
||||
|
||||
const refreshInterval = 20
|
||||
const updateFrame = useCallback(() => {
|
||||
const fps = info.frameRate * playbackState.speed
|
||||
setPlaybackState((cur) => {
|
||||
const newFrame = cur.currentFrame + (fps * refreshInterval) / 1000
|
||||
const clampedNewFrame = Math.min(Math.max(newFrame, 0), info.totalFrames - 1)
|
||||
return {
|
||||
speed: cur.speed,
|
||||
currentFrame: clampedNewFrame,
|
||||
}
|
||||
})
|
||||
}, [info.frameRate, playbackState.speed])
|
||||
|
||||
// Store the latest callback in a ref so the long-lived interval closure can invoke the latest version.
|
||||
// See https://overreacted.io/making-setinterval-declarative-with-react-hooks/ for more
|
||||
const savedCallback = useRef<() => void | null>()
|
||||
useEffect(() => {
|
||||
savedCallback.current = updateFrame
|
||||
}, [updateFrame])
|
||||
|
||||
const isPlaying = useMemo(() => {
|
||||
return playbackState.speed !== 0
|
||||
}, [playbackState.speed])
|
||||
|
||||
useEffect(() => {
|
||||
if (smartKnobState.config.text === Mode.Speed && isPlaying) {
|
||||
const timer = setInterval(() => {
|
||||
if (savedCallback.current) {
|
||||
savedCallback.current()
|
||||
}
|
||||
}, refreshInterval)
|
||||
return () => clearInterval(timer)
|
||||
}
|
||||
}, [smartKnobState.config.text, isPlaying])
|
||||
|
||||
const connectToSerial = async () => {
|
||||
try {
|
||||
if (navigator.serial) {
|
||||
previousPressNonceRef.current = 0
|
||||
receivedPressNonceRef.current = false
|
||||
|
||||
const serialPort = await navigator.serial.requestPort({
|
||||
filters: SmartKnobWebSerial.USB_DEVICE_FILTERS,
|
||||
})
|
||||
serialPort.addEventListener('disconnect', () => {
|
||||
setSmartKnob(null)
|
||||
})
|
||||
const smartKnob = new SmartKnobWebSerial(serialPort, (message) => {
|
||||
if (message.payload === 'smartknobState' && message.smartknobState !== null) {
|
||||
const state = PB.SmartKnobState.create(message.smartknobState)
|
||||
const stateObj = PB.SmartKnobState.toObject(state, {
|
||||
defaults: true,
|
||||
}) as NoUndefinedField<PB.ISmartKnobState>
|
||||
setSmartKnobState(stateObj)
|
||||
} else if (message.payload === 'log' && message.log !== null) {
|
||||
console.log('LOG from smartknob', message.log?.msg)
|
||||
}
|
||||
})
|
||||
setSmartKnob(smartKnob)
|
||||
await smartKnob.openAndLoop()
|
||||
} else {
|
||||
console.error('Web Serial API is not supported in this browser.')
|
||||
setSmartKnob(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with serial port:', error)
|
||||
setSmartKnob(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container component="main" maxWidth="lg">
|
||||
<Paper variant="outlined" sx={{my: {xs: 3, md: 6}, p: {xs: 2, md: 3}}}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Video Playback Control Demo
|
||||
</Typography>
|
||||
{smartKnob !== null ? (
|
||||
<>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={smartKnobConfig.text}
|
||||
exclusive
|
||||
onChange={(e, value: Mode | null) => {
|
||||
if (value === null) {
|
||||
return
|
||||
}
|
||||
changeMode(value)
|
||||
}}
|
||||
aria-label="Mode"
|
||||
>
|
||||
{Object.entries(Mode).map((mode_entry) => (
|
||||
<ToggleButton value={mode_entry[1]} key={mode_entry[1]}>
|
||||
{mode_entry[0]}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
<Typography>
|
||||
Frame {Math.trunc(playbackState.currentFrame)} / {info.totalFrames - 1}
|
||||
<br />
|
||||
Speed {playbackState.speed}
|
||||
</Typography>
|
||||
<Timeline
|
||||
info={info}
|
||||
currentFrame={playbackState.currentFrame}
|
||||
zoomTimelinePixelsPerFrame={interfaceState.zoomTimelinePixelsPerFrame}
|
||||
adjustZoom={(factor) => {
|
||||
setInterfaceState((cur) => {
|
||||
const newZoom = Math.min(
|
||||
Math.max(cur.zoomTimelinePixelsPerFrame * factor, MIN_ZOOM),
|
||||
MAX_ZOOM,
|
||||
)
|
||||
console.log(factor, newZoom)
|
||||
return {
|
||||
...cur,
|
||||
zoomTimelinePixelsPerFrame: newZoom,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : navigator.serial ? (
|
||||
<CardActions>
|
||||
<Button onClick={connectToSerial} variant="contained">
|
||||
Connect via Web Serial
|
||||
</Button>
|
||||
</CardActions>
|
||||
) : (
|
||||
<Typography>
|
||||
Sorry, Web Serial API isn't available in your browser. Try the latest version of Chrome.
|
||||
</Typography>
|
||||
)}
|
||||
<pre>{JSON.stringify(smartKnobConfig, undefined, 2)}</pre>
|
||||
</Paper>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type TimelineProps = {
|
||||
info: VideoInfo
|
||||
currentFrame: number
|
||||
zoomTimelinePixelsPerFrame: number
|
||||
adjustZoom: (factor: number) => void
|
||||
}
|
||||
export const Timeline: React.FC<TimelineProps> = ({info, currentFrame, zoomTimelinePixelsPerFrame, adjustZoom}) => {
|
||||
const gradients = [
|
||||
'linear-gradient( 135deg, #FDEB71 10%, #F8D800 100%)',
|
||||
'linear-gradient( 135deg, #ABDCFF 10%, #0396FF 100%)',
|
||||
'linear-gradient( 135deg, #FEB692 10%, #EA5455 100%)',
|
||||
'linear-gradient( 135deg, #CE9FFC 10%, #7367F0 100%)',
|
||||
'linear-gradient( 135deg, #90F7EC 10%, #32CCBC 100%)',
|
||||
]
|
||||
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
const cursorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleWheel = (event: HTMLElementEventMap['wheel']) => {
|
||||
const delta = event.deltaY
|
||||
if (delta) {
|
||||
event.preventDefault()
|
||||
adjustZoom(1 - delta / 500)
|
||||
}
|
||||
}
|
||||
|
||||
timelineRef.current?.addEventListener('wheel', handleWheel)
|
||||
return () => {
|
||||
timelineRef.current?.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
cursorRef.current?.scrollIntoView()
|
||||
}, [currentFrame, zoomTimelinePixelsPerFrame])
|
||||
return (
|
||||
<div
|
||||
className="timeline-container"
|
||||
ref={timelineRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
margin: '10px auto',
|
||||
overflowX: 'scroll',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="timeline"
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
height: '80px',
|
||||
width: `${zoomTimelinePixelsPerFrame * info.totalFrames}px`,
|
||||
backgroundColor: '#dde',
|
||||
}}
|
||||
>
|
||||
{[...info.boundaryFrames, info.totalFrames].map((f, i, a) => {
|
||||
const lengthFrames = f - (a[i - 1] ?? 0)
|
||||
const widthPixels = zoomTimelinePixelsPerFrame * lengthFrames
|
||||
return (
|
||||
<div
|
||||
key={`clip-${f}`}
|
||||
className="video-clip"
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
top: '10px',
|
||||
height: '60px',
|
||||
width: `${widthPixels}px`,
|
||||
backgroundImage: gradients[i % gradients.length],
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className="playback-cursor"
|
||||
ref={cursorRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
display: 'inline-block',
|
||||
left: `${zoomTimelinePixelsPerFrame * Math.trunc(currentFrame)}px`,
|
||||
width: `${Math.max(zoomTimelinePixelsPerFrame, 1)}px`,
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.4)',
|
||||
borderLeft: '1px solid red',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {App} from './App'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/500.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import {createTheme, ThemeProvider} from '@mui/material/styles'
|
||||
import {VideoInfo} from './types'
|
||||
|
||||
const theme = createTheme()
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
const info: VideoInfo = {
|
||||
totalFrames: 30 * 60 * 5,
|
||||
frameRate: 30,
|
||||
boundaryFrames: [312, 400, 1234, 1290, 3000, 3300, 4000, 8000, 8100],
|
||||
}
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App info={info} />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
1
software/js/packages/example-webserial-timeline/src/react-app-env.d.ts
vendored
Normal file
1
software/js/packages/example-webserial-timeline/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@ -0,0 +1,5 @@
|
||||
export type VideoInfo = {
|
||||
totalFrames: number
|
||||
frameRate: number
|
||||
boundaryFrames: number[]
|
||||
}
|
||||
29
software/js/packages/example-webserial-timeline/src/util.ts
Normal file
29
software/js/packages/example-webserial-timeline/src/util.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export const exhaustiveCheck = (x: never): never => {
|
||||
throw new Error(`Unexpected value: ${x}`, x)
|
||||
}
|
||||
|
||||
export const isSome = <T>(v: T | null | undefined): v is T => {
|
||||
return v !== null && v !== undefined
|
||||
}
|
||||
|
||||
export const lerp = (value: number, inMin: number, inMax: number, min: number, max: number): number => {
|
||||
// Map the input value from the input range to the output range
|
||||
value = ((value - inMin) / (inMax - inMin)) * (max - min) + min
|
||||
|
||||
// Clamp the mapped value between the minimum and maximum range
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
export type NoUndefinedField<T> = {
|
||||
[P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>
|
||||
}
|
||||
|
||||
export function findNClosest(numbers: number[], target: number, n: number): number[] {
|
||||
// First, we sort the numbers in ascending order based on their absolute difference
|
||||
// from the target number. This means that the numbers that are closest to the target
|
||||
// will come first in the sorted array.
|
||||
const sortedNumbers = numbers.sort((a, b) => Math.abs(a - target) - Math.abs(b - target))
|
||||
|
||||
// Next, we return the first N numbers from the sorted array as the N closest numbers.
|
||||
return sortedNumbers.slice(0, n)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
19
software/js/packages/smartknobjs-core/.eslintrc
Normal file
19
software/js/packages/smartknobjs-core/.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-core/.prettierrc
Normal file
10
software/js/packages/smartknobjs-core/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
26
software/js/packages/smartknobjs-core/package.json
Normal file
26
software/js/packages/smartknobjs-core/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "smartknobjs-core",
|
||||
"version": "1.0.0",
|
||||
"description": "SmartKnob Interface Core",
|
||||
"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": {
|
||||
"crc-32": "^1.2.0",
|
||||
"smartknobjs-proto": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
43
software/js/packages/smartknobjs-core/src/cobs.ts
Normal file
43
software/js/packages/smartknobjs-core/src/cobs.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// Based on https://github.com/tcr/node-cobs/blob/master/index.js
|
||||
|
||||
export function encode(buf: Uint8Array) {
|
||||
const dest = [0]
|
||||
let code_ptr = 0
|
||||
let code = 0x01
|
||||
|
||||
const finish = (incllast?: boolean) => {
|
||||
dest[code_ptr] = code
|
||||
code_ptr = dest.length
|
||||
incllast !== false && dest.push(0x00)
|
||||
code = 0x01
|
||||
}
|
||||
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
if (buf[i] == 0) {
|
||||
finish()
|
||||
} else {
|
||||
dest.push(buf[i])
|
||||
code += 1
|
||||
if (code == 0xff) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
finish(false)
|
||||
|
||||
return Uint8Array.from(dest)
|
||||
}
|
||||
|
||||
export function decode(buf: Uint8Array) {
|
||||
const dest: number[] = []
|
||||
for (let i = 0; i < buf.length; ) {
|
||||
const code = buf[i++]
|
||||
for (let j = 1; j < code; j++) {
|
||||
dest.push(buf[i++])
|
||||
}
|
||||
if (code < 0xff && i < buf.length) {
|
||||
dest.push(0)
|
||||
}
|
||||
}
|
||||
return Uint8Array.from(dest)
|
||||
}
|
||||
@ -1,97 +1,69 @@
|
||||
import SerialPort = require('serialport')
|
||||
import {decode, encode} from 'cobs'
|
||||
import {encode as cobsEncode, decode as cobsDecode} from './cobs'
|
||||
import * as CRC32 from 'crc-32'
|
||||
|
||||
import {PB} from 'smartknobjs-proto'
|
||||
|
||||
const PROTOBUF_PROTOCOL_VERSION = 1
|
||||
|
||||
export type MessageCallback = (message: PB.FromSmartKnob) => void
|
||||
export type SendBytes = (packet: Uint8Array) => void
|
||||
|
||||
type QueueEntry = {
|
||||
nonce: number
|
||||
encodedToSmartknobPayload: Uint8Array
|
||||
}
|
||||
|
||||
const sleep = (millis: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, millis)
|
||||
})
|
||||
}
|
||||
export {cobsEncode, cobsDecode}
|
||||
|
||||
export class SmartKnob {
|
||||
export class SmartKnobCore {
|
||||
private static readonly RETRY_MILLIS = 250
|
||||
private static readonly BAUD = 921600
|
||||
public static readonly BAUD = 921600
|
||||
public static readonly USB_DEVICE_FILTERS = [
|
||||
// CH340
|
||||
{
|
||||
usbVendorId: 0x1a86,
|
||||
usbProductId: 0x7523,
|
||||
},
|
||||
// ESP32-S3
|
||||
{
|
||||
usbVendorId: 0x303a,
|
||||
usbProductId: 0x1001,
|
||||
},
|
||||
]
|
||||
|
||||
private port: SerialPort | null
|
||||
private onMessage: MessageCallback
|
||||
private buffer: Buffer
|
||||
private sendBytes: SendBytes
|
||||
|
||||
private outgoingQueue: QueueEntry[] = []
|
||||
private readonly outgoingQueue: QueueEntry[] = []
|
||||
|
||||
private lastNonce = 1
|
||||
private retryTimeout: NodeJS.Timeout | null = null
|
||||
private retryTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
protected portAvailable = false
|
||||
|
||||
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!',
|
||||
})
|
||||
private buffer = new Uint8Array()
|
||||
|
||||
constructor(onMessage: MessageCallback, sendBytes: SendBytes) {
|
||||
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
|
||||
this.onMessage = onMessage
|
||||
this.sendBytes = sendBytes
|
||||
}
|
||||
|
||||
public sendConfig(config: PB.SmartKnobConfig): void {
|
||||
this.sendMessage(
|
||||
this.enqueueMessage(
|
||||
PB.ToSmartknob.create({
|
||||
smartknobConfig: config,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private processBuffer(): void {
|
||||
protected onReceivedData(data: Uint8Array) {
|
||||
this.buffer = Uint8Array.from([...this.buffer, ...data])
|
||||
|
||||
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
|
||||
const raw_buffer = this.buffer.subarray(0, i)
|
||||
const packet = cobsDecode(raw_buffer)
|
||||
this.buffer = this.buffer.slice(i + 1)
|
||||
if (packet.length <= 4) {
|
||||
console.debug(`Received short packet ${this.buffer.slice(0, i)}`)
|
||||
@ -116,7 +88,12 @@ export class SmartKnob {
|
||||
console.warn(`Invalid protobuf message ${payload}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (message.protocolVersion !== PROTOBUF_PROTOCOL_VERSION) {
|
||||
console.warn(
|
||||
`Invalid protocol version. Expected ${PROTOBUF_PROTOCOL_VERSION}, received ${message.protocolVersion}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (message.payload === 'ack') {
|
||||
const nonce = message.ack?.nonce ?? undefined
|
||||
if (nonce === undefined) {
|
||||
@ -125,15 +102,15 @@ export class SmartKnob {
|
||||
this.handleAck(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
this.onMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(message: PB.ToSmartknob) {
|
||||
if (this.port === null) {
|
||||
private enqueueMessage(message: PB.ToSmartknob) {
|
||||
if (!this.portAvailable) {
|
||||
return
|
||||
}
|
||||
message.protocolVersion = PROTOBUF_PROTOCOL_VERSION
|
||||
message.nonce = this.lastNonce++
|
||||
|
||||
// Encode before enqueueing to ensure messages don't change once they're queued
|
||||
@ -159,12 +136,12 @@ export class SmartKnob {
|
||||
this.outgoingQueue.shift()
|
||||
this.serviceQueue()
|
||||
} else {
|
||||
console.debug(`Ignoring unexpected ack for nonce ${nonce}`)
|
||||
console.log(`Ignoring unexpected ack for nonce ${nonce}`)
|
||||
}
|
||||
}
|
||||
|
||||
private serviceQueue(): void {
|
||||
if (this.port === null) {
|
||||
if (!this.portAvailable) {
|
||||
return
|
||||
}
|
||||
if (this.retryTimeout !== null) {
|
||||
@ -174,22 +151,33 @@ export class SmartKnob {
|
||||
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 crcArray = [crc & 0xff, (crc >>> 8) & 0xff, (crc >>> 16) & 0xff, (crc >>> 24) & 0xff]
|
||||
|
||||
const encodedDelimitedPacket = Buffer.concat([encode(packet), Buffer.from([0])])
|
||||
const packet = new Uint8Array(payload.length + 4)
|
||||
packet.set(payload, 0)
|
||||
packet.set(crcArray, payload.length)
|
||||
|
||||
const cobsEncodedPacket = cobsEncode(packet)
|
||||
|
||||
const encodedDelimitedPacket = new Uint8Array(cobsEncodedPacket.length + 1)
|
||||
encodedDelimitedPacket.set(cobsEncodedPacket, 0)
|
||||
encodedDelimitedPacket.set([0], cobsEncodedPacket.length)
|
||||
|
||||
this.retryTimeout = setTimeout(() => {
|
||||
this.retryTimeout = null
|
||||
console.log(`Retrying ToSmartknob...`)
|
||||
this.serviceQueue()
|
||||
}, SmartKnob.RETRY_MILLIS)
|
||||
}, SmartKnobCore.RETRY_MILLIS)
|
||||
|
||||
console.debug(`Sent ${payload.length} byte packet with CRC ${(crc >>> 0).toString(16)}`)
|
||||
this.port.write(encodedDelimitedPacket)
|
||||
console.debug(
|
||||
`Sent ${payload.length} byte payload with CRC ${(crc >>> 0).toString(16)} (${
|
||||
cobsEncodedPacket.length
|
||||
} bytes encoded)`,
|
||||
encodedDelimitedPacket,
|
||||
)
|
||||
this.sendBytes(encodedDelimitedPacket)
|
||||
}
|
||||
}
|
||||
105
software/js/packages/smartknobjs-core/tsconfig.json
Normal file
105
software/js/packages/smartknobjs-core/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": "es6" /* 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"]
|
||||
}
|
||||
19
software/js/packages/smartknobjs-node/.eslintrc
Normal file
19
software/js/packages/smartknobjs-node/.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-node/.prettierrc
Normal file
10
software/js/packages/smartknobjs-node/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "smartknobjs",
|
||||
"version": "0.1.0",
|
||||
"description": "SmartKnob Interface Library",
|
||||
"name": "smartknobjs-node",
|
||||
"version": "1.0.0",
|
||||
"description": "SmartKnob Interface Library for Node.js",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
@ -12,13 +12,11 @@
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cobs": "^0.2.1",
|
||||
"crc-32": "^1.2.0",
|
||||
"serialport": "^9.2.4",
|
||||
"smartknobjs-proto": "^0.1.0"
|
||||
"smartknobjs-core": "^1.0.0",
|
||||
"smartknobjs-proto": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/serialport": "^8.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.25.0",
|
||||
19
software/js/packages/smartknobjs-node/src/index.ts
Normal file
19
software/js/packages/smartknobjs-node/src/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import SerialPort = require('serialport')
|
||||
import {MessageCallback, SmartKnobCore} from 'smartknobjs-core'
|
||||
|
||||
export class SmartKnobNode extends SmartKnobCore {
|
||||
private port: SerialPort | null
|
||||
|
||||
constructor(serialPath: string, onMessage: MessageCallback) {
|
||||
super(onMessage, (packet: Uint8Array) => {
|
||||
this.port?.write(Buffer.from(packet))
|
||||
})
|
||||
this.port = new SerialPort(serialPath, {
|
||||
baudRate: SmartKnobCore.BAUD,
|
||||
})
|
||||
this.port.on('data', (data) => {
|
||||
this.onReceivedData(data)
|
||||
})
|
||||
this.portAvailable = true
|
||||
}
|
||||
}
|
||||
105
software/js/packages/smartknobjs-node/tsconfig.json
Normal file
105
software/js/packages/smartknobjs-node/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": "es6" /* 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,6 +1,6 @@
|
||||
{
|
||||
"name": "smartknobjs-proto",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"description": "SmartKnob Protobuf Generated Code",
|
||||
"main": "dist/smartknob_proto.js",
|
||||
"types": "dist/smartknob_proto.d.ts",
|
||||
|
||||
19
software/js/packages/smartknobjs-webserial/.eslintrc
Normal file
19
software/js/packages/smartknobjs-webserial/.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-webserial/.prettierrc
Normal file
10
software/js/packages/smartknobjs-webserial/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
27
software/js/packages/smartknobjs-webserial/package.json
Normal file
27
software/js/packages/smartknobjs-webserial/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "smartknobjs-webserial",
|
||||
"version": "1.0.0",
|
||||
"description": "SmartKnob Interface Library for Web Serial",
|
||||
"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": {
|
||||
"smartknobjs-core": "^1.0.0",
|
||||
"smartknobjs-proto": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"@types/w3c-web-serial": "^1.0.3",
|
||||
"eslint": "^8.25.0",
|
||||
"prettier": "^2.4.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
62
software/js/packages/smartknobjs-webserial/src/index.ts
Normal file
62
software/js/packages/smartknobjs-webserial/src/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {MessageCallback, SmartKnobCore} from 'smartknobjs-core'
|
||||
|
||||
export class SmartKnobWebSerial extends SmartKnobCore {
|
||||
private port: SerialPort | null
|
||||
private writer: WritableStreamDefaultWriter<Uint8Array> | undefined = undefined
|
||||
|
||||
constructor(port: SerialPort, onMessage: MessageCallback) {
|
||||
super(onMessage, (packet: Uint8Array) => {
|
||||
this.writer?.write(packet).catch(this.onError)
|
||||
})
|
||||
this.port = port
|
||||
this.portAvailable = true
|
||||
this.port.addEventListener('disconnect', () => {
|
||||
console.log('shutting down on disconnect')
|
||||
this.port = null
|
||||
this.portAvailable = false
|
||||
})
|
||||
}
|
||||
|
||||
public async openAndLoop() {
|
||||
if (this.port === null) {
|
||||
return
|
||||
}
|
||||
await this.port.open({baudRate: SmartKnobCore.BAUD})
|
||||
if (this.port.readable === null || this.port.writable === null) {
|
||||
throw new Error('Port missing readable or writable!')
|
||||
}
|
||||
|
||||
const reader = this.port.readable.getReader()
|
||||
try {
|
||||
const writer = this.port.writable.getWriter()
|
||||
writer.write(Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0])).catch(this.onError)
|
||||
this.writer = writer
|
||||
try {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const {value, done} = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
if (value !== undefined) {
|
||||
this.onReceivedData(value)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
console.log('Releasing writer')
|
||||
writer?.releaseLock()
|
||||
}
|
||||
} finally {
|
||||
console.log('Releasing reader')
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
private onError(e: unknown) {
|
||||
console.error('Error writing serial', e)
|
||||
this.port?.close()
|
||||
this.port = null
|
||||
this.portAvailable = false
|
||||
this.writer = undefined
|
||||
}
|
||||
}
|
||||
105
software/js/packages/smartknobjs-webserial/tsconfig.json
Normal file
105
software/js/packages/smartknobjs-webserial/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": "es6" /* 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,4 +0,0 @@
|
||||
declare module 'cobs' {
|
||||
export function decode(buf: Buffer): Buffer
|
||||
export function encode(buf: Buffer, zeroFrame?: boolean): Buffer
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
14
software/python/Pipfile
Normal file
14
software/python/Pipfile
Normal file
@ -0,0 +1,14 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
pyserial = "*"
|
||||
cobs = "*"
|
||||
protobuf = "==3.20.3"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
73
software/python/Pipfile.lock
generated
Normal file
73
software/python/Pipfile.lock
generated
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "a74717fa76c8f068ed910686866d0d3ad0ef124601f1d8cf37cc0592e71d4fee"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.10"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"cobs": {
|
||||
"hashes": [
|
||||
"sha256:3f2e8ef22070c3a95ed998303b81c209dd49aaaebf45173499a1a48b8b17bb6f",
|
||||
"sha256:4625f7648e8b0ea50702cf213dcfcc0a50dc78fc6678b174f1f5e0d6fc3687c1",
|
||||
"sha256:7c4a9e246c11f48e12c1231ad5738ea927f663b7b4ffd1de0be3b1cae5bb3884",
|
||||
"sha256:991fc195f0581e456d3dd21e29df493fe9ca1b772084c26921601e48dbc34bd2",
|
||||
"sha256:b4af64be97f2a7215c3ffcd5cca26e6aa166e49cd90941efab95c76211b6710c",
|
||||
"sha256:c747c4b385f04203e1d2b767d61a69580b58c97eea7a6ed998edd42ddd9fcdc5",
|
||||
"sha256:cb573184d982edf8e7c3c08c753b765790790e7ffdca4c2ca5c258bbd9099f33",
|
||||
"sha256:d0f7e7fcad3020676feef505a0212bcd0c86549d3e4c3680f6a7c77ef599e52b",
|
||||
"sha256:d93b10b5370dc8868960aefb70adb1f73a4e6107b16918307a4aeeefd3edb8f6",
|
||||
"sha256:f68ee5e70ae6be79b424cb270835e864b86320a9fcb8048575443b658f61ee0a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
|
||||
"sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
|
||||
"sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
|
||||
"sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
|
||||
"sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
|
||||
"sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
|
||||
"sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
|
||||
"sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
|
||||
"sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
|
||||
"sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
|
||||
"sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
|
||||
"sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
|
||||
"sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
|
||||
"sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
|
||||
"sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
|
||||
"sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
|
||||
"sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
|
||||
"sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
|
||||
"sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
|
||||
"sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
|
||||
"sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
|
||||
"sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.20.3"
|
||||
},
|
||||
"pyserial": {
|
||||
"hashes": [
|
||||
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
|
||||
"sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
5
software/python/proto_gen/README
Normal file
5
software/python/proto_gen/README
Normal file
@ -0,0 +1,5 @@
|
||||
The files in this directory are auto-generated by the protobuf compiler.
|
||||
|
||||
They're checked into the repo to avoid the need to always re-compile them.
|
||||
|
||||
They can be regenerated by running `<repo_root>/proto/generate_protobuf.py`
|
||||
508
software/python/proto_gen/nanopb_pb2.py
Normal file
508
software/python/proto_gen/nanopb_pb2.py
Normal file
@ -0,0 +1,508 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: nanopb.proto
|
||||
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='nanopb.proto',
|
||||
package='',
|
||||
syntax='proto2',
|
||||
serialized_options=b'\n\030fi.kapsi.koti.jpa.nanopb',
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x0cnanopb.proto\x1a google/protobuf/descriptor.proto\"\xa4\x07\n\rNanoPBOptions\x12\x10\n\x08max_size\x18\x01 \x01(\x05\x12\x12\n\nmax_length\x18\x0e \x01(\x05\x12\x11\n\tmax_count\x18\x02 \x01(\x05\x12&\n\x08int_size\x18\x07 \x01(\x0e\x32\x08.IntSize:\nIS_DEFAULT\x12$\n\x04type\x18\x03 \x01(\x0e\x32\n.FieldType:\nFT_DEFAULT\x12\x18\n\nlong_names\x18\x04 \x01(\x08:\x04true\x12\x1c\n\rpacked_struct\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bpacked_enum\x18\n \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0cskip_message\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tno_unions\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05msgid\x18\t \x01(\r\x12\x1e\n\x0f\x61nonymous_oneof\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x15\n\x06proto3\x18\x0c \x01(\x08:\x05\x66\x61lse\x12#\n\x14proto3_singular_msgs\x18\x15 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x0e\x65num_to_string\x18\r \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0c\x66ixed_length\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x66ixed_count\x18\x10 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0fsubmsg_callback\x18\x16 \x01(\x08:\x05\x66\x61lse\x12/\n\x0cmangle_names\x18\x11 \x01(\x0e\x32\x11.TypenameMangling:\x06M_NONE\x12(\n\x11\x63\x61llback_datatype\x18\x12 \x01(\t:\rpb_callback_t\x12\x34\n\x11\x63\x61llback_function\x18\x13 \x01(\t:\x19pb_default_field_callback\x12\x30\n\x0e\x64\x65scriptorsize\x18\x14 \x01(\x0e\x32\x0f.DescriptorSize:\x07\x44S_AUTO\x12\x1a\n\x0b\x64\x65\x66\x61ult_has\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x0f\n\x07include\x18\x18 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x1a \x03(\t\x12\x0f\n\x07package\x18\x19 \x01(\t\x12\x41\n\rtype_override\x18\x1b \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x19\n\x0bsort_by_tag\x18\x1c \x01(\x08:\x04true\x12.\n\rfallback_type\x18\x1d \x01(\x0e\x32\n.FieldType:\x0b\x46T_CALLBACK*i\n\tFieldType\x12\x0e\n\nFT_DEFAULT\x10\x00\x12\x0f\n\x0b\x46T_CALLBACK\x10\x01\x12\x0e\n\nFT_POINTER\x10\x04\x12\r\n\tFT_STATIC\x10\x02\x12\r\n\tFT_IGNORE\x10\x03\x12\r\n\tFT_INLINE\x10\x05*D\n\x07IntSize\x12\x0e\n\nIS_DEFAULT\x10\x00\x12\x08\n\x04IS_8\x10\x08\x12\t\n\x05IS_16\x10\x10\x12\t\n\x05IS_32\x10 \x12\t\n\x05IS_64\x10@*Z\n\x10TypenameMangling\x12\n\n\x06M_NONE\x10\x00\x12\x13\n\x0fM_STRIP_PACKAGE\x10\x01\x12\r\n\tM_FLATTEN\x10\x02\x12\x16\n\x12M_PACKAGE_INITIALS\x10\x03*E\n\x0e\x44\x65scriptorSize\x12\x0b\n\x07\x44S_AUTO\x10\x00\x12\x08\n\x04\x44S_1\x10\x01\x12\x08\n\x04\x44S_2\x10\x02\x12\x08\n\x04\x44S_4\x10\x04\x12\x08\n\x04\x44S_8\x10\x08:E\n\x0enanopb_fileopt\x12\x1c.google.protobuf.FileOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:G\n\rnanopb_msgopt\x12\x1f.google.protobuf.MessageOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:E\n\x0enanopb_enumopt\x12\x1c.google.protobuf.EnumOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:>\n\x06nanopb\x12\x1d.google.protobuf.FieldOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptionsB\x1a\n\x18\x66i.kapsi.koti.jpa.nanopb'
|
||||
,
|
||||
dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,])
|
||||
|
||||
_FIELDTYPE = _descriptor.EnumDescriptor(
|
||||
name='FieldType',
|
||||
full_name='FieldType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_DEFAULT', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_CALLBACK', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_POINTER', index=2, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_STATIC', index=3, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_IGNORE', index=4, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FT_INLINE', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=985,
|
||||
serialized_end=1090,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_FIELDTYPE)
|
||||
|
||||
FieldType = enum_type_wrapper.EnumTypeWrapper(_FIELDTYPE)
|
||||
_INTSIZE = _descriptor.EnumDescriptor(
|
||||
name='IntSize',
|
||||
full_name='IntSize',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='IS_DEFAULT', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='IS_8', index=1, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='IS_16', index=2, number=16,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='IS_32', index=3, number=32,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='IS_64', index=4, number=64,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1092,
|
||||
serialized_end=1160,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_INTSIZE)
|
||||
|
||||
IntSize = enum_type_wrapper.EnumTypeWrapper(_INTSIZE)
|
||||
_TYPENAMEMANGLING = _descriptor.EnumDescriptor(
|
||||
name='TypenameMangling',
|
||||
full_name='TypenameMangling',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='M_NONE', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='M_STRIP_PACKAGE', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='M_FLATTEN', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='M_PACKAGE_INITIALS', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1162,
|
||||
serialized_end=1252,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_TYPENAMEMANGLING)
|
||||
|
||||
TypenameMangling = enum_type_wrapper.EnumTypeWrapper(_TYPENAMEMANGLING)
|
||||
_DESCRIPTORSIZE = _descriptor.EnumDescriptor(
|
||||
name='DescriptorSize',
|
||||
full_name='DescriptorSize',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DS_AUTO', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DS_1', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DS_2', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DS_4', index=3, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DS_8', index=4, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1254,
|
||||
serialized_end=1323,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_DESCRIPTORSIZE)
|
||||
|
||||
DescriptorSize = enum_type_wrapper.EnumTypeWrapper(_DESCRIPTORSIZE)
|
||||
FT_DEFAULT = 0
|
||||
FT_CALLBACK = 1
|
||||
FT_POINTER = 4
|
||||
FT_STATIC = 2
|
||||
FT_IGNORE = 3
|
||||
FT_INLINE = 5
|
||||
IS_DEFAULT = 0
|
||||
IS_8 = 8
|
||||
IS_16 = 16
|
||||
IS_32 = 32
|
||||
IS_64 = 64
|
||||
M_NONE = 0
|
||||
M_STRIP_PACKAGE = 1
|
||||
M_FLATTEN = 2
|
||||
M_PACKAGE_INITIALS = 3
|
||||
DS_AUTO = 0
|
||||
DS_1 = 1
|
||||
DS_2 = 2
|
||||
DS_4 = 4
|
||||
DS_8 = 8
|
||||
|
||||
NANOPB_FILEOPT_FIELD_NUMBER = 1010
|
||||
nanopb_fileopt = _descriptor.FieldDescriptor(
|
||||
name='nanopb_fileopt', full_name='nanopb_fileopt', index=0,
|
||||
number=1010, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=True, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
|
||||
NANOPB_MSGOPT_FIELD_NUMBER = 1010
|
||||
nanopb_msgopt = _descriptor.FieldDescriptor(
|
||||
name='nanopb_msgopt', full_name='nanopb_msgopt', index=1,
|
||||
number=1010, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=True, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
|
||||
NANOPB_ENUMOPT_FIELD_NUMBER = 1010
|
||||
nanopb_enumopt = _descriptor.FieldDescriptor(
|
||||
name='nanopb_enumopt', full_name='nanopb_enumopt', index=2,
|
||||
number=1010, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=True, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
|
||||
NANOPB_FIELD_NUMBER = 1010
|
||||
nanopb = _descriptor.FieldDescriptor(
|
||||
name='nanopb', full_name='nanopb', index=3,
|
||||
number=1010, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=True, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key)
|
||||
|
||||
|
||||
_NANOPBOPTIONS = _descriptor.Descriptor(
|
||||
name='NanoPBOptions',
|
||||
full_name='NanoPBOptions',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='max_size', full_name='NanoPBOptions.max_size', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='max_length', full_name='NanoPBOptions.max_length', index=1,
|
||||
number=14, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='max_count', full_name='NanoPBOptions.max_count', index=2,
|
||||
number=2, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='int_size', full_name='NanoPBOptions.int_size', index=3,
|
||||
number=7, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='type', full_name='NanoPBOptions.type', index=4,
|
||||
number=3, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='long_names', full_name='NanoPBOptions.long_names', index=5,
|
||||
number=4, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=True,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='packed_struct', full_name='NanoPBOptions.packed_struct', index=6,
|
||||
number=5, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='packed_enum', full_name='NanoPBOptions.packed_enum', index=7,
|
||||
number=10, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='skip_message', full_name='NanoPBOptions.skip_message', index=8,
|
||||
number=6, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='no_unions', full_name='NanoPBOptions.no_unions', index=9,
|
||||
number=8, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='msgid', full_name='NanoPBOptions.msgid', index=10,
|
||||
number=9, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='anonymous_oneof', full_name='NanoPBOptions.anonymous_oneof', index=11,
|
||||
number=11, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proto3', full_name='NanoPBOptions.proto3', index=12,
|
||||
number=12, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proto3_singular_msgs', full_name='NanoPBOptions.proto3_singular_msgs', index=13,
|
||||
number=21, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='enum_to_string', full_name='NanoPBOptions.enum_to_string', index=14,
|
||||
number=13, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fixed_length', full_name='NanoPBOptions.fixed_length', index=15,
|
||||
number=15, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fixed_count', full_name='NanoPBOptions.fixed_count', index=16,
|
||||
number=16, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='submsg_callback', full_name='NanoPBOptions.submsg_callback', index=17,
|
||||
number=22, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='mangle_names', full_name='NanoPBOptions.mangle_names', index=18,
|
||||
number=17, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='callback_datatype', full_name='NanoPBOptions.callback_datatype', index=19,
|
||||
number=18, type=9, cpp_type=9, label=1,
|
||||
has_default_value=True, default_value=b"pb_callback_t".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='callback_function', full_name='NanoPBOptions.callback_function', index=20,
|
||||
number=19, type=9, cpp_type=9, label=1,
|
||||
has_default_value=True, default_value=b"pb_default_field_callback".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='descriptorsize', full_name='NanoPBOptions.descriptorsize', index=21,
|
||||
number=20, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='default_has', full_name='NanoPBOptions.default_has', index=22,
|
||||
number=23, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='include', full_name='NanoPBOptions.include', index=23,
|
||||
number=24, type=9, cpp_type=9, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exclude', full_name='NanoPBOptions.exclude', index=24,
|
||||
number=26, type=9, cpp_type=9, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='package', full_name='NanoPBOptions.package', index=25,
|
||||
number=25, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='type_override', full_name='NanoPBOptions.type_override', index=26,
|
||||
number=27, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=1,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='sort_by_tag', full_name='NanoPBOptions.sort_by_tag', index=27,
|
||||
number=28, type=8, cpp_type=7, label=1,
|
||||
has_default_value=True, default_value=True,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fallback_type', full_name='NanoPBOptions.fallback_type', index=28,
|
||||
number=29, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=1,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=51,
|
||||
serialized_end=983,
|
||||
)
|
||||
|
||||
_NANOPBOPTIONS.fields_by_name['int_size'].enum_type = _INTSIZE
|
||||
_NANOPBOPTIONS.fields_by_name['type'].enum_type = _FIELDTYPE
|
||||
_NANOPBOPTIONS.fields_by_name['mangle_names'].enum_type = _TYPENAMEMANGLING
|
||||
_NANOPBOPTIONS.fields_by_name['descriptorsize'].enum_type = _DESCRIPTORSIZE
|
||||
_NANOPBOPTIONS.fields_by_name['type_override'].enum_type = google_dot_protobuf_dot_descriptor__pb2._FIELDDESCRIPTORPROTO_TYPE
|
||||
_NANOPBOPTIONS.fields_by_name['fallback_type'].enum_type = _FIELDTYPE
|
||||
DESCRIPTOR.message_types_by_name['NanoPBOptions'] = _NANOPBOPTIONS
|
||||
DESCRIPTOR.enum_types_by_name['FieldType'] = _FIELDTYPE
|
||||
DESCRIPTOR.enum_types_by_name['IntSize'] = _INTSIZE
|
||||
DESCRIPTOR.enum_types_by_name['TypenameMangling'] = _TYPENAMEMANGLING
|
||||
DESCRIPTOR.enum_types_by_name['DescriptorSize'] = _DESCRIPTORSIZE
|
||||
DESCRIPTOR.extensions_by_name['nanopb_fileopt'] = nanopb_fileopt
|
||||
DESCRIPTOR.extensions_by_name['nanopb_msgopt'] = nanopb_msgopt
|
||||
DESCRIPTOR.extensions_by_name['nanopb_enumopt'] = nanopb_enumopt
|
||||
DESCRIPTOR.extensions_by_name['nanopb'] = nanopb
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
NanoPBOptions = _reflection.GeneratedProtocolMessageType('NanoPBOptions', (_message.Message,), {
|
||||
'DESCRIPTOR' : _NANOPBOPTIONS,
|
||||
'__module__' : 'nanopb_pb2'
|
||||
# @@protoc_insertion_point(class_scope:NanoPBOptions)
|
||||
})
|
||||
_sym_db.RegisterMessage(NanoPBOptions)
|
||||
|
||||
nanopb_fileopt.message_type = _NANOPBOPTIONS
|
||||
google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(nanopb_fileopt)
|
||||
nanopb_msgopt.message_type = _NANOPBOPTIONS
|
||||
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(nanopb_msgopt)
|
||||
nanopb_enumopt.message_type = _NANOPBOPTIONS
|
||||
google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(nanopb_enumopt)
|
||||
nanopb.message_type = _NANOPBOPTIONS
|
||||
google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(nanopb)
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
141
software/python/proto_gen/smartknob_pb2.py
Normal file
141
software/python/proto_gen/smartknob_pb2.py
Normal file
@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: smartknob.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import nanopb_pb2 as nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fsmartknob.proto\x12\x02PB\x1a\x0cnanopb.proto\"\x9a\x01\n\rFromSmartKnob\x12\x1f\n\x10protocol_version\x18\x01 \x01(\rB\x05\x92?\x02\x38\x08\x12\x16\n\x03\x61\x63k\x18\x02 \x01(\x0b\x32\x07.PB.AckH\x00\x12\x16\n\x03log\x18\x03 \x01(\x0b\x32\x07.PB.LogH\x00\x12-\n\x0fsmartknob_state\x18\x04 \x01(\x0b\x32\x12.PB.SmartKnobStateH\x00\x42\t\n\x07payload\"\xa4\x01\n\x0bToSmartknob\x12\x1f\n\x10protocol_version\x18\x01 \x01(\rB\x05\x92?\x02\x38\x08\x12\r\n\x05nonce\x18\x02 \x01(\r\x12)\n\rrequest_state\x18\x03 \x01(\x0b\x32\x10.PB.RequestStateH\x00\x12/\n\x10smartknob_config\x18\x04 \x01(\x0b\x32\x13.PB.SmartKnobConfigH\x00\x42\t\n\x07payload\"\x14\n\x03\x41\x63k\x12\r\n\x05nonce\x18\x01 \x01(\r\"\x1a\n\x03Log\x12\x13\n\x03msg\x18\x01 \x01(\tB\x06\x92?\x03p\xff\x01\"\x86\x01\n\x0eSmartKnobState\x12\x18\n\x10\x63urrent_position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12#\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfig\x12\x1a\n\x0bpress_nonce\x18\x04 \x01(\rB\x05\x92?\x02\x38\x08\"\xe1\x02\n\x0fSmartKnobConfig\x12\x10\n\x08position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12\x1d\n\x0eposition_nonce\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12\x14\n\x0cmin_position\x18\x04 \x01(\x05\x12\x14\n\x0cmax_position\x18\x05 \x01(\x05\x12\x1e\n\x16position_width_radians\x18\x06 \x01(\x02\x12\x1c\n\x14\x64\x65tent_strength_unit\x18\x07 \x01(\x02\x12\x1d\n\x15\x65ndstop_strength_unit\x18\x08 \x01(\x02\x12\x12\n\nsnap_point\x18\t \x01(\x02\x12\x13\n\x04text\x18\n \x01(\tB\x05\x92?\x02p2\x12\x1f\n\x10\x64\x65tent_positions\x18\x0b \x03(\x05\x42\x05\x92?\x02\x10\x05\x12\x17\n\x0fsnap_point_bias\x18\x0c \x01(\x02\x12\x16\n\x07led_hue\x18\r \x01(\x05\x42\x05\x92?\x02\x38\x10\"\x0e\n\x0cRequestState\"v\n\x17PersistentConfiguration\x12\x0f\n\x07version\x18\x01 \x01(\r\x12#\n\x05motor\x18\x02 \x01(\x0b\x32\x14.PB.MotorCalibration\x12%\n\x06strain\x18\x03 \x01(\x0b\x32\x15.PB.StrainCalibration\"p\n\x10MotorCalibration\x12\x12\n\ncalibrated\x18\x01 \x01(\x08\x12\x1e\n\x16zero_electrical_offset\x18\x02 \x01(\x02\x12\x14\n\x0c\x64irection_cw\x18\x03 \x01(\x08\x12\x12\n\npole_pairs\x18\x04 \x01(\r\"<\n\x11StrainCalibration\x12\x12\n\nidle_value\x18\x01 \x01(\x05\x12\x13\n\x0bpress_delta\x18\x02 \x01(\x05\x62\x06proto3')
|
||||
|
||||
|
||||
|
||||
_FROMSMARTKNOB = DESCRIPTOR.message_types_by_name['FromSmartKnob']
|
||||
_TOSMARTKNOB = DESCRIPTOR.message_types_by_name['ToSmartknob']
|
||||
_ACK = DESCRIPTOR.message_types_by_name['Ack']
|
||||
_LOG = DESCRIPTOR.message_types_by_name['Log']
|
||||
_SMARTKNOBSTATE = DESCRIPTOR.message_types_by_name['SmartKnobState']
|
||||
_SMARTKNOBCONFIG = DESCRIPTOR.message_types_by_name['SmartKnobConfig']
|
||||
_REQUESTSTATE = DESCRIPTOR.message_types_by_name['RequestState']
|
||||
_PERSISTENTCONFIGURATION = DESCRIPTOR.message_types_by_name['PersistentConfiguration']
|
||||
_MOTORCALIBRATION = DESCRIPTOR.message_types_by_name['MotorCalibration']
|
||||
_STRAINCALIBRATION = DESCRIPTOR.message_types_by_name['StrainCalibration']
|
||||
FromSmartKnob = _reflection.GeneratedProtocolMessageType('FromSmartKnob', (_message.Message,), {
|
||||
'DESCRIPTOR' : _FROMSMARTKNOB,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.FromSmartKnob)
|
||||
})
|
||||
_sym_db.RegisterMessage(FromSmartKnob)
|
||||
|
||||
ToSmartknob = _reflection.GeneratedProtocolMessageType('ToSmartknob', (_message.Message,), {
|
||||
'DESCRIPTOR' : _TOSMARTKNOB,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.ToSmartknob)
|
||||
})
|
||||
_sym_db.RegisterMessage(ToSmartknob)
|
||||
|
||||
Ack = _reflection.GeneratedProtocolMessageType('Ack', (_message.Message,), {
|
||||
'DESCRIPTOR' : _ACK,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.Ack)
|
||||
})
|
||||
_sym_db.RegisterMessage(Ack)
|
||||
|
||||
Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), {
|
||||
'DESCRIPTOR' : _LOG,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.Log)
|
||||
})
|
||||
_sym_db.RegisterMessage(Log)
|
||||
|
||||
SmartKnobState = _reflection.GeneratedProtocolMessageType('SmartKnobState', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SMARTKNOBSTATE,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.SmartKnobState)
|
||||
})
|
||||
_sym_db.RegisterMessage(SmartKnobState)
|
||||
|
||||
SmartKnobConfig = _reflection.GeneratedProtocolMessageType('SmartKnobConfig', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SMARTKNOBCONFIG,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.SmartKnobConfig)
|
||||
})
|
||||
_sym_db.RegisterMessage(SmartKnobConfig)
|
||||
|
||||
RequestState = _reflection.GeneratedProtocolMessageType('RequestState', (_message.Message,), {
|
||||
'DESCRIPTOR' : _REQUESTSTATE,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.RequestState)
|
||||
})
|
||||
_sym_db.RegisterMessage(RequestState)
|
||||
|
||||
PersistentConfiguration = _reflection.GeneratedProtocolMessageType('PersistentConfiguration', (_message.Message,), {
|
||||
'DESCRIPTOR' : _PERSISTENTCONFIGURATION,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.PersistentConfiguration)
|
||||
})
|
||||
_sym_db.RegisterMessage(PersistentConfiguration)
|
||||
|
||||
MotorCalibration = _reflection.GeneratedProtocolMessageType('MotorCalibration', (_message.Message,), {
|
||||
'DESCRIPTOR' : _MOTORCALIBRATION,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.MotorCalibration)
|
||||
})
|
||||
_sym_db.RegisterMessage(MotorCalibration)
|
||||
|
||||
StrainCalibration = _reflection.GeneratedProtocolMessageType('StrainCalibration', (_message.Message,), {
|
||||
'DESCRIPTOR' : _STRAINCALIBRATION,
|
||||
'__module__' : 'smartknob_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PB.StrainCalibration)
|
||||
})
|
||||
_sym_db.RegisterMessage(StrainCalibration)
|
||||
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_FROMSMARTKNOB.fields_by_name['protocol_version']._options = None
|
||||
_FROMSMARTKNOB.fields_by_name['protocol_version']._serialized_options = b'\222?\0028\010'
|
||||
_TOSMARTKNOB.fields_by_name['protocol_version']._options = None
|
||||
_TOSMARTKNOB.fields_by_name['protocol_version']._serialized_options = b'\222?\0028\010'
|
||||
_LOG.fields_by_name['msg']._options = None
|
||||
_LOG.fields_by_name['msg']._serialized_options = b'\222?\003p\377\001'
|
||||
_SMARTKNOBSTATE.fields_by_name['press_nonce']._options = None
|
||||
_SMARTKNOBSTATE.fields_by_name['press_nonce']._serialized_options = b'\222?\0028\010'
|
||||
_SMARTKNOBCONFIG.fields_by_name['position_nonce']._options = None
|
||||
_SMARTKNOBCONFIG.fields_by_name['position_nonce']._serialized_options = b'\222?\0028\010'
|
||||
_SMARTKNOBCONFIG.fields_by_name['text']._options = None
|
||||
_SMARTKNOBCONFIG.fields_by_name['text']._serialized_options = b'\222?\002p2'
|
||||
_SMARTKNOBCONFIG.fields_by_name['detent_positions']._options = None
|
||||
_SMARTKNOBCONFIG.fields_by_name['detent_positions']._serialized_options = b'\222?\002\020\005'
|
||||
_SMARTKNOBCONFIG.fields_by_name['led_hue']._options = None
|
||||
_SMARTKNOBCONFIG.fields_by_name['led_hue']._serialized_options = b'\222?\0028\020'
|
||||
_FROMSMARTKNOB._serialized_start=38
|
||||
_FROMSMARTKNOB._serialized_end=192
|
||||
_TOSMARTKNOB._serialized_start=195
|
||||
_TOSMARTKNOB._serialized_end=359
|
||||
_ACK._serialized_start=361
|
||||
_ACK._serialized_end=381
|
||||
_LOG._serialized_start=383
|
||||
_LOG._serialized_end=409
|
||||
_SMARTKNOBSTATE._serialized_start=412
|
||||
_SMARTKNOBSTATE._serialized_end=546
|
||||
_SMARTKNOBCONFIG._serialized_start=549
|
||||
_SMARTKNOBCONFIG._serialized_end=902
|
||||
_REQUESTSTATE._serialized_start=904
|
||||
_REQUESTSTATE._serialized_end=918
|
||||
_PERSISTENTCONFIGURATION._serialized_start=920
|
||||
_PERSISTENTCONFIGURATION._serialized_end=1038
|
||||
_MOTORCALIBRATION._serialized_start=1040
|
||||
_MOTORCALIBRATION._serialized_end=1152
|
||||
_STRAINCALIBRATION._serialized_start=1154
|
||||
_STRAINCALIBRATION._serialized_end=1214
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
56
software/python/simple_example.py
Normal file
56
software/python/simple_example.py
Normal file
@ -0,0 +1,56 @@
|
||||
import os
|
||||
import sys
|
||||
if __name__ == '__main__':
|
||||
if 'PIPENV_ACTIVE' not in os.environ:
|
||||
sys.exit(f'This script should be run in a Pipenv.\n\nRun it as:\npipenv run python {os.path.basename(__file__)}')
|
||||
|
||||
# Place imports below this line
|
||||
import logging
|
||||
import math
|
||||
|
||||
from smartknob_io import (
|
||||
ask_for_serial_port,
|
||||
smartknob_context
|
||||
)
|
||||
from proto_gen import smartknob_pb2
|
||||
|
||||
def _run_example():
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
p = ask_for_serial_port()
|
||||
with smartknob_context(p) as s:
|
||||
# Initialize with an empty state object
|
||||
last_state = smartknob_pb2.SmartKnobState()
|
||||
|
||||
# Callback function to handle state updates
|
||||
def log_state(new_state):
|
||||
nonlocal last_state
|
||||
|
||||
# We'll log the state if it's changed substantially since the last state we recieved
|
||||
config_changed = last_state.config.SerializeToString(deterministic=True) != new_state.config.SerializeToString(deterministic=True)
|
||||
position_changed = last_state.current_position != new_state.current_position
|
||||
sub_position_large_change = abs(last_state.sub_position_unit * last_state.config.position_width_radians - new_state.sub_position_unit * new_state.config.position_width_radians) > math.radians(5)
|
||||
press_nonce_changed = last_state.press_nonce != new_state.press_nonce
|
||||
if config_changed or position_changed or sub_position_large_change or press_nonce_changed:
|
||||
logging.info('State: ' + str(new_state))
|
||||
last_state = new_state
|
||||
|
||||
# Register our state handler function
|
||||
s.add_handler('smartknob_state', log_state)
|
||||
|
||||
# Run forever, set config when enter is pressed
|
||||
while True:
|
||||
input()
|
||||
config = smartknob_pb2.SmartKnobConfig()
|
||||
config.position = 0
|
||||
config.min_position = 0
|
||||
config.max_position = 5
|
||||
config.position_width_radians = math.radians(10)
|
||||
config.detent_strength_unit = 1
|
||||
config.endstop_strength_unit = 1
|
||||
config.snap_point = 1.1
|
||||
config.text = "From Python!"
|
||||
s.set_config(config)
|
||||
|
||||
if __name__ == '__main__':
|
||||
_run_example()
|
||||
262
software/python/smartknob_io.py
Normal file
262
software/python/smartknob_io.py
Normal file
@ -0,0 +1,262 @@
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit('This is a library file to be imported into your own python scripts. It doesn\'t do anything if run directly')
|
||||
|
||||
|
||||
from cobs import cobs
|
||||
from collections import (
|
||||
defaultdict,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
import logging
|
||||
import os
|
||||
from queue import (
|
||||
Empty,
|
||||
Full,
|
||||
Queue,
|
||||
)
|
||||
from random import randint
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import sys
|
||||
from threading import (
|
||||
Thread,
|
||||
Lock,
|
||||
)
|
||||
import time
|
||||
import zlib
|
||||
|
||||
software_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(software_root, 'proto_gen'))
|
||||
|
||||
from proto_gen import smartknob_pb2
|
||||
|
||||
SMARTKNOB_BAUD = 921600
|
||||
PROTOBUF_PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
class Smartknob(object):
|
||||
RETRY_TIMEOUT = 0.25
|
||||
|
||||
def __init__(self, serial_instance):
|
||||
self._serial = serial_instance
|
||||
self._logger = logging.getLogger('smartknob')
|
||||
self._out_q = Queue()
|
||||
self._ack_q = Queue()
|
||||
self._next_nonce = randint(0, 255)
|
||||
self._run = True
|
||||
|
||||
self._lock = Lock()
|
||||
self._message_handlers = defaultdict(list)
|
||||
|
||||
def _read_loop(self):
|
||||
self._logger.debug('Read loop started')
|
||||
buffer = b''
|
||||
while True:
|
||||
buffer += self._serial.read_until(b'\0')
|
||||
if not self._run:
|
||||
return
|
||||
|
||||
if not len(buffer):
|
||||
continue
|
||||
|
||||
if not buffer.endswith(b'\0'):
|
||||
continue
|
||||
|
||||
self._process_frame(buffer[:-1])
|
||||
buffer = b''
|
||||
|
||||
def _process_frame(self, frame):
|
||||
try:
|
||||
decoded = cobs.decode(frame)
|
||||
except cobs.DecodeError:
|
||||
self._logger.debug(f'Failed decode ({len(frame)} bytes)')
|
||||
self._logger.debug(frame)
|
||||
return
|
||||
|
||||
if len(decoded) < 4:
|
||||
return
|
||||
|
||||
payload = decoded[:-4]
|
||||
expected_crc = zlib.crc32(payload) & 0xffffffff
|
||||
provided_crc = (decoded[-1] << 24) \
|
||||
| (decoded[-2] << 16) \
|
||||
| (decoded[-3] << 8) \
|
||||
| decoded[-4]
|
||||
|
||||
if expected_crc != provided_crc:
|
||||
self._logger.debug(f'Bad CRC. expected={hex(expected_crc)}, actual={hex(provided_crc)}')
|
||||
return
|
||||
|
||||
message = smartknob_pb2.FromSmartKnob()
|
||||
message.ParseFromString(payload)
|
||||
self._logger.debug(message)
|
||||
if message.protocol_version != PROTOBUF_PROTOCOL_VERSION:
|
||||
self._logger.warn(f'Invalid protocol version. Expected {PROTOBUF_PROTOCOL_VERSION}, received {message.protocol_version}')
|
||||
|
||||
payload_type = message.WhichOneof('payload')
|
||||
|
||||
# If this is an ack, notify the write thread
|
||||
if payload_type == 'ack':
|
||||
nonce = message.ack.nonce
|
||||
self._ack_q.put(nonce)
|
||||
|
||||
with self._lock:
|
||||
for handler in self._message_handlers[payload_type] + self._message_handlers[None]:
|
||||
try:
|
||||
handler(getattr(message, payload_type))
|
||||
except:
|
||||
self._logger.warning(f'Unhandled exception in message handler ({payload_type})', exc_info=True)
|
||||
|
||||
def _write_loop(self):
|
||||
self._logger.debug('Write loop started')
|
||||
while True:
|
||||
data = self._out_q.get()
|
||||
# Check for shutdown
|
||||
if not self._run:
|
||||
self._logger.debug('Write loop exiting @ _out_q')
|
||||
return
|
||||
(nonce, encoded_message) = data
|
||||
|
||||
next_retry = 0
|
||||
while True:
|
||||
if time.time() >= next_retry:
|
||||
if next_retry > 0:
|
||||
self._logger.debug('Retry write...')
|
||||
self._serial.write(encoded_message)
|
||||
self._serial.write(b'\0')
|
||||
next_retry = time.time() + Smartknob.RETRY_TIMEOUT
|
||||
|
||||
try:
|
||||
latest_ack_nonce = self._ack_q.get(timeout=next_retry - time.time())
|
||||
except Empty:
|
||||
latest_ack_nonce = None
|
||||
|
||||
# Check for shutdown
|
||||
if not self._run:
|
||||
self._logger.debug('Write loop exiting @ _ack_q')
|
||||
return
|
||||
|
||||
if latest_ack_nonce == nonce:
|
||||
break
|
||||
else:
|
||||
self._logger.debug(f'Got unexpected nonce: {latest_ack_nonce}')
|
||||
|
||||
def _enqueue_message(self, message):
|
||||
nonce = self._next_nonce
|
||||
self._next_nonce += 1
|
||||
|
||||
message.protocol_version = PROTOBUF_PROTOCOL_VERSION
|
||||
message.nonce = nonce
|
||||
|
||||
payload = bytearray(message.SerializeToString())
|
||||
|
||||
crc = zlib.crc32(payload) & 0xffffffff
|
||||
payload.append(crc & 0xff)
|
||||
payload.append((crc >> 8) & 0xff)
|
||||
payload.append((crc >> 16) & 0xff)
|
||||
payload.append((crc >> 24) & 0xff)
|
||||
|
||||
encoded_message = cobs.encode(payload)
|
||||
|
||||
self._out_q.put((nonce, encoded_message))
|
||||
|
||||
approx_q_length = self._out_q.qsize()
|
||||
self._logger.debug(f'Out q length: {approx_q_length}')
|
||||
if approx_q_length > 10:
|
||||
self._logger.warning(f'Output queue length is high! ({approx_q_length}) Is the smartknob still connected and functional?')
|
||||
|
||||
def set_config(self, config):
|
||||
message = smartknob_pb2.ToSmartknob()
|
||||
message.smartknob_config.CopyFrom(config)
|
||||
self._enqueue_message(message)
|
||||
|
||||
def start(self):
|
||||
self.read_thread = Thread(target=self._read_loop)
|
||||
self.write_thread = Thread(target=self._write_loop)
|
||||
self.read_thread.start()
|
||||
self.write_thread.start()
|
||||
|
||||
def shutdown(self):
|
||||
self._logger.info('Shutting down...')
|
||||
self._run = False
|
||||
self.read_thread.join()
|
||||
self._logger.debug('Read thread terminated')
|
||||
self._out_q.put(None)
|
||||
self._ack_q.put(None)
|
||||
self.write_thread.join()
|
||||
self._logger.debug('Write thread terminated')
|
||||
|
||||
def add_handler(self, message_type, handler):
|
||||
with self._lock:
|
||||
self._message_handlers[message_type].append(handler)
|
||||
return lambda: self._remove_handler(message_type, handler)
|
||||
|
||||
def _remove_handler(self, message_type, handler):
|
||||
with self._lock:
|
||||
self._message_handlers[message_type].remove(handler)
|
||||
|
||||
def request_state(self):
|
||||
message = smartknob_pb2.ToSmartknob()
|
||||
message.request_state.SetInParent()
|
||||
self._enqueue_message(message)
|
||||
|
||||
def hard_reset(self):
|
||||
self._serial.setRTS(True)
|
||||
self._serial.setDTR(False)
|
||||
time.sleep(0.2)
|
||||
self._serial.setDTR(True)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def smartknob_context(serial_port, default_logging=True, wait_for_comms=True):
|
||||
with serial.Serial(serial_port, SMARTKNOB_BAUD, timeout=1.0) as ser:
|
||||
s = Smartknob(ser)
|
||||
s.start()
|
||||
|
||||
if default_logging:
|
||||
s.add_handler('log', lambda msg: s._logger.info(f'From smartknob: {msg.msg}'))
|
||||
|
||||
if wait_for_comms:
|
||||
s._logger.info('Connecting to smartknob...')
|
||||
q = Queue(1)
|
||||
def startup_handler(message):
|
||||
try:
|
||||
q.put_nowait(None)
|
||||
except Full:
|
||||
pass
|
||||
unregister = s.add_handler('smartknob_state', startup_handler)
|
||||
|
||||
s.request_state()
|
||||
q.get()
|
||||
unregister()
|
||||
s._logger.info('Connected!')
|
||||
|
||||
try:
|
||||
yield s
|
||||
finally:
|
||||
s.shutdown()
|
||||
|
||||
|
||||
def ask_for_serial_port():
|
||||
"""
|
||||
Helper function to ask which port to use via stdin
|
||||
"""
|
||||
print('Available ports:')
|
||||
ports = sorted(
|
||||
filter(
|
||||
lambda p: p.description != 'n/a',
|
||||
serial.tools.list_ports.comports(),
|
||||
),
|
||||
key=lambda p: p.device,
|
||||
)
|
||||
for i, port in enumerate(ports):
|
||||
print('[{: 2}] {} - {}'.format(i, port.device, port.description))
|
||||
print()
|
||||
value = input('Use which port? ')
|
||||
port_index = int(value)
|
||||
assert 0 <= port_index < len(ports)
|
||||
return ports[port_index].device
|
||||
2
thirdparty/nanopb
vendored
2
thirdparty/nanopb
vendored
@ -1 +1 @@
|
||||
Subproject commit 80f9d5bcbc0da72c95d3411b642c109a14298d75
|
||||
Subproject commit b97aa657a706d3ba4a9a6ccca7043c9d6fe41cba
|
||||
Loading…
Reference in New Issue
Block a user