From efe9bdc3e3cfd480430dac2364acc285cead9a14 Mon Sep 17 00:00:00 2001 From: Tyler Miller Date: Thu, 20 Jun 2024 02:53:54 -0400 Subject: [PATCH 01/13] Elecrow v2 export fixes (#239) PDFs generated for elecrow on the latest master currently look like this: ![image](https://github.com/scottbez1/splitflap/assets/15990762/8a188939-1e57-4fbf-9400-bb90268b4982) Dimensions appear to be placed incorrectly when the svg output is mirrored. I updated `add_dimensions` to account for mirroring when its enabled. I tested this in a 20.04 docker container setup similarly to the actions environment and the output looks correct. Result is below: [elecrow_dim_fix.pdf](https://github.com/user-attachments/files/15907775/elecrow_dim_fix.pdf) When testing locally, I also discovered that the inkscape cli has deprecated `--verbs` in favor of `--actions` in version 1.2+. This PR adds a check to handle both inkscape < 1.2 and >= 1.2 when generating elecrow pdfs --- 3d/scripts/generate_2d.py | 28 +++++++++++++++++++--------- 3d/scripts/svg_processor.py | 11 ++++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/3d/scripts/generate_2d.py b/3d/scripts/generate_2d.py index d74d0cdf..c791c4ce 100755 --- a/3d/scripts/generate_2d.py +++ b/3d/scripts/generate_2d.py @@ -162,18 +162,28 @@ def getDimensionSvgContents(text, width): elecrow_zip = os.path.join(laser_parts_directory, 'elecrow.zip') processor.apply_elecrow_style() - processor.add_dimensions(width_mm, height_mm) + processor.add_dimensions(width_mm, height_mm, args.mirror) processor.write(elecrow_svg) logging.info('Resize SVG canvas') - subprocess.check_call([ - app_paths.get('inkscape'), - '--verb=FitCanvasToDrawing', - '--verb=FileSave', - '--verb=FileClose', - '--verb=FileQuit', - elecrow_svg, - ]) + # since version 1.2, inkscape has replaced 'verbs' with 'actions' + # see: https://wiki.inkscape.org/wiki/Using_the_Command_Line + if inkscape._version() < 1.2: + subprocess.check_call([ + app_paths.get('inkscape'), + '--verb=FitCanvasToDrawing', + '--verb=FileSave', + '--verb=FileClose', + '--verb=FileQuit', + elecrow_svg, + ]) + else: + subprocess.check_call([ + app_paths.get('inkscape'), + "--actions=select-all;fit-canvas-to-selection;export-type:svg;export-plain-svg;export-do", + elecrow_svg, + "-o", elecrow_svg, + ]) logging.info('Output PDF') subprocess.check_call([ diff --git a/3d/scripts/svg_processor.py b/3d/scripts/svg_processor.py index d14d7163..80605d81 100644 --- a/3d/scripts/svg_processor.py +++ b/3d/scripts/svg_processor.py @@ -353,30 +353,31 @@ def add_highlight_lines(self, lines, color): self.svg_node.appendChild(new_path_node) - def add_dimensions(self, width_mm, height_mm): + def add_dimensions(self, width_mm, height_mm, mirror=False): width_node = self.dom.createElement("path") - width_node.setAttribute('d', f'M 0 10 l 0 5 l {width_mm} 0 l 0 -5') + mirror_dir = -1 if mirror else 1 + width_node.setAttribute('d', f'M 0 10 l 0 5 l {mirror_dir * width_mm} 0 l 0 -5') width_node.setAttribute('fill', 'none') width_node.setAttribute('stroke', '#ff00ff') width_node.setAttribute('stroke-width', '1') self.svg_node.appendChild(width_node) width_label_node = self.dom.createElement('text') - width_label_node.setAttribute('x', f'{width_mm / 2}') + width_label_node.setAttribute('x', f'{mirror_dir * width_mm / 2}') width_label_node.setAttribute('y', '25') width_label_node.setAttribute('style', 'font: 5px sans-serif; fill: #ff00ff; text-anchor: middle;') width_label_node.appendChild(self.dom.createTextNode(f'{width_mm:.2f} mm')) self.svg_node.appendChild(width_label_node) height_node = self.dom.createElement("path") - height_node.setAttribute('d', f'M -10 0 l -5 0 l 0 -{height_mm} l 5 0') + height_node.setAttribute('d', f'M {-width_mm - 10 if mirror else -10} 0 l -5 0 l 0 -{height_mm} l 5 0') height_node.setAttribute('fill', 'none') height_node.setAttribute('stroke', '#ff00ff') height_node.setAttribute('stroke-width', '1') self.svg_node.appendChild(height_node) height_label_node = self.dom.createElement('text') - height_label_node.setAttribute('x', '-20') + height_label_node.setAttribute('x', f'{-width_mm - 20 if mirror else -20}') height_label_node.setAttribute('y', f'{-height_mm / 2}') height_label_node.setAttribute('style', 'font: 5px sans-serif; fill: #ff00ff; text-anchor: end;') height_label_node.appendChild(self.dom.createTextNode(f'{height_mm:.2f} mm')) From 7f00bd3feaa124fb8afea52ea2cd40c6f11a0097 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Mon, 12 Aug 2024 21:48:35 -0700 Subject: [PATCH 02/13] [webserial example] Remove idle demo --- .../example-webserial-basic/src/App.tsx | 65 ------------------- 1 file changed, 65 deletions(-) diff --git a/software/chainlink/js/packages/example-webserial-basic/src/App.tsx b/software/chainlink/js/packages/example-webserial-basic/src/App.tsx index 65293db2..9b1ac394 100644 --- a/software/chainlink/js/packages/example-webserial-basic/src/App.tsx +++ b/software/chainlink/js/packages/example-webserial-basic/src/App.tsx @@ -22,48 +22,6 @@ const FLAP_COLOR_BLOCKS: Record = { 'y': '#ffd639', } -type MessageDelayMs = [string, number] - -const IDLE_MESSAGES_BY_LENGTH: Record = { - 6: [ - ['SPLIT', 6000], - [' FLAP', 6000], - [' BY', 6000], - ['BEZEK', 6000], - [' LABS', 12000], - ['', 2 * 60 * 1000], - ], - 12: [ - ['gprwygprwygp', 6000], - ['wwwwwwwwwwww', 5000], - [' wwwwwwwwwww', 100], - [' wwwwwwwwww', 100], - [' wwwwwwwww', 100], - [' wwwwwwww', 100], - [' wwwwwww', 100], - [' wwwwww', 100], - [' wwwww', 100], - [' wwww', 100], - [' www', 100], - [' ww', 100], - [' w', 100], - [' ', 500], - ['OPEN SOURCE ', 8000], - [' @SCOTTBEZ1', 12000], - [' ', 4000], - [' 999996 SUBS', 5000], - [' 999997 SUBS', 100], - [' 999998 SUBS', 600], - [' 999999 SUBS', 4000], - ['1000000 SUBS', 8000], - [' ', 4000], - ['COFFEE $3.49', 7000], - ['g BUILD PASS', 7000], - ['r TEST FAIL', 7000], - [' ', 4000], - ['COME TRY ME!', 2 * 60 * 1000], - ], -} type Config = NoUndefinedField @@ -235,29 +193,6 @@ export const App: React.FC = () => { }) }, [splitflapState.modules]) - const idleTimeout = useRef>(); - const curMessage = useRef(0); - const nextMessage = () => { - const idleMessages = IDLE_MESSAGES_BY_LENGTH[splitflapState.modules.length] - const m = idleMessages[(curMessage.current++) % idleMessages.length] - updateSplitflap(m[0], true) - idleTimeout.current = setTimeout(nextMessage, m[1]) - } - useEffect(() => { - if (!inputValue.user) { - return - } - const t = idleTimeout.current - if (t) { - clearTimeout(t) - curMessage.current = 0 - } - idleTimeout.current = setTimeout(() => { - setInputValue({val:'', user: false}) - nextMessage() - }, 1 * 60 * 1000) - }, [inputValue, splitflapState.modules.length]) - const numModules = splitflapState.modules.length const charWidth = Math.max(1000 / numModules, 40) From 779d44670607fe0cb47753ceeb8369b5a7c9b827 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Mon, 12 Aug 2024 23:13:24 -0700 Subject: [PATCH 03/13] More README updates in preparation for v2 launch --- README.md | 117 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 46c3b264..ba585f91 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Note: the ordering and assembly guides have not yet been updated for the beta v2 - [Classic controller](#classic-controller-electronics-deprecated) + [Miscellaneous Tools](#miscellaneous-tools) - [Flaps and Fonts](#flaps-and-fonts) + - [Combined Front Panel Generator](#combined-front-panel-generator) - [3D Printed Tools](#3d-printed-tools) - [Chainlink Driver Tester](#chainlink-driver-tester) * [Code](#code) @@ -109,7 +110,7 @@ The mechanical/structural components are made from laser-cut 3mm MDF or acrylic, You can view an interactive 3d model of the design [here](https://scottbez1.github.io/splitflap/embed.html?branch=master). -The beta v2 mechanical design officially supports variants with 52 flaps (perfect for use with the new ["Epilogue" printed flaps](https://www.etsy.com/listing/1685633114/)) and 40 flaps. But you can always modify the design to customize it further. +The beta v2 mechanical design officially supports variants with 52 flaps (perfect for use with the new ["Epilogue" printed flaps](https://bezeklabs.etsy.com/listing/1685633114/)) and 40 flaps. But you can always modify the design to customize it further. ### Stable v0.7 (40-flap modules) ![2d laser cut rendering](https://s3.amazonaws.com/splitflap-artifacts/refs/tags/v0.7/3d/3d_laser_raster.png) @@ -170,19 +171,18 @@ power management/distribution and fault monitoring, UART and RS-485 connections, Each module needs a hall-effect sensor for start-up calibration and fault monitoring. #### Sensors for stable v0.7 hardware -Older sensors for the v0.7 and older laser-cut hardware can be found in the [tagged sensor release](https://github.com/scottbez1/splitflap/releases/tag/releases%2Fsensor%2Fv1.1) +Older sensors for the v0.7 and older laser-cut hardware can be found in the [tagged sensor release](https://github.com/scottbez1/splitflap/releases/tag/releases%2Fsensor%2Fv1.1). + +These older sensors are not compatible with v2 laser-cut hardware. -#### Sensors for v0.7 legacy laser-cut hardware -Older sensors for the v0.7 and older laser-cut hardware can be found in the [tagged sensor release](https://github.com/scottbez1/splitflap/releases/tag/releases%2Fsensor%2Fv1.1) - - - - +#### Beta Sensors v2 + + @@ -190,9 +190,14 @@ Older sensors for the v0.7 and older laser-cut hardware can be found in the [tag - + +New sensors for the v2 laser-cut hardware - these use surface mount components and are optimized for PCB assembly at JLCPCB. These new sensors are not compatible with v0.7 and older laser-cut hardware. + +Packs of 6 sensors are [available mostly-assembled in the Bezek Labs store](https://bezeklabs.etsy.com/listing/1696745674), +and come with the right-angle pin headers and magnets you'll need. Purchases support continued development of this project. + Latest auto-generated (untested!) artifacts:warning:: * Schematic [pdf](https://s3.amazonaws.com/splitflap-artifacts/master/electronics-v2/sensor_smd-schematic.pdf) @@ -207,6 +212,7 @@ Latest auto-generated (untested!) artifacts:warning:: * PCB gerbers [zip](https://s3.amazonaws.com/splitflap-artifacts/master/electronics-v2/sensor_smd-panelized-jlc/gerbers.zip) * PCB BOM (for JLCPCB assembly) [csv](https://s3.amazonaws.com/splitflap-artifacts/master/electronics-v2/sensor_smd-panelized-jlc/bom.csv) * PCB CPL (for JLCPCB assembly) [csv](https://s3.amazonaws.com/splitflap-artifacts/master/electronics-v2/sensor_smd-panelized-jlc/pos.csv) +* Purchase sensor kits in the US: [Bezek Labs](https://bezeklabs.etsy.com/listing/1696745674) :warning:For tested/stable/recommended artifacts, use the [latest release](https://github.com/scottbez1/splitflap/releases) instead @@ -224,7 +230,7 @@ for easy SMD/THT assembly to validate data integrity up and down the whole chain * Module order goes from right-to-left since this is intended to be installed and accessed from *behind* the modules -Chainlink Driver boards are [available mostly-assembled in the Bezek Labs store](https://www.etsy.com/listing/1123280069/splitflap-chainlink-driver-v11), +Chainlink Driver boards are [available mostly-assembled in the Bezek Labs store](https://bezeklabs.etsy.com/listing/1123280069/splitflap-chainlink-driver-v11), and come with the additional connectors and ribbon cables you'll need. Purchases support continued development of this project. More information on building and using Chainlink Drivers is available in the [Chainlink Driver User Guide](https://paper.dropbox.com/doc/Chainlink-Driver-v1.1-Electronics-User-Guide--BW2lxdjVkAxva68kYw2doWQEAg-U0DAXrSxEoOhgSoRU39hq). @@ -272,7 +278,7 @@ and then run a wire from the onboard screw terminals to the Chainlink Driver's m * Optional 5V regulator allows for powering the ESP32 without a USB connection, using the 12V motor power supply -Chainlink Buddy \[T-Display\] boards are [available in the Bezek Labs store](https://www.etsy.com/listing/1109357786/splitflap-chainlink-buddy-t-display), +Chainlink Buddy \[T-Display\] boards are [available in the Bezek Labs store](https://bezeklabs.etsy.com/listing/1109357786/splitflap-chainlink-buddy-t-display), and come with the additional connectors you'll need. Purchases support continued development of this project. @@ -302,7 +308,7 @@ Latest auto-generated (untested!) artifacts:warning:: The Chainlink Buddy \[Breadboard\] makes it easy to connect a Chainlink Driver to a breadboard for prototyping. You could use 5 dupont wires and have a messy rats nest, or you could use a single ribbon cable and this slick breakout board. -Chainlink Buddy \[Breadboard\] boards are [available in the Bezek Labs store](https://www.etsy.com/listing/1123863267/splitflap-chainlink-buddy-breadboard), +Chainlink Buddy \[Breadboard\] boards are [available in the Bezek Labs store](https://bezeklabs.etsy.com/listing/1123863267/splitflap-chainlink-buddy-breadboard), and come with the additional connectors you'll need. Purchases support continued development of this project. @@ -392,6 +398,20 @@ that is extremely configurable: * Front/back - for batch duplex printing, generate separate front-side and back-side files (e.g. sign shop printing on a flat sheet of PVC) * Side-by-side - for individual flap printing, each flap's front design is laid out side-by-side with its back design +TODO: finish documenting this and render some example images... + +#### Combined Front Panel Generator +If you'd like to share a single front face across multiple modules (rather than each module having its own front face), the repo +includes a script to generate a combined front panel for laser-cutting or CNC milling/routing. + +You can modify: +* Number of rows and columns +* Horizontal and vertical spacing/separation of modules +* Overall outer width and height of the panel + +For CNC cutting, the script supports rendering a vector file optimized for thicker material (e.g. 6mm MDF) where only the bolt-holes will be through-cut. In this mode, the slots for the top/bottom enclosure pieces can be cut as ~4mm pockets so they aren't visible from the front face. The script automatically generates dog-bone shapes for these pocket cuts. + + TODO: finish documenting this and render some example images... #### 3D Printed Tools @@ -454,53 +474,56 @@ The driver firmware is written using PlatformIO with the Arduino framework and i The firmware implements a closed-loop controller that accepts letters as input over USB serial and drives the stepper motors using a precomputed acceleration ramp for smooth control. The firmware automatically calibrates the spool position at startup, using the hall-effect magnetic sensor, and will automatically recalibrate itself if it ever detects that the spool position has gotten out of sync. If a commanded rotation is expected to bring the spool past the "home" position, it will confirm that the sensor is triggered neither too early nor too late; otherwise it will search for the "home" position to get in sync before continuing to the desired letter. -### Computer Control Software -The display can be controlled by a computer connected to the Arduino over USB serial. A basic python library for interfacing with the Arduino and a demo application that displays random words can be found in the [software](software) directory. +### Serial protocol +In order for a computer to communicate with the splitflap, it appears as a USB serial device. -Commands to the display are sent in a basic plain-text format, and messages _from_ the display are single-line JSON objects, always with a `type` entry describing which type of message it is. +However, usage of Arduino’s `Serial` is strictly forbidden, and instead a `logger` abstraction is provided for sending basic text debug logs. Other data is transferred in a structured way, described below. -When the Arduino starts up, it sends an initialization message that looks like: -``` -{"type":"init", "num_modules":4} -``` +This allows flexibility in the format of data transferred over serial, and in fact the splitflap provides 2 different serial modes that serve different purposes. -The display will automatically calibrate all modules, and when complete it will send a status update message: -``` -{ - "type":"status", - "modules":[ - {"state":"normal", "flap":" ", "count_missed_home":0, "count_unexpected_home":0}, - {"state":"sensor_error", "flap":"e", "count_missed_home":0, "count_unexpected_home":0}, - {"state":"sensor_error", "flap":"e", "count_missed_home":0, "count_unexpected_home":0}, - {"state":"sensor_error", "flap":"e", "count_missed_home":0, "count_unexpected_home":0} - ] -} -``` -(Note: this is sent as a single line, but has been reformatted for readability above) -In this case the Arduino was programmed to support 4 modules, but only 1 module is connected, so the other 3 end up in `"sensor_error"` state. More on status updates below. +#### Plaintext mode -At this point you can command the display to show some letters. To do this, send a message to the Arduino that looks like this: +By default, it starts in “plaintext” mode, which is developer-friendly and you’re probably familiar with if you’ve opened a serial monitor with the splitflap connected: ``` -=hiya\n +{"type":"init", "num_modules":6} ``` -The `=` indicates a movement command, followed by any number of letters, followed by a newline. You don't have to send the exact number of modules - if you send fewer letters than modules, only the first N modules will be updated and the remainder won't move. For instance, you could send `=a\n` as shorthand to only set the first module (even if there are 12 modules connected). Any letters that can't be displayed are considered a no-op for that module. -Whenever ALL modules come to a stop, the Arduino will send a status update message (just like the one following initialization, shown above). Here's what the fields mean in each module's status entry: -- **state** - `normal` indicates it's working as intended, `sensor_error` indicates the module can't find the home position and has given up trying (it will no longer respond to movement commands until told to recalibrate - see below). `panic` indicates the firmware detected a programming bug and has gone into failsafe mode (it will no longer respond to movement commands and requires a full reset of the Arduino to recover - should never happen). -- **flap** - which letter is shown by this module -- **count\_missed\_home** - number of times the module expected to pass the home position but failed to detect it. If this is non-zero, it indicates either a flaky sensor or that the motor may have jammed up. The module automatically attempts to recalibrate whenever it misses the home position, so if this number is non-zero and the module is still in the `normal` state, it means the module successfully recovered from the issue(s). However, if this number keeps going up over continued use, it may indicate a recurrent transient issue that warrants investigation. -- **count\_unexpected\_home** - number of times the module detected the home position when it wasn't supposed to. This is rare, but would indicate a flaky/broken sensor that is tripping at the wrong time. Just like with missed home errors, unexpected home errors will cause the module to attempt to recalibrate itself. +However, this isn’t great for programmatically configuring or receiving updates from the splitflap, so instead the firmware offers a programmatic interface using a binary protocol based on Google’s Protobuf standard. -If you want to make all modules recalibrate their home position, send a single @ symbol (no newline follows): -``` -@ -``` -This recalibrates all modules, including any that were in the `sensor_error` state; if recalibration succeeds they will return to the `normal` state and start responding to movement commands again. +#### Protobuf (binary/programmatic) mode + +The protobuf-based binary serial mode is a compact and flexible way to transfer structured data from the host computer to the splitflap and vice-versa. + + +##### Benefits of protobuf +protobuf provides several benefits over other encoding mechanisms like JSON: + +1. Well-defined schema. If you’re curious about the format of data to expect or send, you just need to check the protobuf file +2. Code generation. Instead of hand-writing JSON parsers every time the data changes, protobuf provides code generation of the encoding and decoding logic, and data-structures. Splitflap uses nanopb to generate C structs based off the schema, and all the code for encoding/decoding that data from the binary format. +3. Relatively compact/efficient wire encoding. It’s not a primary goal in this project, but the binary wire encoding is generally fairly compact, due to omitting default/unspecified fields, using variable length encodings, etc. It’s certainly much more compact than JSON which uses strings to describe every field in every message. +4. Backwards/forwards compatibility. Not super relevant to this project, but many common schema changes are backwards and forwards compatible, meaning an older client or a newer client will be able to handle them gracefully. +##### Disadvantages of protobuf + +1. Not human-readable. This makes it much harder to debug, as you need something that can interpret messages. Since they’re binary, viewing anything in a terminal directly will be fruitless. +2. Not self-describing. If you come across a JSON document, you can generally tell what it means because fields have string names/labels describing their contents. Protobuf has no such thing (it uses integer field numbers, which does make renaming fields easier) so you need to have a copy of the schema (.proto file - and you’d better hope it matches the data!) in order to understand an encoded message. + +This is why the splitflap defaults to plaintext mode to make basic validation/debugging easier. + +##### How it works +Protobuf messages are encoded to their binary wire format and a CRC32 checksum appended. Then that entire binary string is COBS encoded into a packet, and delimited/framed by 0 (NULL) bytes when sent over serial. This provides a basic packet-based interface with integrity checks (rather than the raw, stream-based interface of a serial connection). + +The splitflap automatically switches to binary protobuf mode when it receives a 0 byte. + + +### Computer Control Software +The display can be controlled by a computer connected to the ESP32 over USB serial. If you've built a display and want to test it out, check out the web-based demo [here](https://scottbez1.github.io/splitflap) which will connect to your display using USB - no applications/installation necessary! +The firmware supports a plaintext serial mode (enabled by default) for ease of testing, and a protobuf-based binary mode used by the software libraries for enhanced programmatic control and feedback. +You can find example Typescript and Python libraries in the [`software/chainlink`](software/chainlink) folder. # Contributing/Modifying @@ -562,7 +585,7 @@ I'd love to hear your thoughts and questions about this project, and happy to in This project is licensed under Apache v2 (see [LICENSE.txt](LICENSE.txt)). - Copyright 2015-2021 Scott Bezek and the splitflap contributors + Copyright 2015-2024 Scott Bezek and the splitflap contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From e89f35adc2a8d8cf842fe5c0e88be590dd84bcf9 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Fri, 16 Aug 2024 20:46:14 -0700 Subject: [PATCH 04/13] Fix generate_combined_front_panel and generate_fonts --- 3d/scripts/generate_combined_front_panel.py | 2 +- 3d/scripts/generate_fonts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/3d/scripts/generate_combined_front_panel.py b/3d/scripts/generate_combined_front_panel.py index 5e694bb4..3e3ea218 100755 --- a/3d/scripts/generate_combined_front_panel.py +++ b/3d/scripts/generate_combined_front_panel.py @@ -40,7 +40,7 @@ def render(extra_variables, output_directory): renderer = Renderer(os.path.join(source_parts_dir, 'combined_front_panel.scad'), output_directory, extra_variables) renderer.clean() - svg_output = renderer.render_svgs(panelize_quantity = 1) + svg_output, _ = renderer.render_svgs(panelize_quantity = 1) logging.info('\n\n\nDone rendering to SVG: ' + svg_output) if __name__ == '__main__': diff --git a/3d/scripts/generate_fonts.py b/3d/scripts/generate_fonts.py index b2bd4b60..7d0a5078 100755 --- a/3d/scripts/generate_fonts.py +++ b/3d/scripts/generate_fonts.py @@ -44,7 +44,7 @@ def render(extra_variables, skip_optimize, output_directory): renderer = Renderer(os.path.join(source_parts_dir, 'font_generator.scad'), output_directory, extra_variables) renderer.clean() - svg_output = renderer.render_svgs(panelize_quantity = 1) + svg_output, _ = renderer.render_svgs(panelize_quantity = 1) processor = SvgProcessor(svg_output) From edee41aa22253d5a4457705dd068433608d30a0b Mon Sep 17 00:00:00 2001 From: Steven Shamlian Date: Sat, 5 Oct 2024 18:05:33 -0400 Subject: [PATCH 05/13] Update Python example (and v2 character set) for new features (#242) - Replaced `~` with `@` in default v2 character set to match shipped flaps - Get character set from splitflap display (if possible, otherwise use previous legacy alphabet) - Use upper case in demo words to match new character sets --- arduino/splitflap/Splitflap/config.h | 2 +- software/chainlink/demo.py | 8 ++++---- software/chainlink/splitflap_proto.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/arduino/splitflap/Splitflap/config.h b/arduino/splitflap/Splitflap/config.h index 809b76d3..bbf1eb44 100644 --- a/arduino/splitflap/Splitflap/config.h +++ b/arduino/splitflap/Splitflap/config.h @@ -60,7 +60,7 @@ const uint8_t flaps[NUM_FLAPS] = { ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'g', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'r', - '.', '?', '-', '$', '\'', '#', 'y', 'p', ',', '!', '~', '&', 'w' + '.', '?', '-', '$', '\'', '#', 'y', 'p', ',', '!', '@', '&', 'w' }; // Flap option 3: v2 flaps (limited 40-flap set using the first 40 flaps of the set) diff --git a/software/chainlink/demo.py b/software/chainlink/demo.py index 85b56db3..160b1661 100644 --- a/software/chainlink/demo.py +++ b/software/chainlink/demo.py @@ -7,10 +7,10 @@ ) words = [ - 'alpaca', 'baboon', 'badger', 'beluga', 'bobcat', 'ferret', - 'gopher', 'impala', 'jackal', 'jaguar', 'kitten', 'marmot', - 'monkey', 'ocelot', 'rabbit', 'racoon', 'turtle', 'walrus', - 'weasel', 'wombat', + 'ALPACA', 'BABOON', 'BADGER', 'BELUGA', 'BOBCAT', 'FERRET', + 'GOPHER', 'IMPALA', 'JACKAL', 'JAGUAR', 'KITTEN', 'MARMOT', + 'MONKEY', 'OCELOT', 'RABBIT', 'RACOON', 'TURTLE', 'WALRUS', + 'WEASEL', 'WOMBAT', ] diff --git a/software/chainlink/splitflap_proto.py b/software/chainlink/splitflap_proto.py index e2cd881a..fa76c71b 100644 --- a/software/chainlink/splitflap_proto.py +++ b/software/chainlink/splitflap_proto.py @@ -41,15 +41,11 @@ class ForceMovement(Enum): RETRY_TIMEOUT = 0.25 - # TODO: read alphabet from splitflap once this is possible - _DEFAULT_ALPHABET = [ - ' ', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '.', - ',', - '\'', + _LEGACY_ALPHABET = [ + ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', + '3', '4', '5', '6', '7', '8', '9', '.', ',', "'", ] def __init__(self, serial_instance): @@ -66,7 +62,8 @@ def __init__(self, serial_instance): self._current_config = splitflap_pb2.SplitflapConfig() self._num_modules = None - self._alphabet = Splitflap._DEFAULT_ALPHABET + self._alphabet = Splitflap._LEGACY_ALPHABET + self._alphabet_received = False def _read_loop(self): self._logger.debug('Read loop started') @@ -125,6 +122,9 @@ def _process_frame(self, frame): self._current_config.modules.append(splitflap_pb2.SplitflapConfig.ModuleConfig()) else: assert self._num_modules == num_modules_reported, f'Number of reported modules changed (was {self._num_modules}, now {num_modules_reported})' + elif payload_type == 'general_state' and not self._alphabet_received: + self._alphabet_received = True + self._alphabet = list(message.general_state.flap_character_set.decode('utf-8')) with self._lock: for handler in self._message_handlers[payload_type] + self._message_handlers[None]: From 938dfbc3f1b67425dda5d41c297db71c42a1c03e Mon Sep 17 00:00:00 2001 From: Johnr24 Date: Sat, 5 Oct 2024 23:06:41 +0100 Subject: [PATCH 06/13] Update mqtt_task.cpp - added Home Assistant discovery schema (#240) now the split flap will automatically add itself to home assistant as a text entity. --- arduino/splitflap/esp32/splitflap/mqtt_task.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arduino/splitflap/esp32/splitflap/mqtt_task.cpp b/arduino/splitflap/esp32/splitflap/mqtt_task.cpp index 6e68283c..4045b70b 100644 --- a/arduino/splitflap/esp32/splitflap/mqtt_task.cpp +++ b/arduino/splitflap/esp32/splitflap/mqtt_task.cpp @@ -55,6 +55,10 @@ void MQTTTask::connectMQTT() { if (mqtt_client_.connect(HOSTNAME "-" MQTT_USER, MQTT_USER, MQTT_PASSWORD)) { logger_.log("MQTT connected"); mqtt_client_.subscribe(MQTT_COMMAND_TOPIC); + char buf[256]; + snprintf(buf, sizeof(buf), "{\"name\": \"%s\", \"command_topic\": \"%s\", \"state_topic\": \"%s\", \"unique_id\": \"%s\"}", HOSTNAME, MQTT_COMMAND_TOPIC, MQTT_COMMAND_TOPIC, HOSTNAME); + mqtt_client_.publish("homeassistant/text/splitflap/config", buf); + logger_.log("Published MQTT discovery message"); } else { snprintf(buf, sizeof(buf), "MQTT failed rc=%d will try again in 5 seconds", mqtt_client_.state()); logger_.log(buf); @@ -76,4 +80,5 @@ void MQTTTask::run() { delay(1); } } + #endif From ccad061552b6d025c77c9b5fa63c4fc15d8a4919 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Sat, 5 Oct 2024 15:28:32 -0700 Subject: [PATCH 07/13] Update upload-artifact actions (#243) Old version is deprecated and failing CI --- .github/workflows/3d.yml | 2 +- .github/workflows/electronics.yml | 12 ++++++------ .github/workflows/electronics_v2.yml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/3d.yml b/.github/workflows/3d.yml index dd4e2111..7d1065ee 100644 --- a/.github/workflows/3d.yml +++ b/.github/workflows/3d.yml @@ -100,7 +100,7 @@ jobs: gzip 3d/build/outputs/3d_colored_stl/*.stl - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: 3d diff --git a/.github/workflows/electronics.yml b/.github/workflows/electronics.yml index 7ece8145..7d0f025a 100644 --- a/.github/workflows/electronics.yml +++ b/.github/workflows/electronics.yml @@ -65,7 +65,7 @@ jobs: PYTHONUNBUFFERED: 1 - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-classic @@ -150,7 +150,7 @@ jobs: run: cp -r electronics/build/bom electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-chainlink @@ -237,7 +237,7 @@ jobs: run: cp -r electronics/build/bom electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-chainlink-tester @@ -322,7 +322,7 @@ jobs: run: cp -r electronics/build/bom electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-chainlink-base @@ -430,7 +430,7 @@ jobs: run: cp -r electronics/build/bom electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-chainlink-buddy-t-display @@ -538,7 +538,7 @@ jobs: run: cp -r electronics/build/bom electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-chainlink-buddy-breadboard diff --git a/.github/workflows/electronics_v2.yml b/.github/workflows/electronics_v2.yml index d00ecd7d..9ed579ca 100644 --- a/.github/workflows/electronics_v2.yml +++ b/.github/workflows/electronics_v2.yml @@ -87,7 +87,7 @@ jobs: ls -lah electronics/build/outputs - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: electronics-v2 From 40a672a4ea011ec180d7f41b0969cd6131ceb479 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Sat, 5 Oct 2024 17:29:26 -0700 Subject: [PATCH 08/13] Remove deprecated reference to pcb_case.scad --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ba585f91..b48adaeb 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,6 @@ The project also includes a number of optional 3D printed designs to make assemb * [a flap scoring jig](3d/tools/scoring_jig.scad) for precisely marking the cut point when splitting CR80 cards * [a flap punch jig](3d/tools/punch_jig.scad) for aligning the punch when making the pin cutouts on either side of a flap * [a flap container](3d/tools/flap_container.scad) for storing and organizing stacks of completed flaps -* [a sensor PCB holder](3d/tools/pcb_case.scad) for storing and protecting soldered sensor boards All of these designs are parametric and customizable within OpenSCAD. To print them, open up the relevant file in OpenSCAD and use `File -> Export -> Export as STL` to render the design as an STL file for your slicer. From 2410367a2b4630f59a1afad04f3b5ed4c86f5a49 Mon Sep 17 00:00:00 2001 From: Scott Bezek Date: Sat, 5 Oct 2024 17:49:14 -0700 Subject: [PATCH 09/13] Fix line optimizations for newer svg.path (#244) Fixes #115 The fix from #116 was previously applied but wasn't quite working on svg.path 6.3, and it looks like the issue was that svg.path doesn't enforce/check consistency between adjacent path elements now that Moves are tracked separately from Lines, so after modifying lines and omitting Moves, the `d` string generated for the Path didn't have correct Moves to handle the gaps between adjacent Line start/ends. This change continues treating Close as a Line (and continues omitting Moves from the original path), and now inserts Move elements in the reconstructed Paths when there's a mismatch between adjacent Line end/start points. --- 3d/scripts/requirements.txt | 2 +- 3d/scripts/svg_processor.py | 47 +++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/3d/scripts/requirements.txt b/3d/scripts/requirements.txt index cb4aa3ee..df964d2c 100644 --- a/3d/scripts/requirements.txt +++ b/3d/scripts/requirements.txt @@ -1 +1 @@ -svg.path==4.0.2 +svg.path==6.3 diff --git a/3d/scripts/svg_processor.py b/3d/scripts/svg_processor.py index 80605d81..e83632e1 100644 --- a/3d/scripts/svg_processor.py +++ b/3d/scripts/svg_processor.py @@ -15,6 +15,8 @@ from __future__ import print_function from collections import defaultdict import os + +svg_path_install = f'python3 -m pip install -r {os.path.join(os.path.dirname(__file__), "requirements.txt")}' try: from svg.path import ( Path, @@ -24,7 +26,11 @@ parse_path, ) except: - raise RuntimeError(f'Error loading svg.path library. Run "python3 -m pip install -r {os.path.join(os.path.dirname(__file__), "requirements.txt")}" to install it') + raise RuntimeError(f'Error loading svg.path library. Run "{svg_path_install}" to install it') + +from importlib.metadata import version +assert int(version('svg.path').split('.')[0]) == 6, f'svg.path library is not new enough (found {version("svg.path")}). Run "{svg_path_install}" to install it' + from xml.dom import minidom @@ -159,10 +165,11 @@ def remove_redundant_lines(self): path_text = path.attributes['d'].value path_obj = parse_path(path_text) for line_index, line in enumerate(path_obj): - # Moves don't draw anything by themselves, but they do set the - # target for subsequent closes, so they should not be removed. - slope, intersect = _get_slope_intersect(line.start, line.end) - if not isinstance(line, Move): + if isinstance(line, Close): + # Treat Close as a Line + line = Line(line.start, line.end) + if isinstance(line, Line): + slope, intersect = _get_slope_intersect(line.start, line.end) # TODO: float inaccuracy and rounding may cause collinear lines to end up in separate buckets in rare # cases, so this is not quite correct. Would be better to put lines into *2* nearest buckets in each # dimension to avoid edge cases. @@ -203,12 +210,17 @@ def remove_redundant_lines(self): filtered_path = Path() for line_index, line in enumerate(path_obj): + if isinstance(line, Close): + # Treat Close as a Line + line = Line(line.start, line.end) if i in to_remove: + assert isinstance(line, Line) assert path_index == to_remove[i][0] assert line_index == to_remove[i][1] removed += 1 removed_length += line.length() elif i in to_update: + assert isinstance(line, Line) assert path_index == to_update[i][0] assert line_index == to_update[i][1] replacement_line = to_update[i][2] @@ -216,22 +228,27 @@ def remove_redundant_lines(self): filtered_path.append(replacement_line) kept += 1 kept_length += replacement_line.length() - elif isinstance(line, Close): - # Replace the close with a line, because if we removed all - # or part of the previous line in this path, a close will - # not work as expected. - new_line = Line(line.start, line.end) - filtered_path.append(new_line) - kept += 1 - kept_length += new_line.length() - else: + elif isinstance(line, Line): filtered_path.append(line) kept += 1 kept_length += line.length() + else: + print(f'Omitting non-line in reconstructed path at index {line_index}: {line!r}') i += 1 # Update the path data with the filtered path data - path.attributes['d'] = filtered_path.d() + last_line = None + new_path = Path() + for line in filtered_path: + # Newer versions of svg.path keep Moves but don't ensure consistency with line start/ends. We've stripped non-Lines above, so + # we need to add back Moves where Line segments are not connected + if last_line is None or abs(last_line.end.real - line.start.real) > 0.001 or abs(last_line.end.imag - line.start.imag) > 0.001: + new_path.append(Move(line.start)) + new_path.append(line) + last_line = line + + path.attributes['d'] = new_path.d() + print(f'Optimized line path from\n {path_text!r}\n to\n {path.attributes["d"].value!r}') print('Removed {} lines ({} length) and kept {} lines ({} length)'.format( removed, From 13046a86f8e893701551e08947f1a6e90abd6382 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Sun, 6 Oct 2024 01:56:19 +0100 Subject: [PATCH 10/13] flap_container.scad: use flap.scad (#235) Otherwise we get warnings, and things won't render! ``` WARNING: Ignoring unknown module 'flap_2d' in file flap_container.scad, line 76 WARNING: Ignoring unknown module 'flap_2d' in file flap_container.scad, line 118 ``` --- 3d/tools/flap_container.scad | 1 + 1 file changed, 1 insertion(+) diff --git a/3d/tools/flap_container.scad b/3d/tools/flap_container.scad index ba3f895d..2613eef0 100644 --- a/3d/tools/flap_container.scad +++ b/3d/tools/flap_container.scad @@ -16,6 +16,7 @@ include <../flap_dimensions.scad> use <../splitflap.scad> +use <../flap.scad> num_flaps = 40; containers_x = 1; From fa9a7bfdf7e0410bb31c7782e0686a0656359ba5 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Sun, 6 Oct 2024 01:59:44 +0100 Subject: [PATCH 11/13] README.md: Fix a couple of typos (#233) Co-authored-by: Scott Bezek --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b48adaeb..bb9703fa 100644 --- a/README.md +++ b/README.md @@ -335,7 +335,7 @@ Latest auto-generated (untested!) artifacts:warning:: For larger displays, you should take additional care to make the hardware more robust to potential faults. The Chainlink Base is an experimental (but unsupported) controller design that adds some additional functionality. This has been tested and appears to work, but is not recommended for general use. -The Chainlink Base PCB is an optional alternative to a Chainink Buddy, designed for particularly large displays. +The Chainlink Base PCB is an optional alternative to a Chainlink Buddy, designed for particularly large displays. It hosts the ESP32 and adds additional connectivity options (terminals for UART and RS485 serial) and power distribution (independently-monitored power channels for multiple "zones" of Driver boards). From e1eb9b9cef89c13da10bc11dcd907621c58a51a0 Mon Sep 17 00:00:00 2001 From: Roland Date: Sat, 5 Oct 2024 19:02:57 -0600 Subject: [PATCH 12/13] adding some documentation and a python requirements.txt for ease of use (#232) Hi, I was trying to figure out how to use the demo and thought a little bit of additional documentation could be helpful. --- software/chainlink/README.md | 25 +++++++++++++++++++++++++ software/chainlink/requirements.txt | 1 + 2 files changed, 26 insertions(+) create mode 100644 software/chainlink/README.md create mode 100644 software/chainlink/requirements.txt diff --git a/software/chainlink/README.md b/software/chainlink/README.md new file mode 100644 index 00000000..98f23ef2 --- /dev/null +++ b/software/chainlink/README.md @@ -0,0 +1,25 @@ +# Chainlink software + +## Python Code + +Run the demo by installing the requirement dependencies outlined in the [requirements.txt](./requirements.txt). + +### Running the demo + +The demo will just send multiple different words to the splitflap board. You may start the demo by executing: + +```bash +python demo.py +``` + +You may get an output looking like this: + +```bash +Available ports: +[ 0] /dev/ttyACM0 - USB Single Serial +[ 1] /dev/ttyAMA0 - ttyAMA0 +``` + +Select which port the demo script should execute. + +Now the demo should send different words to the splitflap board with short breaks in between them. diff --git a/software/chainlink/requirements.txt b/software/chainlink/requirements.txt new file mode 100644 index 00000000..95d74a67 --- /dev/null +++ b/software/chainlink/requirements.txt @@ -0,0 +1 @@ +protobuf==3.18.3 \ No newline at end of file From 8db0a90b5b3824a570d532d53c72a17426355a36 Mon Sep 17 00:00:00 2001 From: Ted M Lin Date: Sat, 5 Oct 2024 21:04:57 -0400 Subject: [PATCH 13/13] Fix buffer overrun in showString() (#224) Found a crash by sending a too-long string via MQTT. I assume the check in `showString()` was just forgotten, rather than intentional. --- arduino/splitflap/esp32/core/splitflap_task.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino/splitflap/esp32/core/splitflap_task.cpp b/arduino/splitflap/esp32/core/splitflap_task.cpp index 0497ecd9..e9b39534 100644 --- a/arduino/splitflap/esp32/core/splitflap_task.cpp +++ b/arduino/splitflap/esp32/core/splitflap_task.cpp @@ -376,7 +376,7 @@ void SplitflapTask::log(const char* msg) { void SplitflapTask::showString(const char* str, uint8_t length, bool force_full_rotation) { Command command = {}; command.command_type = CommandType::MODULES; - for (uint8_t i = 0; i < length; i++) { + for (uint8_t i = 0; i < length && i < NUM_MODULES; i++) { int8_t index = findFlapIndex(str[i]); if (index != -1) { if (force_full_rotation || index != modules[i]->GetTargetFlapIndex()) {