Compare commits

..
Author SHA1 Message Date
Snacj 252fb31d65 Fixed merge issues 2025-10-31 13:24:56 +01:00
snacj bf12a25af4 Fixed merge issues 2025-10-31 12:32:15 +01:00
snacj c66f046a9b Fixed Formatting 2025-10-31 12:31:40 +01:00
snacj a7b1158f8d Fixed merge issues. 2025-10-31 12:30:53 +01:00
snacj be87b377ba Changed frontend Formatting 2025-10-31 12:29:23 +01:00
snacj 111664185e Fixed Formatting 2025-10-31 12:29:23 +01:00
snacj 29ccaea7f2 fixed formatting 2025-10-31 12:29:23 +01:00
snacj 8402199601 Fixed Merge issues 2025-10-31 12:29:23 +01:00
snacj b7399c8d82 Fixed Formula 2025-10-31 12:29:23 +01:00
snacj db71805912 Changed P Controller to Pi controller with dead time 2025-10-31 12:29:23 +01:00
snacj 3fd1b09589 Added Pi and Ei to the update logic. 2025-10-31 12:29:23 +01:00
snacj c806f59df5 Changed P Controller to be Pi Controller. 2025-10-31 12:29:23 +01:00
snacj a5c0d75f33 Fixed Clippy and Formatting 2025-10-31 12:29:23 +01:00
snacj 9629652d4f fixed merge issue 2025-10-31 12:29:23 +01:00
snacj 5ee0b7e61b Fixed Clippy 2025-10-31 12:29:23 +01:00
snacj bb975081e8 Testing fix for frontend freeze 2025-10-31 12:29:23 +01:00
snacj 6629055f9f Fixed clippy 2025-10-31 12:29:23 +01:00
snacj 3b10790146 Removed unwrap and fixed clippy 2025-10-31 12:29:23 +01:00
snacj cb6a954e3b Removed inline import 2025-10-31 12:29:23 +01:00
snacj bbf58e6b18 Fixed Error Pid to P 2025-10-31 12:29:23 +01:00
snacj 9af2364528 Fixed Merge Issue 2025-10-31 12:29:23 +01:00
snacj 7d774b0025 Fixed Merge issue 2025-10-31 12:29:23 +01:00
snacj 94655093cc Added Algorithm Only speed calculation for Diameter Regulation 2025-10-31 12:29:23 +01:00
snacj 5b57273153 Removed logs for testing 2025-10-31 12:29:23 +01:00
snacj 02951794f1 Fixed issues with kp scaling factor 2025-10-31 12:29:23 +01:00
snacj d1f96cc816 changed PID controller to P controller 2025-10-31 12:29:23 +01:00
snacj 673b85420c Implemented deadtime_p_controller to regulate puller speed by filament
diameter.
2025-10-31 12:29:23 +01:00
snacj 493cb39396 Moved PID settings impl from Laser to Winder 2025-10-31 12:29:22 +01:00
snacj 34d6e85ec3 fixed PID impl 2025-10-31 12:27:09 +01:00
snacj dd79586b13 Adjusted default PID setting and replaced info! with trace! 2025-10-31 12:27:09 +01:00
snacj fab801e62f Fixed Formatting 2025-10-31 12:27:09 +01:00
snacj 37a1641450 Fixed formatting. 2025-10-31 12:27:09 +01:00
snacj 6e4baaa356 Fixed merge issue 2025-10-31 12:27:09 +01:00
snacj 3292c62212 Added Debug messages 2025-10-31 12:27:09 +01:00
snacj 3e1d83a0dd changed maximum velocity by diameter for testing. 2025-10-31 12:27:09 +01:00
snacj 47dcd34364 Implemented setting of measured diameter 2025-10-31 12:27:09 +01:00
snacj 2c134c4479 Added working PID settings. 2025-10-31 12:27:09 +01:00
snacj 8613e0c5a8 Fixed Laser Winder connection 2025-10-31 12:27:09 +01:00
snacj 02daf33e55 Testing laser winder connection. 2025-10-31 12:27:09 +01:00
snacj 7b7eafee3b Added debug messages to test the connection 2025-10-31 12:27:07 +01:00
snacj 400fc016ae fixed naming 2025-10-31 11:46:20 +01:00
snacj 8b66ee415a Maybe fix laser Machine is Serial 2025-10-31 11:46:20 +01:00
snacj 89fef65347 Fixed Comments 2025-10-31 11:46:20 +01:00
snacj 5fbf5b7b97 Fix PID settings through laser. 2025-10-31 11:46:12 +01:00
snacj 8278bf579c Implemented Diameter Regulation Mode logic 2025-10-31 11:29:59 +01:00
snacj 6f7611ada5 Added Connect Machine selector to laser frontend 2025-10-31 11:29:59 +01:00
snacj a3ff479941 Fixed Machine Connection 2025-10-31 11:29:59 +01:00
snacj d14e1183d5 Fixed Laser Settings Page 2025-10-31 11:29:59 +01:00
snacj c7495a7d92 Added Pid Settings Logic 2025-10-31 11:29:59 +01:00
snacj 5428e950b0 Added Winder Laser Connection Frontend & Backend 2025-10-31 11:29:59 +01:00
Robin Krämer 13117a5d94 Merge pull request #845 from qitechgmbh/844-add-winder-mock-machine
added Winder mock
2025-10-28 17:58:25 +01:00
kraemr 6b74c7ec7a removed default mock-machine 2025-10-28 17:53:04 +01:00
kraemr e8b561f722 added Winder mock 2025-10-28 17:53:04 +01:00
Robin Krämer 75ee5af180 Merge pull request #852 from qitechgmbh/851-improve-caching
851 improve caching
2025-10-28 17:46:21 +01:00
kraemr 29f924f6cc removed socketioManager 2025-10-27 16:42:03 +01:00
kraemr 6e058c8e95 Keep namespaces alive 2025-10-27 15:11:30 +01:00
Robin Krämer 6822da80cf Merge pull request #838 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-a50d7f32cf
Bump playwright and @playwright/test in /electron
2025-10-27 11:24:43 +01:00
dependabot[bot] 37e3302f61 Bump playwright and @playwright/test in /electron
Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together.


Updates `playwright` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

Updates `@playwright/test` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.56.1
  dependency-type: indirect
- dependency-name: "@playwright/test"
  dependency-version: 1.56.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:21:25 +01:00
Robin Krämer 9b369d71ee Merge pull request #839 from qitechgmbh/dependabot/npm_and_yarn/electron/vite-6.4.1
Bump vite from 6.3.6 to 6.4.1 in /electron
2025-10-27 11:21:10 +01:00
dependabot[bot] cf58f0db6f Bump vite from 6.3.6 to 6.4.1 in /electron
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:17:46 +01:00
Robin Krämer 299f23c796 Merge pull request #847 from qitechgmbh/846-refactor-initialization-to-be-based-around-futures
started refactoring initialization to use futures instead
2025-10-27 11:14:18 +01:00
kraemr b58e4333a0 removed traces 2025-10-27 11:08:21 +01:00
kraemr f76cb9c381 Fixed Loop async code 2025-10-27 11:01:04 +01:00
kraemr e93e10d14d fixed all warnings and restored original ethercat discovery behaviour 2025-10-23 20:13:48 +02:00
kraemr 025464ebff all init functions look sane now
fixed compilation errors

fixed compile errors

refactored serial device detection successfully

added serial_detection functions

fixed  Killed error, and improved serial_detection

fixed all remaining issues

fix

fix
2025-10-23 20:13:33 +02:00
kraemr ae8f77f070 started refactoring initialization to use futures instead 2025-10-22 17:56:52 +02:00
Robin Krämer 97811a12d1 Merge pull request #842 from qitechgmbh/changelog-2.11.0
updated Changelog
2025-10-21 13:11:26 +02:00
kraemr 1f7e01820a updated Changelog 2025-10-21 13:10:37 +02:00
Robin Krämer 2f73771bbd Merge pull request #827 from qitechgmbh/825-add-button-to-change-the-spool-motor-direction-of-the-winder
825 add button to change the spool motor direction of the winder
2025-10-21 13:01:25 +02:00
Christian 503ac18e7b Ensure spool RPM is always displayed as a positive value 2025-10-21 12:37:34 +02:00
Christian 6a99b21c15 Fix spool speed rendering to display absolute values 2025-10-21 12:37:34 +02:00
Christian 170667f2b4 Add spool direction control to Winder2
- Introduced a new setting for spool direction (Forward/Reverse) in Winder2PresetsPage and Winder2Settings.
- Updated the state management to include forward direction in useWinder.
- Enhanced the API to handle SetSpoolForward mutation.
- Adjusted the spool speed controller to apply direction based on the forward setting.
2025-10-21 12:37:34 +02:00
Robin Krämer 314b7f2484 Merge pull request #840 from qitechgmbh/828-add-xl-winder-button-to-the-winder-config
828 add xl winder button to the winder config
2025-10-21 12:28:22 +02:00
Christian 9353b12707 Fix traverse limit reset logic in handleXlModeChange function 2025-10-21 11:51:02 +02:00
Christian 063810f906 Add gotoTraverseHome call when switching from XL to normal mode 2025-10-21 11:51:02 +02:00
Christian ac7ce126e6 Refactor handleXlModeChange to reset traverse limits when switching from XL mode 2025-10-21 11:51:02 +02:00
Christian a920830dcf Update WINDER2_TRAVERSE_MAX_XL to 385 mm for improved configuration 2025-10-21 11:51:02 +02:00
Christian 15d74fcee0 Add configuration for Winder2 settings and implement XL mode functionality 2025-10-21 11:51:02 +02:00
Robin Krämer c598e3fc9f Merge pull request #826 from qitechgmbh/818-smooth-fluctuating-temperature-extruder
818 smooth fluctuating temperature extruder
2025-10-20 18:16:08 +02:00
snacj 0dba0f2774 Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj e2cdb6a94c Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj 5dd2011e78 removed file 2025-10-20 18:12:06 +02:00
snacj 7bc142dc83 Fixed error in Extruder mock and removed zone from UpdateStateOptimistically 2025-10-20 18:12:06 +02:00
snacj 1931f00806 Changed rounding and step size to 0.001 for temperature PID 2025-10-20 18:12:06 +02:00
snacj 253336b02c Refactored frontend code of new PID settings 2025-10-20 18:12:06 +02:00
snacj b0c3a97f7f Fixed 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
snacj 22887d4f7a Added 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
Robin Krämer 00dbc90c85 Merge pull request #834 from qitechgmbh/lag-spike-fix
removed async sleep from control loop
2025-10-20 18:07:21 +02:00
kraemr 9ade1d8993 removed async sleep from control loop 2025-10-20 18:04:52 +02:00
Robin Krämer b2fb803033 Merge pull request #829 from qitechgmbh/808-lock-kernel-in-nix-to-last-working-rt-kernel
Newest kernel with realtime preemption
2025-10-20 13:42:38 +02:00
nixos-install.sh 988ed5b959 Newest kernel with realtime preemption 2025-10-17 17:36:14 +02:00
Oshgnacknak 2e62f99724 Merge pull request #806 from qitechgmbh/fix-caps
Fix caps in cargo run Linux
2025-10-17 15:01:53 +02:00
Oshgnacknak 530327b1e8 Fix caps in cargo run Linux 2025-10-17 14:55:44 +02:00
Robin Krämer 2f75b31fa9 Merge pull request #816 from qitechgmbh/812-improve-interface-discovery-to-only-use-ethernet-devices
fixed the atrocious interface_discovery
2025-10-17 12:42:31 +02:00
kraemr ce8f0a8c4b fixed the atrocious interface_discovery 2025-10-17 12:40:18 +02:00
Robin Krämer 321aae5b86 Merge pull request #820 from qitechgmbh/changelog-210
updated Changelog
2025-10-16 15:16:05 +02:00
kraemr 7712ea2d1f updated Changelog 2025-10-16 15:15:36 +02:00
Robin Krämer 175f64f784 Merge pull request #804 from qitechgmbh/803-add-winder-gearbox-conversion
803 add winder gearbox conversion
2025-10-16 12:39:14 +02:00
ChristianandCopilot 9357960463 Update electron/src/machines/winder/winder2/useWinder.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 12:22:54 +02:00
Christian 4d043906bc Remove unnecessary whitespace in puller speed calculation 2025-10-16 12:22:54 +02:00
Christian b99b7f796b Reset target speed to 0 when setting gear ratio and adjust puller speed calculation based on gear ratio 2025-10-16 12:22:54 +02:00
Christian a7b0414613 Add gear ratio calculation and display in Winder2 control and presets 2025-10-16 12:22:54 +02:00
Christian 13c608b11a Add gear ratio functionality to puller speed controller
- Introduced gear ratio enum with options for 1:5 and 1:10.
- Updated puller speed controller to include gear ratio state.
- Implemented gear ratio selection in Winder2Settings component.
- Added corresponding mutation and state management in useWinder2 hook.
2025-10-16 12:22:54 +02:00
Oshgnacknak d14fb91736 Simpliy panic handling and fix backtrace (#809)
* Hide memory locking behind feature flag

* Simplify panic handling
2025-10-15 21:09:05 +02:00
Oshgnacknak 6beaa444af Simplify panic handling 2025-10-15 17:08:37 +02:00
Oshgnacknak 3b552558d4 Hide memory locking behind feature flag 2025-10-15 17:08:37 +02:00
Robin Krämer 9cc63bf8fb Merge pull request #805 from qitechgmbh/743-electron-doesnt-use-gpu-for-hw-acceleration
Remove bad hardware acceleration flag
2025-10-15 10:31:04 +02:00
Oshgnacknak 70ce7625b2 Remove bad hardware acceleration flag 2025-10-13 15:00:21 +02:00
Robin Krämer 9936764125 Merge pull request #717 from qitechgmbh/700-add-a-show-from-option-for-graphs
700 add a show from option for graphs
2025-10-13 12:17:26 +02:00
Christian a0f09e1671 Improve formatting and adjust future time validation messages in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 1100b880f9 Add validation to prevent future time selection in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 6224f33ba5 fix eslint 2025-10-13 12:14:09 +02:00
Christian b0a9b5a6d2 Fix styling 2025-10-13 12:14:09 +02:00
ChristianandCopilot ac7247c87a Update electron/src/components/time/TimeInput.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 12:14:09 +02:00
Christian 2ea459f3be Refactor button styles and improve formatting in GraphControls and TimeInput components 2025-10-13 12:14:09 +02:00
Christian 25250f640f Add showFromTimestamp functionality to graph components and implement TimeInput for time selection 2025-10-13 12:14:09 +02:00
Robin Krämer 57a0cc8af4 Merge pull request #776 from qitechgmbh/775-add-explanation-of-padding-to-the-winder-manual
Add padding parameter explanation to manual for filament winding
2025-10-13 12:13:53 +02:00
Christian 5ebb978eae Add padding example image to the winder docs 2025-10-13 12:09:20 +02:00
Christian d647ee6ebe fix format 2025-10-13 12:09:20 +02:00
Christian fea203ae7a Add padding parameter explanation to manual for filament winding 2025-10-13 12:09:20 +02:00
Robin Krämer b5eb413d37 Merge pull request #791 from qitechgmbh/788-fix-remote-setting-reset-and-update-label-in-update-tab
Fix remote setting reset and update label to "Repository Owner" in Update tab
2025-10-13 12:08:43 +02:00
Christian aa1e5c4f39 Fix typos in GitHub repository owner label and description 2025-10-13 12:05:31 +02:00
Christian ee91934bf7 Refactor Github source management: integrate condition store and update UI labels 2025-10-13 12:05:31 +02:00
Milan von dem Bussche 1fa04eae9a Update README.md
Added YouTube Explainer Video about QiTech Control
2025-10-10 18:41:46 +02:00
Robin Krämer 98e1269f6f Merge pull request #800 from qitechgmbh/changelog-2.9.1
updated Changelog
2025-10-10 15:07:51 +02:00
kraemr 8f640fcb21 updated Changelog 2025-10-10 15:06:30 +02:00
Robin Krämer 6c120b7fe9 Merge pull request #796 from qitechgmbh/795-winding-when-puller-reverse-doesnt-work-in-adaptive
adaptive spool speed now takes negative speeds into account,
2025-10-08 18:12:23 +02:00
kraemr 033b93b9da puller speed now is always positive in frontend 2025-10-08 17:49:55 +02:00
kraemr 6092358e87 adaptive spool speed now takes negative speeds into account,
aswell as Auto Stop Spool
2025-10-08 17:40:50 +02:00
Robin Krämer 71de39e871 Merge pull request #793 from qitechgmbh/790-refactor-aquapath-newrs-to-match-others
790 refactor aquapath newrs to match others
2025-10-08 16:29:49 +02:00
joe 54ffa193c3 fixed fmt 2025-10-08 16:23:40 +02:00
joe 03164f1ceb refactor(aquapath1): cleanup and fix device initialization and configuration 2025-10-08 15:45:58 +02:00
Robin Krämer f67a46ab4c Merge pull request #787 from qitechgmbh/786-add-memory-locking
added CAP_IPC_LOCK and added memory locking
2025-10-07 13:54:02 +02:00
kraemr 610790a778 added CAP_IPC_LOCK and memory locking 2025-10-07 13:04:49 +02:00
Robin Krämer 6eaf451ad4 Merge pull request #692 from JanHBade/master
Update README.md
2025-10-07 12:57:27 +02:00
JanHBade 022e0adae3 Update README.md
Minor typo in EtherCAT
2025-10-07 12:57:16 +02:00
Robin Krämer 6e6d4b31e7 Merge pull request #746 from qitechgmbh/640-improve-scroll-bar-styling-for-manuals-to-resemble-macos-style
Update scrollbar styling to be similar to macos
2025-10-07 12:45:12 +02:00
Christian 0dafad3fb5 fix code stye 2025-10-07 12:40:01 +02:00
Christian 534de55239 Refactor scrollbar styles to apply only to html and body elements 2025-10-07 12:40:01 +02:00
Christian d9f92bcdf2 Refactor scrollbar styles for Markdown and Terminal components to be similar to MacOS styling 2025-10-07 12:40:01 +02:00
Robin Krämer 1570299e38 Merge pull request #782 from qitechgmbh/781-refactor-winderv2-emit-functions-to-emitrs
refactored winder2's emit methods to emit.rs
2025-10-06 17:01:38 +02:00
kraemr c95f6f3bd9 refactored winder2's emit methods to emit.rs 2025-10-06 16:36:59 +02:00
Robin Krämer 9fcde2a234 Merge pull request #780 from qitechgmbh/772-remove-dependencies-that-are-not-necessary
decreased compile time by removing jemalloc, and using only f64 uom
2025-10-06 16:05:08 +02:00
kraemr a2ffc41e05 decreased compile time by removing jemalloc, and using only f64 uom 2025-10-06 16:02:29 +02:00
Robin Krämer ff13baf257 Merge pull request #779 from qitechgmbh/changelog-2.9.0
updated Changelog
2025-10-06 14:25:39 +02:00
kraemr 8f5df07c19 updated Changelog 2025-10-06 14:24:43 +02:00
Robin Krämer 299b38515a Merge pull request #778 from qitechgmbh/777-increase-max-step-size
Increased Winder Step Size
2025-10-06 14:12:24 +02:00
kraemr 2b073aeb76 Increased Winder Step Size 2025-10-06 13:56:20 +02:00
Robin Krämer 7a6f92f8a4 Merge pull request #771 from qitechgmbh/770-update-machine-implementation-status-in-the-readme
Update machine documentation to reflect current implementation status
2025-10-02 17:07:13 +02:00
Christian 2e283ea95f Update machine documentation to reflect current implementation status 2025-10-02 17:03:55 +02:00
Robin Krämer 829bb5ea1c Merge pull request #625 from qitechgmbh/improved-disconnect-handling
Introduce machine connection
2025-10-02 14:26:07 +02:00
Oshgnacknak 0be18aaed0 Derive machines from macro again
This time we also add get_machine_identification_unique.

Also fix import warnings
2025-10-02 13:58:54 +02:00
Oshgnacknak ffe0f09061 Clean up machine event creation 2025-10-01 16:50:04 +02:00
nixos-install.sh 730d61a18d Don't clear ethercat slots, we want to keep the namespaces 2025-10-01 15:57:03 +02:00
Oshgnacknak fc07525976 Don't remove, only disconnect serial machines
This allows use to keep the namespaces
2025-10-01 15:57:03 +02:00
Oshgnacknak c48953aa6b Introduce machine connection
This is to hold on to the namespace of
a machine even if the devices of that
machine has disconnected and to allow
reattaching to the namespace once the
devices reconnects.

Additionally to the new struct,
a wast refactor took place cleaning up
connections to machines, cross connections
between machines the way we downcast
machines downcasts.
2025-10-01 15:57:03 +02:00
Melvin Spieß 0c948b436d Merge pull request #764 from qitechgmbh/dependabot/cargo/axum-0.8.6
Bump axum from 0.8.4 to 0.8.6
2025-10-01 12:05:48 +02:00
Robin Krämer 1b3f92dd1c Merge pull request #725 from qitechgmbh/425-fix-terminal-component-issues-v2
425 fix terminal component issues v2
2025-10-01 11:58:55 +02:00
dependabot[bot] c29c549a1c Bump axum from 0.8.4 to 0.8.6
Bumps [axum](https://github.com/tokio-rs/axum) from 0.8.4 to 0.8.6.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.8.4...axum-v0.8.6)

---
updated-dependencies:
- dependency-name: axum
  dependency-version: 0.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:57:52 +00:00
kraemr 17622cacde set log level to "info" for rust 2025-10-01 11:56:29 +02:00
kraemr 0b423454ec fixed line duplication in Terminal Component on Update Page 2025-10-01 11:56:29 +02:00
kraemr 81ec98f670 dont clear log, causes react errors 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com 1aed425544 eslint bypass for control character 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com da080ace68 try using clear functions before interactions 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com ded8dd2d2d added react-virtualized 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com edd83ba559 added virtualized list 2025-10-01 11:56:29 +02:00
Melvin Spieß 7d1d8559a5 Merge pull request #672 from qitechgmbh/671-refactor-machine-creation
reduced code duplication in new.rs of machines
2025-10-01 11:56:17 +02:00
kraemr f0bb9bfed1 fixed lifetimes of get_ethercat_device and refactored buffer 2025-10-01 11:52:13 +02:00
93094049+kraemr@users.noreply.github.com c6ea057736 renamed helper function 2025-10-01 11:52:13 +02:00
93094049+kraemr@users.noreply.github.com 90a00198a7 reduced code duplication in new.rs of machines 2025-10-01 11:52:12 +02:00
Melvin Spieß f8241547fe Merge pull request #762 from qitechgmbh/dependabot/cargo/erased-serde-0.4.8
Bump erased-serde from 0.4.6 to 0.4.8
2025-10-01 11:50:54 +02:00
Melvin Spieß 1cb5d51c13 Merge pull request #760 from qitechgmbh/dependabot/npm_and_yarn/electron/tanstack/react-router-1.130.17
Bump @tanstack/react-router from 1.130.12 to 1.130.17 in /electron
2025-10-01 11:38:53 +02:00
Melvin Spieß c42cdf8db1 Merge pull request #759 from qitechgmbh/dependabot/npm_and_yarn/electron/radix-ui/react-navigation-menu-1.2.14
Bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 in /electron
2025-10-01 11:38:22 +02:00
Melvin Spieß 69014ef8c6 Merge pull request #758 from qitechgmbh/dependabot/npm_and_yarn/electron/types/react-19.1.16
Bump @types/react from 19.1.9 to 19.1.16 in /electron
2025-10-01 11:35:53 +02:00
Melvin Spieß 5faab29a2b Merge pull request #763 from qitechgmbh/dependabot/cargo/libc-0.2.176
Bump libc from 0.2.175 to 0.2.176
2025-10-01 11:35:31 +02:00
dependabot[bot] 9a8b16ca69 Bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 in /electron
Bumps [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:32:49 +00:00
Melvin Spieß 53f3c66214 Merge pull request #765 from qitechgmbh/dependabot/cargo/regex-1.11.3
Bump regex from 1.11.2 to 1.11.3
2025-10-01 11:30:59 +02:00
dependabot[bot] 432bf0d6ba Bump libc from 0.2.175 to 0.2.176
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.175 to 0.2.176.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.176
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:30:49 +00:00
dependabot[bot] f3990f35f1 Bump erased-serde from 0.4.6 to 0.4.8
Bumps [erased-serde](https://github.com/dtolnay/erased-serde) from 0.4.6 to 0.4.8.
- [Release notes](https://github.com/dtolnay/erased-serde/releases)
- [Commits](https://github.com/dtolnay/erased-serde/compare/0.4.6...0.4.8)

---
updated-dependencies:
- dependency-name: erased-serde
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:30:40 +00:00
dependabot[bot] 6f17412363 Bump regex from 1.11.2 to 1.11.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.2 to 1.11.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.2...1.11.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:28:49 +00:00
dependabot[bot] 68907b9750 Bump @types/react from 19.1.9 to 19.1.16 in /electron
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.9 to 19.1.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:26:34 +00:00
dependabot[bot] e7007e1980 Bump @tanstack/react-router from 1.130.12 to 1.130.17 in /electron
Bumps [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) from 1.130.12 to 1.130.17.
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.130.17/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.130.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:26:27 +00:00
Melvin Spieß 3bcd96d73c Merge pull request #766 from qitechgmbh/dependabot/cargo/anyhow-1.0.100
Bump anyhow from 1.0.99 to 1.0.100
2025-10-01 11:09:51 +02:00
dependabot[bot] 8a77c62a5e Bump anyhow from 1.0.99 to 1.0.100
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.99 to 1.0.100.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.100
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:24:32 +00:00
kraemr 93add82ab5 updated Changelog 2025-09-30 15:29:27 +02:00
Robin Krämer 690b1f7ab3 Merge pull request #757 from qitechgmbh/revert-ethercat-setup
fixed ethercat setup timeouts and retry
2025-09-29 17:19:33 +02:00
kraemr 3c07b94e3a fixed ethercat setup timeouts and retry 2025-09-29 17:18:49 +02:00
Robin Krämer 3b63b55859 Merge pull request #756 from qitechgmbh/fix-winder-regression
fixed winder regression due to refactor
2025-09-29 17:10:33 +02:00
kraemr f87982c465 fixed winder regression due to refactor 2025-09-29 17:01:04 +02:00
Robin Krämer b03cedb55c Merge pull request #447 from qitechgmbh/fix/438
Implementation AquaPath
2025-09-29 15:42:32 +02:00
kraemr f5934efa6c restored mock-machine functionality 2025-09-29 15:39:25 +02:00
kraemr 186619cd48 fixed compilation error 2025-09-29 15:33:25 +02:00
Darmenov Alisher 583d4c1a4d resolved comment 2025-09-29 15:33:25 +02:00
Anatomik5 63bee54948 AquapathV1 2025-09-29 15:33:25 +02:00
Robin Krämer 1523d64ed7 Merge pull request #749 from qitechgmbh/738-add-mock-machines
738 add mock machines
2025-09-29 11:32:39 +02:00
kraemr 48fdde61dc further improved readability by moving feature flags to mod.rs 2025-09-29 11:30:54 +02:00
kraemr 7bc700ac5f when selecting mock-machine feature extruder now appears as well 2025-09-29 11:30:54 +02:00
93094049+kraemr@users.noreply.github.com bdd6147f92 rewrote extruder and winder, also added mock-machine for extruder 2025-09-29 11:30:54 +02:00
93094049+kraemr@users.noreply.github.com f7f65bdaaa refactored all emit functions out of mod.rs for clarity for extruder 2025-09-29 11:30:54 +02:00
Robin Krämer 232b0782ca Merge pull request #754 from qitechgmbh/748-add-feature-flag-or-env-var-for-setting-loop-timeout
748 add feature flag or env var for setting loop timeout
2025-09-29 11:30:19 +02:00
kraemr c3c36a26b6 also added development-build flag to irq affinity code 2025-09-25 17:11:18 +02:00
kraemr 7f22bdfbf2 added feature development-build to set timeout 2025-09-25 17:06:42 +02:00
Robin Krämer ff4c579f7b Merge pull request #673 from qitechgmbh/632-add-documentation-for-creating-a-new-machine
added documentation for adding a machine
2025-09-25 15:33:41 +02:00
93094049+kraemr@users.noreply.github.com 9683882775 added documentation for adding a machine 2025-09-25 15:32:33 +02:00
Robin Krämer 893e01532d Merge pull request #753 from qitechgmbh/633-update-documentation-for-adding-new-devices
updated devices documentation by adding an example and explaining the architecture
2025-09-25 14:53:17 +02:00
kraemr 713f78fa4e updated devices documentation by adding an example
and explaining how devices fit into the architecture
2025-09-25 14:50:32 +02:00
Robin Krämer f444347fb9 Merge pull request #752 from qitechgmbh/751-document-performancestability-tweaks-made
added performance and stabillity documentation
2025-09-25 11:37:04 +02:00
kraemr ea03c0f755 added performance and stabillity documentation 2025-09-25 11:31:53 +02:00
Robin Krämer e283a0f164 Merge pull request #745 from qitechgmbh/744-windermode-switch-from-modewind-to-modepull-or-modehold-disables-the-wind-button
fixed emit state after state change
2025-09-23 15:41:28 +02:00
snacj 6820206d78 fixed emit state after state change 2025-09-23 15:38:47 +02:00
Robin Krämer 5069ee425c Merge pull request #742 from qitechgmbh/681-remove-unused-or-redundant-crates
removed unused crates
2025-09-23 13:37:48 +02:00
kraemr f93f342508 added ignores for cargo machete 2025-09-23 13:17:29 +02:00
kraemr c6d3563101 removed unused crates 2025-09-23 12:56:24 +02:00
Robin Krämer f2f893c917 Merge pull request #741 from qitechgmbh/739-add-roundness-value-to-laser-graph-and-control-page
Added Roundness Value to 2 Axis Laser
2025-09-22 14:27:30 +02:00
Robin KrämerandCopilot bbbca4085d Update server/src/machines/laser/mod.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 14:24:42 +02:00
Robin KrämerandCopilot 4fdf461520 Update server/src/machines/laser/mod.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 14:23:37 +02:00
snacj dcfc6728ba Made Roundness unitless 2025-09-22 14:10:25 +02:00
snacj 2673a92f50 Added Roundness Value to 2 Axis Laser 2025-09-22 13:39:59 +02:00
Robin Krämer 4d72185ba9 Merge pull request #675 from qitechgmbh/512-add-troubleshooting-documentation
512 add troubleshooting documentation
2025-09-18 19:44:43 +02:00
93094049+kraemr@users.noreply.github.com ade4f5cd2b updated troubleshooting.md 2025-09-18 19:41:15 +02:00
93094049+kraemr@users.noreply.github.com cf1ebf2b3d added troubleshooting.md 2025-09-18 19:41:15 +02:00
Robin Krämer 6b5cb7d87e Merge pull request #737 from qitechgmbh/fix_state_handling
Fix state handling
2025-09-17 18:26:01 +02:00
93094049+kraemr@users.noreply.github.com 4894bbc231 fixed all state issues 2025-09-17 18:20:31 +02:00
Robin Krämer 721c5a99ee Merge pull request #736 from qitechgmbh/less_amps_spool_test
Less amps spool test
2025-09-17 17:28:23 +02:00
93094049+kraemr@users.noreply.github.com 36fccc80cb set puller amps to 2.7A 2025-09-17 17:21:15 +02:00
93094049+kraemr@users.noreply.github.com e14fe86306 testing with less current 2025-09-17 17:21:15 +02:00
Robin Krämer 95c9f99961 Merge pull request #735 from qitechgmbh/fix_compiler_flags
Fix compiler flags
2025-09-17 12:53:50 +02:00
93094049+kraemr@users.noreply.github.com 546189b9c8 removed log::error 2025-09-17 12:47:46 +02:00
93094049+kraemr@users.noreply.github.com dfbc2a65c0 hopefully all compiler flags now work as expected 2025-09-17 12:47:46 +02:00
Robin Krämer 2038d79e18 Merge pull request #734 from qitechgmbh/changelog-v2.7.0
updated Changelog for 2.7.0
2025-09-17 11:47:54 +02:00
93094049+kraemr@users.noreply.github.com 188caf397b updated Changelog for 2.7.0 2025-09-17 11:44:22 +02:00
Robin Krämer 13138b1d7d Merge pull request #726 from qitechgmbh/video-streaming
Add video streaming api
2025-09-16 18:25:34 +02:00
Icedsoda 5466efd8c3 add video streaming api 2025-09-16 18:23:36 +02:00
Robin Krämer 61f0dc92d1 Merge pull request #733 from qitechgmbh/685-use-nixos-cache-server-for-private-packages
685 use nixos cache server for private packages
2025-09-16 17:49:32 +02:00
93094049+kraemr@users.noreply.github.com 073c174203 removed unnecessary comment 2025-09-16 17:44:03 +02:00
93094049+kraemr@users.noreply.github.com 1d0f7231e5 added substituter
fixed nix config

actually fixed config now

test

removed git hash as version

nix sucks

removed gitcommitHash from overlays

added fake version

fixed compile script

test

fixed script

updated ip

test

test 2

uppdated script and fixed config

to check for caching

test

fixed config

test

test

extra-subs changed to trusted-public-keys

test

test

removed substituter

removed print statements
2025-09-16 17:44:03 +02:00
93094049+kraemr@users.noreply.github.com 9401c81581 added scripts to compile our nix packages 2025-09-16 17:44:03 +02:00
Robin Krämer 936e54f3e2 Merge pull request #731 from qitechgmbh/666-implement-laserv2
666 implement laserv2
2025-09-16 16:54:29 +02:00
snacj 0c02fc1ce0 Added Comment do explain 45deg rotation 2025-09-16 16:52:12 +02:00
snacj c723f1df00 Fixed Graphs to work with 2axis and 1 axis 2025-09-16 16:52:12 +02:00
snacj c65a3f7fb1 Renamed Values and changed to one Graph 2025-09-16 16:52:12 +02:00
snacj a0c59f9bb6 Added Const for graph color. 2025-09-16 16:52:12 +02:00
snacj 069dcd65bb Removed tracing::info! 2025-09-16 16:52:12 +02:00
snacj 5331d39c1d Changed Visual of Diameter to use x and y values 2025-09-16 16:52:12 +02:00
snacj 2c928e6158 Implemented 2-Axis Laser 2025-09-16 16:52:12 +02:00
snacj 3ae17479cb Adjusted Frontend to work with 2-Axis Laser. 2025-09-16 16:52:12 +02:00
93094049+kraemr@users.noreply.github.com b3589755f5 fixed winder events 2025-09-16 16:41:38 +02:00
Robin Krämer e7f97ff4d4 Merge pull request #729 from qitechgmbh/728-set_irq_handler_affinity-doesnt-work-on-macos
fixed target_os flags for set_irq_affinity
2025-09-16 15:37:40 +02:00
93094049+kraemr@users.noreply.github.com 7b4a3b52b3 fixed target_os flags for set_irq_affinity 2025-09-16 15:30:16 +02:00
Robin Krämer 310fd99730 Merge pull request #711 from qitechgmbh/694-over-time-nixos-generations-takes-up-all-diskspace-even-when-deleted
added collect-garbage on each delete
2025-09-15 19:38:31 +02:00
93094049+kraemr@users.noreply.github.com 26473f5596 collect-garbage does not exist, replaced with nix store gc 2025-09-15 19:36:46 +02:00
93094049+kraemr@users.noreply.github.com ead8a02b6a added collect-garbage on each delete 2025-09-15 19:36:46 +02:00
Robin Krämer b2ca9cf792 Merge pull request #712 from qitechgmbh/708-refactor-nix-config-so-that-no-dynamic-installinfonix-is-created
Read git information during build instead of addign new commit via installInfo.nix
2025-09-15 18:29:19 +02:00
nixos-install.sh 301c254446 Pass git info into gitInfo.nix via env vars 2025-09-15 17:57:43 +02:00
Robin Krämer 769091d271 Merge pull request #720 from qitechgmbh/714-pressing-the-same-button-twice-triggers-isloading
714 pressing the same button twice triggers isloading
2025-09-15 16:55:05 +02:00
93094049+kraemr@users.noreply.github.com 1bc7907ba0 all machines now use old emit_state again, but extruder uses custom maybe_emit_state_hash
fixed compile error after merge conflict
2025-09-15 16:49:47 +02:00
93094049+kraemr@users.noreply.github.com 8260c606dc removed maybe_emit_state from every machine except extruder 2025-09-15 16:48:09 +02:00
Christian c0b7e563c5 Merge pull request #713 from qitechgmbh/596-extend-total-power-output-tile-2
596 extend total power output tile
2025-09-15 16:10:45 +02:00
Melvin Spieß 9062e15712 Merge pull request #727 from qitechgmbh/722-remove-estimated-diameter-value
722 remove estimated diameter value
2025-09-15 15:34:57 +02:00
snacj 134da6fad3 fixed formatting 2025-09-15 15:25:31 +02:00
snacj 4cec6ef711 Changed Radius to Factor 2025-09-15 15:13:09 +02:00
snacj 36d64c85fb Removed Estimated Diameter Value 2025-09-15 15:06:30 +02:00
Robin Krämer 8e00019023 Merge pull request #721 from qitechgmbh/as-any-mut
Add `Machin::as_any_mut`
2025-09-15 13:44:51 +02:00
Icedsoda 575d4d2d26 Add Machin::as_any_mut 2025-09-15 13:26:47 +02:00
Robin Krämer 19e9ebc20f Merge pull request #719 from qitechgmbh/718-ethercat-timeout-on-setup
set wait_loop_delay to zero
2025-09-15 09:34:00 +02:00
93094049+kraemr@users.noreply.github.com dd4476b786 set wait_loop_delay to zero 2025-09-15 09:32:20 +02:00
Christian fc283a64a7 Merge branch 'master' into 596-extend-total-power-output-tile-2 2025-09-13 17:05:56 +02:00
Robin Krämer b6dc1d35c6 Merge pull request #710 from qitechgmbh/707-make-adaptive-mode-for-winder-default
changed MinMax to Adaptive by default
2025-09-11 15:27:20 +02:00
93094049+kraemr@users.noreply.github.com 4f542510d6 changed MinMax to Adaptive by default 2025-09-11 15:23:08 +02:00
Oshgnacknak 0ca334decc Disable hardening in nix develop shell (#705)
Hardening causes jmalloc to fail at compilation
2025-09-11 12:57:34 +02:00
Robin Krämer 239a926301 Merge pull request #706 from qitechgmbh/703-fix-permissions-for-set_irq_affinity
703 fix permissions for set irq affinity
2025-09-11 11:01:27 +02:00
93094049+kraemr@users.noreply.github.com 9f4c7fbedd fixed permissions 2025-09-11 10:59:21 +02:00
93094049+kraemr@users.noreply.github.com 6a9de48bb7 added extra logging 2025-09-11 10:59:21 +02:00
93094049+kraemr@users.noreply.github.com d9fc22399a added DAC_OVERRIDE CAP 2025-09-11 10:59:21 +02:00
Robin Krämer f41b0dc0c2 Merge pull request #697 from qitechgmbh/dependabot/npm_and_yarn/electron/vite-6.3.6
Bump vite from 6.3.5 to 6.3.6 in /electron
2025-09-10 15:58:37 +02:00
dependabot[bot] 14233c0aa1 Bump vite from 6.3.5 to 6.3.6 in /electron
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 13:48:41 +00:00
Robin Krämer 58e56c4226 Merge pull request #702 from qitechgmbh/701-fix-optimistic-state-handling
701 fix optimistic state handling
2025-09-10 15:01:24 +02:00
93094049+kraemr@users.noreply.github.com 8ee184475e only return state not optimisticstate 2025-09-10 14:56:05 +02:00
93094049+kraemr@users.noreply.github.com be9c5b1be5 fixed optimistic state handling 2025-09-10 14:55:08 +02:00
Robin Krämer 8411f8f2b9 Merge pull request #698 from qitechgmbh/696-pin-ethernet-interrupt-handler-to-the-same-core-as-ethercrab-task
696 pin ethernet interrupt handler to the same core as ethercrab task
2025-09-10 14:19:59 +02:00
93094049+kraemr@users.noreply.github.com 5441a7a126 changed lint, renamed irq_handling funcs, refactor 2025-09-10 14:08:32 +02:00
93094049+kraemr@users.noreply.github.com b2425de273 fixed non unix compilation 2025-09-10 13:55:59 +02:00
93094049+kraemr@users.noreply.github.com 82d5e73129 removed unused import 2025-09-10 13:45:58 +02:00
93094049+kraemr@users.noreply.github.com 31189ebdef refactored irq_handling into own file 2025-09-10 13:45:58 +02:00
93094049+kraemr@users.noreply.github.com ee1720fa95 added code to set irq handler affinity 2025-09-10 13:45:58 +02:00
Robin Krämer 72dc0634f3 Merge pull request #693 from qitechgmbh/clippy-fix
Use Cargo Clippy
2025-09-10 13:43:51 +02:00
Icedsoda 13f5125daa cargo clippy --fix 2025-09-10 12:14:19 +02:00
Icedsoda 2b6904b145 configure clippy 2025-09-10 12:14:19 +02:00
Robin Krämer f776a4ba19 Merge pull request #695 from qitechgmbh/688-pressure-and-rpm-are-switched
reverse prop doesnt make sense on Extruder2ControlPage Regulation
2025-09-09 16:54:29 +02:00
93094049+kraemr@users.noreply.github.com a7c8cb218f reverse prop doesnt make sense on Extruder2ControlPage Regulation 2025-09-09 16:49:31 +02:00
Robin Krämer 3cb2d20df9 Merge pull request #690 from qitechgmbh/sync-act
Make `act` method non-async
2025-09-09 14:21:22 +02:00
Icedsoda ee4b5b15d7 Make act method non-async 2025-09-09 14:18:21 +02:00
93094049+kraemr@users.noreply.github.com 299e6314b8 Revert "using virtualized list"
This reverts commit 01170e1a1b.
2025-09-09 13:15:46 +02:00
93094049+kraemr@users.noreply.github.com 01170e1a1b using virtualized list 2025-09-09 13:13:52 +02:00
Robin Krämer 6fe2e498aa Merge pull request #686 from qitechgmbh/668-frontend-buttons-are-flickering
Fixed Buttons Flickering
2025-09-09 11:21:18 +02:00
93094049+kraemr@users.noreply.github.com fc7262f8f0 Fixed Buttons Flickering 2025-09-09 11:16:56 +02:00
Robin Krämer 880d2ddcb0 Merge pull request #684 from qitechgmbh/683-ethercat-loop-broken
683 ethercat loop broken
2025-09-08 14:06:02 +02:00
93094049+kraemr@users.noreply.github.com e9ea378ef0 set loop timer to 500us 2025-09-08 13:44:33 +02:00
93094049+kraemr@users.noreply.github.com 145ad7ed2c added 1ms delay 2025-09-08 13:25:46 +02:00
93094049+kraemr@users.noreply.github.com d5ce65c868 added 1ms delay 2025-09-08 13:23:39 +02:00
93094049+kraemr@users.noreply.github.com 1bc76dfe09 wait_loop_delay set to 0 2025-09-08 12:52:45 +02:00
Robin Krämer 17e82cf5ea Merge pull request #680 from qitechgmbh/402-many-journald-logs-slow-down-the-frontend
402 many journald logs slow down the frontend
2025-09-08 12:04:59 +02:00
93094049+kraemr@users.noreply.github.com 926650802b reduces Logs, by A LOT 2025-09-08 12:01:01 +02:00
Robin Krämer fba7c0375d Merge pull request #677 from qitechgmbh/676-reduce-heap-allocationsdeallocations-in-machineact
676 reduce heap allocationsdeallocations in machineact
2025-09-08 11:51:46 +02:00
93094049+kraemr@users.noreply.github.com 27be5b8ec4 fixed extruder 2025-09-08 11:49:42 +02:00
93094049+kraemr@users.noreply.github.com d036246b2a refactored all machines 2025-09-08 11:49:42 +02:00
93094049+kraemr@users.noreply.github.com 59a9b15632 started refactoring MachineAct 2025-09-08 11:49:42 +02:00
Robin Krämer c09e061390 Merge pull request #679 from qitechgmbh/678-remove-loopthrottle
678 remove loopthrottle
2025-09-08 11:49:22 +02:00
93094049+kraemr@users.noreply.github.com 02f6dffaff fixed compile error and replaced loopthrottle with sleep 2025-09-08 10:00:37 +02:00
93094049+kraemr@users.noreply.github.com 509d4c8734 removed unused import 2025-09-08 09:57:03 +02:00
93094049+kraemr@users.noreply.github.com 084fdef754 wait_loop_delay of 1ms 2025-09-08 09:56:38 +02:00
93094049+kraemr@users.noreply.github.com a2f21da355 remove LoopThrottle 2025-09-08 09:55:47 +02:00
Robin Krämer b722502f5b Merge pull request #674 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-37.3.1
Bump electron from 37.2.6 to 37.3.1 in /electron
2025-09-04 19:04:55 +02:00
dependabot[bot] ab45407763 Bump electron from 37.2.6 to 37.3.1 in /electron
Bumps [electron](https://github.com/electron/electron) from 37.2.6 to 37.3.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v37.2.6...v37.3.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 37.3.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 18:57:30 +02:00
Robin Krämer dd4fbbdafd Merge pull request #647 from qitechgmbh/dependabot/npm_and_yarn/electron/radix-ui/react-collapsible-1.1.12
Bump @radix-ui/react-collapsible from 1.1.11 to 1.1.12 in /electron
2025-09-04 12:25:18 +02:00
dependabot[bot] c8c609d88b Bump @radix-ui/react-collapsible from 1.1.11 to 1.1.12 in /electron
Bumps [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) from 1.1.11 to 1.1.12.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 12:22:05 +02:00
Robin Krämer 92a4905b44 Merge pull request #649 from qitechgmbh/dependabot/npm_and_yarn/electron/zustand-5.0.8
Bump zustand from 5.0.7 to 5.0.8 in /electron
2025-09-04 12:21:47 +02:00
dependabot[bot] 1c2aea7cb1 Bump zustand from 5.0.7 to 5.0.8 in /electron
Bumps [zustand](https://github.com/pmndrs/zustand) from 5.0.7 to 5.0.8.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.7...v5.0.8)

---
updated-dependencies:
- dependency-name: zustand
  dependency-version: 5.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 12:18:57 +02:00
Robin Krämer 44127bec42 Merge pull request #669 from qitechgmbh/603-add-electrical-diagrams-to-the-machines-manuals
Implement electrical diagrams into the manuals
2025-09-04 12:12:00 +02:00
Christian f48b97dd24 Add electrical diagram images to the public folder and implement them in the md files for extruder and winder. 2025-09-03 21:28:23 +02:00
Icedsoda 74c107c032 Merge pull request #667 from qitechgmbh/fix-stuck-loading
fixed infinite loading error
2025-09-03 19:23:59 +02:00
93094049+kraemr@users.noreply.github.com c4263dbe84 fixed infinite loading error 2025-09-03 19:00:50 +02:00
Icedsoda a6b7ec914d Add rustdoc 2025-09-03 17:25:18 +02:00
Icedsoda 56c095a10c Merge pull request #446 from qitechgmbh/fix/61
Add `EL4002`
2025-09-03 16:44:09 +02:00
Icedsoda f486aaa31b .mailmap 2025-09-03 16:42:57 +02:00
Darmenov Alisher c4f2f96c2e f 2025-09-03 16:31:34 +02:00
Anatomik5 99988c0445 el4002 and el40xx pdo implementation 2025-09-03 16:28:42 +02:00
Robin Krämer 9c5191a6ae Merge pull request #662 from qitechgmbh/changelog-2.6.0
Update CHANGELOG.md
2025-09-03 15:56:22 +02:00
93094049+kraemr@users.noreply.github.com 2f66715738 added missing Cargo.lock changes 2025-09-03 15:46:31 +02:00
Robin Krämer bcfd579a4b Update CHANGELOG.md 2025-09-03 11:54:44 +02:00
Icedsoda bef13832d6 Merge pull request #659 from qitechgmbh/575-automatically-send-stateevent-when-changed
575 automatically send stateevent when changed
2025-09-03 11:30:40 +02:00
93094049+kraemr@users.noreply.github.com 130ebf1681 changed mock and winder to 30 fps and added missing maybe_emit 2025-09-03 11:29:22 +02:00
93094049+kraemr@users.noreply.github.com 587c8c8fdb removed ethercrab (again) 2025-09-03 11:29:22 +02:00
93094049+kraemr@users.noreply.github.com 1ad9896be4 fixed linter errors 2025-09-03 11:29:22 +02:00
93094049+kraemr@users.noreply.github.com a6cdcf893d removed unused imports 2025-09-03 11:29:22 +02:00
93094049+kraemr@users.noreply.github.com 37ca881ed7 fixed stateschema
fixed stateschema for real this time
2025-09-03 11:29:17 +02:00
93094049+kraemr@users.noreply.github.com c38d7c0056 added maybe emit to mock 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 99b9a8710c added maybe_emit to winder2 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com a859426b06 added maybe_emit to laser 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 479908bf59 made changes for extruder and added derive 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 3bc6522095 fixed hasher compare func 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 6fcce62b04 added hasher_serializer and compare func 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 060a7b6dbc refactoring States 2025-09-03 11:27:09 +02:00
93094049+kraemr@users.noreply.github.com 73521ad80a state gets sent when changed 2025-09-03 11:27:09 +02:00
Icedsoda 5c3897db9b Merge pull request #658 from qitechgmbh/65-handbook-for-extruder-v2
added extruder manual
2025-09-03 11:23:27 +02:00
Icedsoda e659ebbfd5 added extruder handbook 2025-09-03 10:14:41 +02:00
Icedsoda 3e98d08a0f Merge pull request #661 from qitechgmbh/lfs
track images in git lfs
2025-09-03 10:09:47 +02:00
Icedsoda 12069c0cd2 track images in git lfs 2025-09-03 10:09:17 +02:00
Robin Krämer a62d492a0a Merge pull request #645 from qitechgmbh/dependabot/cargo/tracing-subscriber-0.3.20
Bump tracing-subscriber from 0.3.19 to 0.3.20
2025-09-02 15:33:58 +02:00
dependabot[bot] 2b65d2ddd5 Bump tracing-subscriber from 0.3.19 to 0.3.20
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:28:44 +02:00
Robin Krämer 9bb16d5cc5 Merge pull request #651 from qitechgmbh/dependabot/npm_and_yarn/electron/radix-ui/react-select-2.2.6
Bump @radix-ui/react-select from 2.2.5 to 2.2.6 in /electron
2025-09-02 15:27:52 +02:00
dependabot[bot] 9b8076b357 Bump @radix-ui/react-select from 2.2.5 to 2.2.6 in /electron
Bumps [@radix-ui/react-select](https://github.com/radix-ui/primitives) from 2.2.5 to 2.2.6.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:25:02 +02:00
Robin Krämer c86d34d881 Merge pull request #653 from qitechgmbh/dependabot/cargo/serde_json-1.0.143
Bump serde_json from 1.0.140 to 1.0.143
2025-09-02 15:22:52 +02:00
Robin Krämer 6dd031df24 Merge branch 'master' into dependabot/cargo/serde_json-1.0.143 2025-09-02 14:57:56 +02:00
Robin Krämer 40b33bf28d Merge pull request #652 from qitechgmbh/dependabot/cargo/serialport-4.7.3
Bump serialport from 4.7.2 to 4.7.3
2025-09-02 14:56:26 +02:00
dependabot[bot] 9a93c7bbd9 Bump serialport from 4.7.2 to 4.7.3
Bumps [serialport](https://github.com/serialport/serialport-rs) from 4.7.2 to 4.7.3.
- [Changelog](https://github.com/serialport/serialport-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/serialport/serialport-rs/compare/v4.7.2...v4.7.3)

---
updated-dependencies:
- dependency-name: serialport
  dependency-version: 4.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:53:38 +02:00
dependabot[bot] 24ef2b1c6c Bump serde_json from 1.0.140 to 1.0.143
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.140 to 1.0.143.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.140...v1.0.143)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.143
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:52:53 +02:00
Robin Krämer 37359b4058 Merge pull request #654 from qitechgmbh/dependabot/cargo/regex-1.11.2
Bump regex from 1.11.1 to 1.11.2
2025-09-02 14:51:54 +02:00
dependabot[bot] ce19f515d7 Bump regex from 1.11.1 to 1.11.2
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.1 to 1.11.2.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.1...1.11.2)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:49:40 +02:00
Robin Krämer 1359e0a33d Merge pull request #655 from qitechgmbh/dependabot/cargo/anyhow-1.0.99
Bump anyhow from 1.0.98 to 1.0.99
2025-09-02 14:49:29 +02:00
dependabot[bot] 3b234df7e7 Bump anyhow from 1.0.98 to 1.0.99
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.98 to 1.0.99.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.99
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 12:48:03 +00:00
Icedsoda b820a433df Merge pull request #641 from qitechgmbh/604-manuals-have-two-scroll-bars
604 manuals have two scroll bars
2025-09-01 14:09:38 +02:00
Icedsoda 9bf061bf6d Merge pull request #657 from qitechgmbh/634-store-graphs-timeframe-globally
added global timeframe store
2025-09-01 14:05:25 +02:00
93094049+kraemr@users.noreply.github.com 84f1fa123f fixed linter error 2025-09-01 14:00:14 +02:00
93094049+kraemr@users.noreply.github.com 91660fd1ec fixed compile errors 2025-09-01 13:58:07 +02:00
93094049+kraemr@users.noreply.github.com 28c4a801a6 added global timeframe store 2025-09-01 13:53:41 +02:00
Robin Krämer 22af6e61af Merge pull request #639 from qitechgmbh/extruder-regulation-mode-order-reversion
Add reverse prop to `SelectionGroupBoolean`
2025-09-01 11:48:37 +02:00
Icedsoda 794a309d71 feat(control): Add reverse prop to SelectionGroupBoolean
This change introduces a `reverse` property to the `SelectionGroupBoolean` component to allow for more flexible UI representation.

- When `reverse` is set to true, the `optionTrue` and `optionFalse` props are swapped.
- This allows the visual "on" state to correspond to a `false` value and vice-versa.
- The prop is implemented on the Inverter Regulation control on the Extruder2 page.
2025-09-01 11:14:38 +02:00
Christian 273d695d0d fix eslint error 2025-08-28 21:12:10 +02:00
Christian 76d5ae613a fix code formatting 2025-08-28 21:12:10 +02:00
Christian 14cca9e8cd fix different padding leading to an line jump when changing from passive to active 2025-08-28 21:12:10 +02:00
Christian 8f5ac3706f Improve active heading detection and scrolling behavior 2025-08-28 21:12:10 +02:00
Oshgnacknak d928f02f2b Merge pull request #642 from qitechgmbh/356-substitute-unmaintained-serial-crate
Improve serial conneciton handling
2025-08-28 17:42:44 +02:00
Oshgnacknak 77c099fa8a Formatting 2025-08-28 17:39:48 +02:00
Oshgnacknak bf9e5e2a3c Allow serial connection errors to bubble up and actually log them 2025-08-28 17:39:48 +02:00
Oshgnacknak b1ccbf3585 Fix permission issues for USB serial devices 2025-08-28 17:39:48 +02:00
Oshgnacknak 1f1050f115 Remove unmaintained serial crate on favor of serialport 2025-08-28 17:39:48 +02:00
Christian df0f86ce7e feat(extruder): Add total energy consumption tracking and related metrics 2025-08-28 16:55:26 +02:00
Icedsoda 007def4a01 Merge pull request #635 from qitechgmbh/627-add-documentation-for-hardware-tests-memtestcyclictest
627 add documentation for hardware tests memtestcyclictest
2025-08-28 16:39:18 +02:00
Icedsoda 3737cc4292 Merge pull request #636 from qitechgmbh/511-add-a-code-styling-document
511 add a code styling document
2025-08-28 16:38:40 +02:00
Icedsoda 8723eb1e55 Merge pull request #638 from qitechgmbh/circular-converter
Add `CircularConverter`
2025-08-28 14:34:29 +02:00
Icedsoda 1192fc28a3 refactor(converters): Introduce CircularConverter
This patch introduces a new `CircularConverter` and refactors the `LinearStepConverter` to use it. This change separates the concerns of geometric conversion from step-based conversion, leading to a more modular and maintainable design.

- **feat(converters): Add `CircularConverter`**
  - A new `CircularConverter` is added to handle all conversions between linear and angular quantities (position, velocity, acceleration, jerk).
  - It can be initialized from radius, diameter, or circumference.
  - This centralizes the geometric conversion logic into a single, well-tested module.

- **refactor(converters): Update `LinearStepConverter`**
  - The `LinearStepConverter` is refactored to compose and use the new `CircularConverter`.
  - It no longer stores `radius` or `circumference` directly.
  - All linear-to-angular calculations are now delegated to the `CircularConverter`, simplifying the `LinearStepConverter`'s implementation and removing duplicated code.
2025-08-28 14:32:07 +02:00
Oshgnacknak 23af0a82ec Merge pull request #637 from qitechgmbh/transmission
Add `Transmission` trait and `FixedTransmission` struct
2025-08-28 14:02:49 +02:00
Icedsoda a2ee1d91d9 Refactor: Introduce generic Transmission trait
This patch refactors the transmission calculation logic by replacing the specific `TransmissionConverter` with a more generic `Transmission` trait.

- A new `transmission` module is created, containing the `Transmission` trait and a `FixedTransmission` implementation.
- The `Transmission` trait provides a generic interface for converting between input and output values for various `uom` types (e.g., `AngularVelocity`, `Angle`, `Velocity`).
- The hardcoded `TransmissionConverter` is removed.
- The `ScrewSpeedController` is updated to use the new `FixedTransmission`, making the ratio configuration more explicit and improving type safety.
2025-08-28 13:45:22 +02:00
93094049+kraemr@users.noreply.github.com eb9830f8a7 updated docs 2025-08-28 13:31:14 +02:00
93094049+kraemr@users.noreply.github.com 3f92b11864 added code-styling backend doc 2025-08-28 13:31:14 +02:00
93094049+kraemr@users.noreply.github.com 96785e8876 updated docs 2025-08-28 13:01:17 +02:00
93094049+kraemr@users.noreply.github.com d36925f616 added memtest docs 2025-08-28 13:01:17 +02:00
Robin Krämer bdc8d7bbd0 Merge pull request #621 from qitechgmbh/no-driver-message-hex
refactor(devices): Display device IDs in hex format
2025-08-27 14:31:46 +02:00
Icedsoda 268685858d refactor(devices): Display device IDs in hex format
- Change the error message for unknown devices to display IDs in hexadecimal format.
- This improves readability and makes it easier to look up device information.
2025-08-27 14:29:22 +02:00
Icedsoda eeafd93e3e Merge pull request #630 from qitechgmbh/629-ethercrab-submodule-on-master
removed ethercrab submodule
2025-08-27 14:28:48 +02:00
93094049+kraemr@users.noreply.github.com 8fa6213f9c removed submodule 2025-08-27 14:23:55 +02:00
Robin Krämer f8b186ec5d Merge pull request #618 from qitechgmbh/fail-early
Fail early in run script if compile fails
2025-08-27 14:23:48 +02:00
Oshgnacknak 4f6a1a0493 Fail early in run script if compile fails 2025-08-27 13:53:24 +02:00
Icedsoda 8ee9b233f6 Merge pull request #608 from qitechgmbh/copilot-instructions
Add copilot-instructions.md
2025-08-27 13:39:18 +02:00
IcedsodaandCopilot 4f6f148c70 add compilot-instructions.md
Update .github/copilot-instructions.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-27 13:36:46 +02:00
Icedsoda 3ea8f38139 Merge pull request #628 from qitechgmbh/changelog-2.5.0
Update CHANGELOG.md
2025-08-27 13:15:27 +02:00
Robin Krämer 3657bbf53e Update CHANGELOG.md 2025-08-27 13:14:30 +02:00
Robin Krämer 92b098c240 Update CHANGELOG.md 2025-08-27 12:48:14 +02:00
Robin Krämer 7350eeb354 Merge pull request #626 from qitechgmbh/591-improve-elctron-frame-times
591 improve electron frame times
2025-08-27 11:37:23 +02:00
93094049+kraemr@users.noreply.github.com 67e2b33db7 intel driver properly loads now
added comments and increased niceness values

added intel gpu driver as module

added mesa mesa_drivers

mesa_drivers has been removed

enable drm render

force i915 enable

fixed config issue

commented kernelPatches

hopefully fixed  now

added intel as xvideo driver

removed driSupport

fixed config file

removed deprecated powermanagement

does not exist ...

downgrade to lts rt kernel

nixos is not behaving

added neofetch and pciutils
2025-08-27 11:33:54 +02:00
Icedsoda d9eb49c37c Merge pull request #623 from qitechgmbh/el2008-identity-b
Add `EL2008_IDENTITY_B`
2025-08-25 16:52:30 +02:00
Icedsoda dba7ce7494 Add EL2008_IDENTITY_B 2025-08-25 16:26:27 +02:00
Icedsoda 50d16feb3d Merge pull request #620 from qitechgmbh/re-introduce-io-uring
Re-introduce `io_uring`
2025-08-25 15:57:27 +02:00
Icedsoda 437f94659c Re-introduce io_uring 2025-08-25 14:20:59 +02:00
Melvin Spieß 53efecc636 Merge pull request #614 from qitechgmbh/613-io_uring-causes-setup-to-fail
removed io_uring
2025-08-21 12:15:16 +02:00
93094049+kraemr@users.noreply.github.com c2371d095e use tx_rx_task import 2025-08-20 16:33:00 +02:00
93094049+kraemr@users.noreply.github.com cacde7868c removed io_uring 2025-08-20 15:33:34 +02:00
Icedsoda 7cf06e6a9d Merge pull request #607 from qitechgmbh/copilot/fix-3cec0eaa-f61d-4097-a3a3-fb391de9205b
Add EtherCAT IO performance metrics logging (fixes #578)
2025-08-20 12:46:16 +02:00
Icedsoda 83688daaf4 Refactor: Rename cycle metrics to loop and adjust config
This patch refactors the performance metrics collection for better clarity and tunes its parameters.

- Renamed "cycle" metrics to "loop" metrics to more accurately reflect the timing of the main processing loop. This change is applied to variable names and log messages.
- Increased the metrics window size from 10,000 to 30,000 samples.
- Extended the logging interval from 5 to 30 seconds for less verbose output.
- Simplified log messages for brevity.
2025-08-20 12:44:49 +02:00
copilot-swe-agent[bot]and1cedsoda f55a6c6569 Add EtherCAT performance metrics module and integrate into app state
Co-authored-by: 1cedsoda <29167146+1cedsoda@users.noreply.github.com>
2025-08-20 12:37:41 +02:00
copilot-swe-agent[bot] e42cc64f5d Initial plan 2025-08-20 12:37:41 +02:00
Robin Krämer 5c0a9f3bb6 Merge pull request #602 from qitechgmbh/594-remove-motor-volts-graph
Remove motor voltage references from extruder components
2025-08-20 12:30:41 +02:00
Christian b719731926 Remove motor voltage references from extruder components 2025-08-20 12:22:56 +02:00
Icedsoda 5b4d81fec2 Merge pull request #599 from qitechgmbh/593-Fix-Graph-Labels-for-Temperatures
Add title to temp value sensors in temperature graph
2025-08-20 12:17:32 +02:00
Robin Krämer 4f50395eb9 Merge pull request #600 from qitechgmbh/595-Extend-Power-Outputs-Graph
Add combined power and motor power graphs to Extruder2GraphsPage, cor…
2025-08-20 12:09:51 +02:00
Christian 30a7134db9 Add combined power and motor power graphs to Extruder2GraphsPage, correct the order of the values by front to back 2025-08-19 13:57:32 +02:00
Robin Krämer 474d64c2a2 Merge pull request #601 from qitechgmbh/598-Images-in-the-manual-are-not-loading
Fix images in the manual not loading
2025-08-19 13:53:52 +02:00
Christian 0293654803 fix code styling 2025-08-18 15:51:54 +02:00
Christian 80ca509998 Move images to electron public folder 2025-08-18 15:26:38 +02:00
Christian a84157bd94 Add title to temp value sensors in temperature graph 2025-08-15 14:18:12 +02:00
Oshgnacknak 0007f14296 Merge pull request #592 from qitechgmbh/540-long-preset-names-mess-with-ui
Fix 540: long preset names mess with ui
2025-08-14 18:06:17 +02:00
Oshgnacknak 307ad265a9 Move preset menu options into hamburger menu 2025-08-14 18:05:02 +02:00
Oshgnacknak 41f36fdf2e Fix ellipsis when rendering long preset names 2025-08-14 18:05:02 +02:00
Oshgnacknak 86c28fef8a Merge pull request #590 from qitechgmbh/changelog_2.4.0_update
Update CHANGELOG.md
2025-08-14 17:37:21 +02:00
Robin Krämer c01c09548c Update CHANGELOG.md 2025-08-14 17:34:40 +02:00
Oshgnacknak 8f76bb88e7 Merge pull request #589 from qitechgmbh/588-limit-backend-and-frontend-to-30-fps
588 limit backend and frontend to 30 fps
2025-08-14 17:19:04 +02:00
93094049+kraemr@users.noreply.github.com ea24c2f85c replaced 60fps with 30fps 2025-08-14 17:14:35 +02:00
93094049+kraemr@users.noreply.github.com 806603c583 Every machine now sends live events every 33.3 ms 2025-08-14 17:13:18 +02:00
93094049+kraemr@users.noreply.github.com 4b9a788be4 changed comments and timeouts to 30fps 2025-08-14 17:13:18 +02:00
93094049+kraemr@users.noreply.github.com 2d91d34e9a Introduced a 30FPS Constant 2025-08-14 17:13:18 +02:00
Oshgnacknak 7e50cce009 Merge pull request #582 from qitechgmbh/dependabot/cargo/libc-0.2.175
Bump libc from 0.2.172 to 0.2.175
2025-08-14 16:53:39 +02:00
Robin Krämer 2f642a11d0 Merge pull request #587 from qitechgmbh/574-isolate-core-for-realtime-kernel
testing with cores 2,3 isolated
2025-08-14 16:19:39 +02:00
93094049+kraemr@users.noreply.github.com 6cb027b12e added comments 2025-08-14 16:15:35 +02:00
93094049+kraemr@users.noreply.github.com 58903a885d testing with cores 2,3 isolated 2025-08-14 16:15:35 +02:00
Oshgnacknak eda4b7a9b5 Merge pull request #584 from qitechgmbh/dependabot/cargo/syn-2.0.105
Bump syn from 2.0.101 to 2.0.105
2025-08-14 16:14:19 +02:00
Oshgnacknak 2f48b4093f Merge pull request #556 from qitechgmbh/dependabot/npm_and_yarn/electron/tmp-0.2.4
Bump tmp from 0.2.3 to 0.2.4 in /electron
2025-08-14 16:14:03 +02:00
dependabot[bot] 676f19ea0d Bump libc from 0.2.172 to 0.2.175
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.172 to 0.2.175.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.175/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.172...0.2.175)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.175
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 14:02:55 +00:00
dependabot[bot] 0252cd5ef3 Bump syn from 2.0.101 to 2.0.105
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.101 to 2.0.105.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.101...2.0.105)

---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.105
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 14:01:45 +00:00
dependabot[bot] ac248e31a1 Bump tmp from 0.2.3 to 0.2.4 in /electron
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 14:01:19 +00:00
Oshgnacknak 58e43f4fc8 Merge pull request #545 from qitechgmbh/dependabot/cargo/tokio-1.47.1
Bump tokio from 1.45.0 to 1.47.1
2025-08-14 15:58:45 +02:00
Oshgnacknak 3845926b97 Merge pull request #581 from qitechgmbh/dependabot/npm_and_yarn/electron/tanstack/react-query-5.84.2
Bump @tanstack/react-query from 5.84.1 to 5.84.2 in /electron
2025-08-14 15:56:30 +02:00
Oshgnacknak 78c99580bf Merge pull request #583 from qitechgmbh/dependabot/cargo/rand-0.9.2
Bump rand from 0.9.1 to 0.9.2
2025-08-14 15:55:40 +02:00
dependabot[bot] 3526711f4d Bump rand from 0.9.1 to 0.9.2
Bumps [rand](https://github.com/rust-random/rand) from 0.9.1 to 0.9.2.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.1...rand_core-0.9.2)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:47:44 +00:00
dependabot[bot] 5ab97839c6 Bump @tanstack/react-query from 5.84.1 to 5.84.2 in /electron
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.84.1 to 5.84.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.84.2/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.84.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:42:48 +00:00
Oshgnacknak c92455226f Merge pull request #566 from qitechgmbh/dependabot/npm_and_yarn/electron/eslint-plugin-prettier-5.5.4
Bump eslint-plugin-prettier from 5.5.3 to 5.5.4 in /electron
2025-08-14 15:41:27 +02:00
Oshgnacknak 3b91162fd3 Merge pull request #565 from qitechgmbh/dependabot/npm_and_yarn/electron/types/node-24.2.1
Bump @types/node from 22.17.0 to 24.2.1 in /electron
2025-08-14 15:40:57 +02:00
dependabot[bot] c3da8bb357 Bump tokio from 1.45.0 to 1.47.1
Dependabot couldn't find the original pull request head commit, 32fa361c9bbd7a001d6efe407f2697c376ee0d54.
2025-08-14 13:36:08 +00:00
Oshgnacknak 70dd9d2072 Merge pull request #586 from qitechgmbh/youtube-thumbnais
Add YouTube Thumbnail to README.md
2025-08-14 15:36:00 +02:00
Oshgnacknak 2a87e17909 Merge pull request #375 from qitechgmbh/dependabot/cargo/thread-priority-2.1.0
Bump thread-priority from 1.2.0 to 2.1.0
2025-08-14 15:33:57 +02:00
Oshgnacknak ac95e23e18 Merge pull request #585 from qitechgmbh/dependabot/npm_and_yarn/electron/electron/fuses-2.0.0
Bump @electron/fuses from 1.8.0 to 2.0.0 in /electron
2025-08-14 15:33:09 +02:00
dependabot[bot] 5e606e485f Bump @electron/fuses from 1.8.0 to 2.0.0 in /electron
Bumps [@electron/fuses](https://github.com/electron/fuses) from 1.8.0 to 2.0.0.
- [Release notes](https://github.com/electron/fuses/releases)
- [Changelog](https://github.com/electron/fuses/blob/main/.releaserc.json)
- [Commits](https://github.com/electron/fuses/compare/v1.8.0...v2.0.0)

---
updated-dependencies:
- dependency-name: "@electron/fuses"
  dependency-version: 2.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:32:50 +00:00
Icedsoda a35f65b8d0 Update README.md 2025-08-14 15:31:49 +02:00
dependabot[bot] 587f7f6c88 Bump thread-priority from 1.2.0 to 2.1.0
Bumps [thread-priority](https://github.com/iddm/thread-priority) from 1.2.0 to 2.1.0.
- [Commits](https://github.com/iddm/thread-priority/commits)

---
updated-dependencies:
- dependency-name: thread-priority
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:31:01 +00:00
dependabot[bot] eedf71b933 Bump eslint-plugin-prettier from 5.5.3 to 5.5.4 in /electron
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.5.3 to 5.5.4.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.3...v5.5.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.5.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:30:43 +00:00
dependabot[bot] 671c36e9c1 Bump @types/node from 22.17.0 to 24.2.1 in /electron
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.17.0 to 24.2.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:30:31 +00:00
Oshgnacknak f59b1f4096 Merge pull request #564 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-37.2.6
Bump electron from 36.7.3 to 37.2.6 in /electron
2025-08-14 15:29:15 +02:00
dependabot[bot] 138c5614d1 Bump electron from 36.7.3 to 37.2.6 in /electron
Bumps [electron](https://github.com/electron/electron) from 36.7.3 to 37.2.6.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v36.7.3...v37.2.6)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 37.2.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 13:28:18 +00:00
Oshgnacknak b0e1a09d61 Merge pull request #580 from qitechgmbh/depbot
Fix dependabot.yml
2025-08-14 15:26:45 +02:00
Oshgnacknak 29a8a5281b Fix dependabot.yml 2025-08-14 15:19:49 +02:00
Oshgnacknak 92cb25749a Merge pull request #579 from qitechgmbh/1cedsoda-patch-1
Make dependabot less verbose
2025-08-14 15:12:35 +02:00
Icedsoda 0b26dfe9de Update dependabot.yml 2025-08-14 15:10:38 +02:00
Oshgnacknak a1117ad09d Merge pull request #353 from qitechgmbh/dependabot/cargo/serialport-4.7.2
Bump serialport from 4.7.1 to 4.7.2
2025-08-14 15:10:07 +02:00
Oshgnacknak d39bd0b7fa Merge pull request #350 from qitechgmbh/dependabot/cargo/socketioxide-0.17.2
Bump socketioxide from 0.17.1 to 0.17.2
2025-08-14 15:09:29 +02:00
Oshgnacknak 51db45c341 Merge pull request #567 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-forge/maker-rpm-7.8.3
Bump @electron-forge/maker-rpm from 7.8.2 to 7.8.3 in /electron
2025-08-14 15:06:32 +02:00
Oshgnacknak c74fabb1f0 Merge pull request #505 from qitechgmbh/build-frontend-in-ci
Build front-end in CI
2025-08-14 15:03:21 +02:00
Oshgnacknak afc9b2a135 Fix compile errors 2025-08-14 14:55:17 +02:00
Oshgnacknak e3cfb6d0a0 Build front-end in CI
Currently, electron-forge seems to mute a loot of the error messages.
This is why first compiling with tsc might help
2025-08-14 14:55:17 +02:00
Robin Krämer 30a521f27d Merge pull request #572 from qitechgmbh/io-uring
Use `io_uring` on linux
2025-08-14 14:53:11 +02:00
Icedsoda bba2759c1a use io_uring on linux 2025-08-14 14:48:59 +02:00
Oshgnacknak 4c1f870eaa Merge pull request #577 from qitechgmbh/576-missing-el2002-revision
576 missing el2002 revision
2025-08-14 14:45:53 +02:00
93094049+kraemr@users.noreply.github.com 5d317a9841 added missing properties 2025-08-13 15:22:38 +02:00
93094049+kraemr@users.noreply.github.com 4c750ec3be added missing revision to el2002 2025-08-13 15:10:21 +02:00
Robin Krämer da531208e5 Merge branch 'master' into dependabot/npm_and_yarn/electron/electron-forge/maker-rpm-7.8.3 2025-08-13 14:24:19 +02:00
Robin Krämer 554560681b Merge pull request #516 from qitechgmbh/format-write
Allow running linter with fix option
2025-08-13 14:23:43 +02:00
Oshgnacknak 05198ee7e9 Allow running linter with fix option 2025-08-13 14:17:33 +02:00
Robin Krämer a3b9f1b505 Merge pull request #551 from qitechgmbh/550-Allow-wound-distance-reset-during-winding
550 allow wound distance reset during winding
2025-08-13 14:14:39 +02:00
Christian 08578bf856 add warning dialog when wantig to reset while winding 2025-08-13 14:09:30 +02:00
Christian ea65f3cd2c make reset possible while winding 2025-08-13 14:09:30 +02:00
Robin Krämer 4df8815bfa Merge pull request #463 from qitechgmbh/445-implement-el3062-0030
Add `EL3062-0030`
2025-08-13 13:53:28 +02:00
Darmenov Alisher 5f645c3d3a working el3062-0030 2025-08-13 13:50:10 +02:00
Darmenov Alisher c774ace112 feat: Add support for EL3062-0030 device in EtherCAT HAL 2025-08-13 13:50:10 +02:00
Icedsoda 64a6ab0410 Add EL3062-0030 EEPROM dump 2025-08-13 13:50:10 +02:00
Robin Krämer def9c22b64 Merge pull request #571 from qitechgmbh/570-preempt_rt-causes-electron-to-lag
added niceness value to wrapper
2025-08-11 16:10:56 +02:00
93094049+kraemr@users.noreply.github.com 6d6f90bf9f added niceness value to wrapper 2025-08-11 14:06:07 +02:00
Robin Krämer 12dc482e91 Merge pull request #560 from qitechgmbh/fix-typo-in-traverse_controller
Fix typo in traverse controller
2025-08-11 14:04:49 +02:00
snacj b52c047cdf Fix typo in traverse controller 2025-08-11 13:59:10 +02:00
Robin Krämer fa75e91ce6 Merge pull request #569 from qitechgmbh/568-use-realtime-kernel-preempt_rt
fixed config for RT Kernel
2025-08-11 13:29:22 +02:00
93094049+kraemr@users.noreply.github.com c52d4d464d fixed config for RT 2025-08-11 12:17:25 +02:00
Robin Krämer 9719b5251c Merge pull request #563 from qitechgmbh/562-Add-electrical-wiring-diagrams-for-extruder-and-winder
Add wiring diagrams for extruder and winder.
2025-08-11 11:20:53 +02:00
dependabot[bot] ea796bbea6 Bump @electron-forge/maker-rpm from 7.8.2 to 7.8.3 in /electron
Bumps [@electron-forge/maker-rpm](https://github.com/electron/forge) from 7.8.2 to 7.8.3.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.8.2...v7.8.3)

---
updated-dependencies:
- dependency-name: "@electron-forge/maker-rpm"
  dependency-version: 7.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 03:56:03 +00:00
Christian 594a8730a4 Refractor file naming 2025-08-10 14:56:23 +02:00
Christian 03ae4aeefd Add wiring diagrams for extruder and winder. 2025-08-10 14:52:21 +02:00
Oshgnacknak 57f6e50e0e Merge pull request #558 from qitechgmbh/557-fix-rust-formatting-error
removed unused imports
2025-08-07 15:05:49 +02:00
93094049+kraemr@users.noreply.github.com fcebb2d0f2 fixed formatting error 2025-08-07 14:55:07 +02:00
93094049+kraemr@users.noreply.github.com 56ebc80aab removed unused imports 2025-08-07 14:47:24 +02:00
Oshgnacknak 6d8aba1577 Merge pull request #555 from qitechgmbh/rust-formating
Run formatter for rust code in ci
2025-08-06 16:36:21 +02:00
Oshgnacknak ebaf033b36 Run formatter in ci 2025-08-06 16:35:40 +02:00
Oshgnacknak 71f314cf75 Fix warnings 2025-08-06 16:35:40 +02:00
Oshgnacknak f520c759ce Run formatter once 2025-08-06 16:35:40 +02:00
Robin Krämer b8d0b1a630 Update CHANGELOG.md
Update CHANGELOG.md
2025-08-06 16:30:24 +02:00
Icedsoda 11deedb75d Merge pull request #553 from qitechgmbh/552-show-all-branches-on-the-choosversionpage
Show all branches
2025-08-05 16:45:31 +02:00
Robin Krämer 50a61f9e21 Merge pull request #549 from qitechgmbh/519-show-inverter-voltagecurrent
519 show inverter voltagecurrent
2025-08-05 15:48:00 +02:00
Christian 2df5a06930 fix control page graph 2025-08-05 15:44:55 +02:00
TheBest6337 0ae30ba8fb fix styling 2025-08-05 15:44:55 +02:00
TheBest6337 2b80d6c806 Smaller Timewindow and sampleInterval to be more perfomant 2025-08-05 15:44:55 +02:00
Christian fd2ff77b62 fix formatting 2025-08-05 15:44:55 +02:00
Christian 46e699b745 Add combined power consumption calculation and graph it in the ui 2025-08-05 15:44:55 +02:00
Christian 5e0dbf9d7e Add motor power and current tracking to extruder graphs and add power calculation to the extruder api 2025-08-05 15:44:55 +02:00
93094049+kraemr@users.noreply.github.com eefc9b3834 fixed errors and fixed units 2025-08-05 15:44:55 +02:00
93094049+kraemr@users.noreply.github.com bbc9783961 fixed frontend and added timeseries 2025-08-05 15:44:55 +02:00
93094049+kraemr@users.noreply.github.com 3c998d2f5c fixed naming 2025-08-05 15:44:55 +02:00
93094049+kraemr@users.noreply.github.com 52cd1ce050 added backend to read frequency, voltage and current of motor 2025-08-05 15:44:55 +02:00
Icedsoda 5140410006 Show all branches 2025-08-04 15:53:46 +02:00
Christian 852beb5a06 Revert "flip plus and minus buttons"
This reverts commit 2cf150a541.
2025-07-31 13:52:42 +02:00
TheBest6337 2cf150a541 flip plus and minus buttons 2025-07-31 13:48:30 +02:00
Oshgnacknak 4f2f1b227c Merge pull request #541 from qitechgmbh/526-import-and-export-presets
Allow importing and exporting presets
2025-07-30 23:37:51 +02:00
Oshgnacknak 7c1f77ff7b Allow importing presets from file 2025-07-30 23:29:54 +02:00
Oshgnacknak 8b2b769435 Use typed storage instead of any 2025-07-30 23:29:54 +02:00
Oshgnacknak 5350ca8f53 General purpose file input matching our button style 2025-07-30 23:29:40 +02:00
Oshgnacknak 14f4ac1248 Allow exporting individual presets 2025-07-30 19:50:57 +02:00
Oshgnacknak 548ecf5723 Merge pull request #539 from qitechgmbh/537-developer-docs-for-presers
Create presets documentation
2025-07-29 16:01:07 +02:00
Oshgnacknak 99e74759b5 Create presets.md 2025-07-29 15:59:17 +02:00
Oshgnacknak ec177906c8 Merge pull request #538 from qitechgmbh/527-presets-for-the-laser
Implement presets for the laser
2025-07-29 14:44:38 +02:00
Oshgnacknak afdf38eaf2 Implement presets for the laser
Closes #527
2025-07-29 14:42:16 +02:00
Oshgnacknak d8d509812b Merge pull request #531 from qitechgmbh/525-preset-migrations
Validate presets after loading them from the store
2025-07-29 14:23:41 +02:00
Robin Krämer 3eeeadab5c Update CHANGELOG.md 2025-07-25 17:39:43 +02:00
Robin Krämer 657c3cff85 Update CHANGELOG.md 2025-07-25 17:38:46 +02:00
Oshgnacknak 7ea7721ea3 Validate presets after loading them from the store 2025-07-25 17:16:07 +02:00
Robin Krämer b5ccf10128 Merge pull request #528 from qitechgmbh/524-error-thrown-in-console-when-no-presets-exist
Fix loading presets if the store does not exist yet
2025-07-25 16:17:09 +02:00
Oshgnacknak a5a0b1a5e5 Fix loading presets if the store does not exist yet 2025-07-25 16:08:31 +02:00
Robin Krämer 1e8ef812e9 Merge pull request #523 from qitechgmbh/520-add-overload-warning
add overload warning for the inverter
2025-07-25 14:51:17 +02:00
93094049+kraemr@users.noreply.github.com 4965e73b0c fixed styling 2025-07-25 12:40:19 +02:00
Christian 6db2b91052 add overload warning for the inverter 2025-07-24 18:52:31 +02:00
Robin Krämer a930d058c2 Merge pull request #504 from qitechgmbh/winder-1-docs-v2
64 Winder 1 docs v2
2025-07-24 14:38:28 +02:00
93094049+kraemr@users.noreply.github.com 5b7acbc7f3 updated prettier version 2025-07-24 14:27:58 +02:00
93094049+kraemr@users.noreply.github.com 344d319d12 force line endings 2025-07-24 14:22:33 +02:00
93094049+kraemr@users.noreply.github.com d5534e0967 line endings caused issues 2025-07-24 14:18:38 +02:00
93094049+kraemr@users.noreply.github.com 680cb71dde added manual back to winder 2025-07-24 14:02:05 +02:00
93094049+kraemr@users.noreply.github.com a26ccb0a1f fixed regression 2025-07-24 14:02:05 +02:00
93094049+kraemr@users.noreply.github.com 9fa037a9f9 fixed styling again 2025-07-24 14:02:05 +02:00
93094049+kraemr@users.noreply.github.com f07dcbf8f8 fixed styling issues 2025-07-24 14:02:04 +02:00
Christian 783ee38a3a fix: sizing and formatting errors 2025-07-24 14:02:04 +02:00
Christian 4a7be41dfe add line under headings and make them bolt 2025-07-24 14:02:04 +02:00
Christian 8b6960ae64 fix: adjust list styling in Markdown component for better alignment 2025-07-24 14:02:04 +02:00
Christian cf0b71bb69 fix: typo 2025-07-24 14:02:04 +02:00
Christian e5608d061f fix: improve Markdown component styling 2025-07-24 14:02:04 +02:00
Christian 7eba9523d4 fix: update image paths in manuals for correct referencing 2025-07-24 14:02:04 +02:00
Christian 5ad85b1a8c add rehype-raw plugin to Markdown component for HTML support 2025-07-24 14:02:04 +02:00
93094049+kraemr@users.noreply.github.com 064a904975 for easier testing added winder doc to MockManual 2025-07-24 14:02:04 +02:00
93094049+kraemr@users.noreply.github.com ccbab4afba renamed winder-1 to manual 2025-07-24 14:02:04 +02:00
Janhegerq 79870d995a correction of winder usermanual 2025-07-24 14:02:04 +02:00
Janhegerq 0d95f1c44f renamed picture 2025-07-24 14:02:04 +02:00
Janhegerq 62aabaa36a fini9shed windr usermanual 2025-07-24 14:02:04 +02:00
Janhegerq b57b746379 pictures for winder 2025-07-24 14:02:04 +02:00
Janhegerq d1dbe7550d updated with pictures 2025-07-24 14:02:04 +02:00
Janhegerq 38d2364ac4 renamed file (Titelseite) 2025-07-24 14:02:04 +02:00
Janhegerq 581b4f7773 delted file(manual) 2025-07-24 14:02:04 +02:00
Janhegerq 0550ecec7b file moved to electron/src/assets/markdown/winder 2025-07-24 14:02:04 +02:00
Janhegerq 3071482a87 named winder description 2025-07-24 14:02:04 +02:00
Janhegerq 0c926a5acd renamed signs 2025-07-24 14:02:04 +02:00
Janhegerq 59387eb214 named mandatory sign 2025-07-24 14:02:04 +02:00
Janhegerq 9693af313f named mandatory signs 2025-07-24 14:02:04 +02:00
Janhegerq df1beabfa8 named security signs 2025-07-24 14:02:04 +02:00
Janhegerq 9630142921 named screenshots 2025-07-24 14:02:04 +02:00
Janhegerq fcef922ab1 named warning labels 2025-07-24 14:02:04 +02:00
Janhegerq edd8c83402 named warning labels 2025-07-24 14:02:04 +02:00
Janhegerq 09d9801482 Add files via upload 2025-07-24 14:02:04 +02:00
Janhegerq f50997ec23 Update winder-1.md
experiment Bild
2025-07-24 14:02:04 +02:00
Janhegerq 5524e9f10a Add files via upload 2025-07-24 14:02:04 +02:00
Janhegerq 7e73e2f879 Update winder-1.md 2025-07-24 14:02:04 +02:00
Robin Krämer 96719dae15 Merge pull request #517 from qitechgmbh/482-communicate-with-winder
482 communicate with winder
2025-07-23 12:18:26 +02:00
snacj e78e9ba9e3 Fixed Typo 2025-07-23 12:11:38 +02:00
snacj a730ed3228 Fixed Merge Conflicts 2025-07-23 12:04:24 +02:00
snacj c3b0a8643d Fixed unused imports unreachable code and unused configs 2025-07-23 11:55:12 +02:00
snacj 422d98bc7c Fixed machine connection backend errors 2025-07-23 11:55:12 +02:00
snacj f1b61dad9b Moved Machine Connection dropdown to settings page 2025-07-23 11:55:12 +02:00
snacj c97768975b Fixed Connection Errors 2025-07-23 11:54:29 +02:00
snacj 0b882e8bde added machine connection logic and dropdown to buffer and winder 2025-07-23 11:54:29 +02:00
snacj 6752b503bc Added Connected Machine State to Buffer and Winder 2025-07-23 11:54:29 +02:00
snacj 847963fb1c Changed Title of dropdown menu 2025-07-23 11:54:29 +02:00
snacj 4441b60d4a Added Connecting Machine Functionality to Winder and Buffer backend 2025-07-23 11:54:29 +02:00
snacj cb74f4fb6a Fixed Merge issues 2025-07-23 11:54:29 +02:00
snacj fae9c7151c Added frontend Connection Logic to Mock Machine 2025-07-23 11:54:29 +02:00
snacj 7aa36c0361 Added Reverse connection and fixed deadlock 2025-07-23 11:53:54 +02:00
snacj eb4b4e948d Fixed compiling errors due to new MachineAct trait 2025-07-23 11:53:54 +02:00
snacj 13af890e29 Added disconnection logic to backend 2025-07-23 11:53:54 +02:00
snacj d366f52127 Added Availability of machine to frontend 2025-07-23 11:53:54 +02:00
snacj 8f8535b6a3 Fixed Manager to also return weak serial machine 2025-07-23 11:53:54 +02:00
snacj 83fa27c7be Added clear selection and disconnectMachine functionality in frontend 2025-07-23 11:53:54 +02:00
snacj 8295496671 Fixed usage of component machineConnectionDropdown 2025-07-23 11:53:54 +02:00
snacj 9277521dbd Fixed Machine Selection dropdown 2025-07-23 11:53:54 +02:00
snacj 3797fc6469 Refactoring of registry 2025-07-23 11:53:54 +02:00
snacj 9700e47358 Implemented as_any function to machines 2025-07-23 11:53:54 +02:00
snacj 8ee1c63e8d Added machine manager reference to machines 2025-07-23 11:53:54 +02:00
snacj dae3def188 Added and implemented downcast function 2025-07-23 11:53:54 +02:00
snacj fa3dc0aa8c Added Buffer Speed Controller 2025-07-23 11:53:54 +02:00
snacj 69fcdb969d Changed Smartpointers from Box to Arc in machine manager 2025-07-23 11:53:54 +02:00
snacj 9b03b4d909 Fixed Typo 2025-07-23 11:53:54 +02:00
snacj 5e9fb93a21 Added MachineConnectionDropdown component 2025-07-23 11:53:54 +02:00
Robin Krämer b6842a71fb Merge pull request #514 from qitechgmbh/62-persist-config-values
Persist machine configuration in presets
2025-07-23 00:05:00 +02:00
Oshgnacknak f89645a516 Use types of schemas for type checking 2025-07-22 13:19:32 +02:00
Oshgnacknak d928a4ba81 Fix winder preset preview table 2025-07-22 13:17:02 +02:00
Oshgnacknak c457b21e64 Fix units in extruder preset preview 2025-07-22 13:17:02 +02:00
Oshgnacknak c63be3688b Introduce new preset dialog and thus fix styling 2025-07-22 13:17:02 +02:00
Oshgnacknak 21c40ac7ad Implement presets page for the extruder 2025-07-22 13:17:01 +02:00
Oshgnacknak 0938f68d8d Adapt to newer presets for winder 2 2025-07-22 13:17:01 +02:00
Oshgnacknak 87e903781b Have own zod schema for persisted presets 2025-07-22 13:17:01 +02:00
Oshgnacknak 6753f92128 Rename defaultData to defaultState 2025-07-22 13:17:01 +02:00
Oshgnacknak 59353ff06f Use z.coerce to always parse preset.lastModified as a date 2025-07-22 13:17:01 +02:00
Oshgnacknak 938bbc97a6 Parse presets from local storage via zod 2025-07-22 13:17:01 +02:00
Oshgnacknak bb02eeb2b0 Turn type Preset into zod schema 2025-07-22 13:16:57 +02:00
Oshgnacknak 7bc91d73a6 Use default state if state is not fully loaded yet 2025-07-22 13:12:10 +02:00
Oshgnacknak 59783bc2c3 Save latest settings in a preset upon machine change 2025-07-22 13:12:10 +02:00
Oshgnacknak d93ff842ab Don't show date for default preset 2025-07-22 13:12:10 +02:00
Oshgnacknak 78417edcb8 only show default preset of loaded from server 2025-07-22 13:12:10 +02:00
Oshgnacknak f0f1c2c69e Fix setting presets in mock page 2025-07-22 13:12:10 +02:00
Oshgnacknak b4043d1a75 Show a preset with the default settings to the user 2025-07-22 13:12:10 +02:00
Oshgnacknak 3614f812b9 Abstract rendering a preset's preview into its own component 2025-07-22 13:12:10 +02:00
Oshgnacknak 26d3af414f Sort presets by last modified 2025-07-22 13:12:10 +02:00
Oshgnacknak ec8eb6a835 Implement preset preview for winder2 2025-07-22 13:12:10 +02:00
Oshgnacknak 69f0f73087 Render preview dialog before applying presets 2025-07-22 13:12:09 +02:00
Oshgnacknak 0b3e4c8c96 Fix mock graph 2025-07-22 13:12:09 +02:00
Oshgnacknak b8777ee4da Filter presets to only show presets of the current machine 2025-07-22 13:12:09 +02:00
Oshgnacknak 0bc415271e Add presets page for winder2 2025-07-22 13:12:09 +02:00
Oshgnacknak efa32e91ee Implement PresetsPage in a generic manner 2025-07-22 13:12:03 +02:00
Oshgnacknak f0927513cd Allow saving and applying presets presets on the mock machine 2025-07-22 13:05:00 +02:00
Oshgnacknak 3599c26c54 Allow setting 3 frequencies in the UI 2025-07-22 13:05:00 +02:00
Oshgnacknak aa97091b98 Report multiple sine waves in the mock machine 2025-07-22 13:05:00 +02:00
Robin Krämer ea62de3fdc Merge pull request #503 from qitechgmbh/437-add-buffered-winder-base
437 add buffered winder base
2025-07-21 16:02:23 +02:00
Robin Krämer b42089e992 Merge branch 'master' into 437-add-buffered-winder-base 2025-07-21 15:58:31 +02:00
Robin Krämer ebffbb69ec Merge pull request #502 from qitechgmbh/fix-caps
Set caps also for real time
2025-07-21 15:54:48 +02:00
snacj c48b5b2292 Changed frontend Formatting 2025-07-21 15:54:36 +02:00
Oshgnacknak 8abd6558d1 Set caps also for real time clock 2025-07-21 15:51:02 +02:00
snacj dc0b4fada3 Removed unused imports and Debug messages 2025-07-21 15:49:03 +02:00
Robin Krämer 92cacbc6c8 Merge pull request #513 from qitechgmbh/510-add-a-getting-started-dev-documentation
510 add a getting started dev documentation
2025-07-21 14:28:26 +02:00
93094049+kraemr@users.noreply.github.com 4bad318ce7 updated readme and getting-started dev docs 2025-07-21 14:25:41 +02:00
93094049+kraemr@users.noreply.github.com 0eeb7f372e added Contributing 2025-07-21 14:18:30 +02:00
93094049+kraemr@users.noreply.github.com 1a72f3f8bb added inital dev docs and updated build script 2025-07-21 14:11:38 +02:00
Robin Krämer 522ace8a4f Merge pull request #508 from qitechgmbh/501-raise-pressure-limit-of-extruder-and-add-conifrmation
501 raise pressure limit of extruder and add confirmation to EditValue
2025-07-21 12:05:50 +02:00
snacj 7e776dc360 Fixed formatting 2025-07-17 16:15:54 +02:00
snacj 6487ddc6d2 Cleaned up Buffered winder base. Ready for review 2025-07-17 16:11:26 +02:00
93094049+kraemr@users.noreply.github.com 4ebd09d2eb fixed compile errors 2025-07-16 16:29:15 +02:00
snacj 330b000b6e Added start of controller logic 2025-07-16 16:24:02 +02:00
snacj 7a817945ee Added StateEvent to manipulate Live Event Data 2025-07-16 16:24:02 +02:00
snacj 47b1b7fc82 Added live event functionality with sineWave example live value. 2025-07-16 16:24:02 +02:00
snacj 7f2ea72ad3 Implemented Buffer Communication Frontend and Backend with new refactored style events. 2025-07-16 16:24:02 +02:00
snacj 62553d1a2e Buffer frontend accepts backend events. 2025-07-16 16:24:02 +02:00
snacj cff28876ee Added backend Event emits for Buffer1 2025-07-16 16:24:02 +02:00
snacj 56032ed59c Changed formatting 2025-07-16 16:24:01 +02:00
snacj 73245a31c6 added button logic to frontend buffer 2025-07-16 16:24:01 +02:00
snacj f423ac2a14 Added mutation and simulation for moving buffer motor 2025-07-16 16:24:01 +02:00
snacj 82377c2c25 Added constant for Buffer 2025-07-16 16:24:01 +02:00
snacj 08bd2c8cee Created and added Buffer1Page and Buffer1ControlPage to routes 2025-07-16 16:24:01 +02:00
snacj 7205fdea22 Changed Roles of Stepper Puller in backend to fit frontend 2025-07-16 16:24:01 +02:00
snacj c1c420ce8f Updated registry with Buffer1; Renamed BufferedWinder to Buffer1 2025-07-16 16:24:01 +02:00
snacj 576e80cc39 Added Buffer1 properties 2025-07-16 16:24:01 +02:00
snacj a51a871b70 Added different devices to new function of BufferedWinder 2025-07-16 16:24:01 +02:00
snacj f686420e1e added modes to api.rs&new.rs 2025-07-16 16:24:01 +02:00
snacj 2237a84d88 Added folder buffered_winder under server/src/machines/ ; added files act.rs;api.rs;mod.rs;new.rs to buffered_winder and added buffered_winder to mod.rs in server/src/machines 2025-07-16 16:24:01 +02:00
93094049+kraemr@users.noreply.github.com 5c00d28396 raised maximum for Target Pressure 2025-07-16 16:09:19 +02:00
93094049+kraemr@users.noreply.github.com 2b0cff358e added optional confirmation to EditValue 2025-07-16 16:08:49 +02:00
Robin Krämer c3b7f9d050 Merge pull request #498 from qitechgmbh/497-laser-control-page-diameter-circle-extends-over-the-boundaries
clamped diameter visualisation to avoid large circles
2025-07-15 16:08:17 +02:00
93094049+kraemr@users.noreply.github.com a7fce331a0 fixed lint error 2025-07-15 16:06:24 +02:00
93094049+kraemr@users.noreply.github.com 6dd8573584 removed console log and cleaned up code 2025-07-15 16:04:24 +02:00
93094049+kraemr@users.noreply.github.com 124418dc7a clamped diameter visualisation to avoid large circles 2025-07-15 16:00:49 +02:00
Robin Krämer 59e8ddbf82 Update CHANGELOG.md 2025-07-15 12:28:14 +02:00
Robin Krämer 366f58f072 Update CHANGELOG.md 2025-07-15 12:25:06 +02:00
Robin Krämer 7a12950c22 Update CHANGELOG.md 2025-07-15 12:24:47 +02:00
Robin Krämer 668000adf9 Update CHANGELOG.md 2025-07-15 12:24:07 +02:00
Robin Krämer f2a2fee8f6 Delete tmux-dev.sh
Keep this out of master branch please
2025-07-15 12:22:47 +02:00
Robin Krämer b412fb9d4e Merge pull request #492 from qitechgmbh/169-conditions-to-enter-extrusion-mode
added confirmation for extrusion mode based on temperature
2025-07-14 17:49:34 +02:00
93094049+kraemr@users.noreply.github.com 73de9f2e4c added confirmation for extrusion mode based on temperature 2025-07-14 17:47:49 +02:00
Robin Krämer f4e5570ad5 Merge pull request #480 from qitechgmbh/471-add-winder-auto-stop-or-pull-after-n-meters-of-filament
471 add winder auto stop or pull after n meters of filament
2025-07-14 17:46:09 +02:00
93094049+kraemr@users.noreply.github.com 2d1da2bbf5 small refactor/cleanup 2025-07-14 17:43:01 +02:00
93094049+kraemr@users.noreply.github.com 23cb7ce992 frontend/backend refactor and changed progress to meters 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 941a1edb0c backend refactored 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 19af948deb frontend refactoring 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 63ea9a0f7d fixed lint error 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 784e0f8652 changed naming 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com e5f6f3f8e6 implemented backend 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 7c47ef0dee implemented frontend for auto pull/stop 2025-07-14 17:33:31 +02:00
Robin Krämer 2a668d8602 Merge pull request #495 from qitechgmbh/138-update-nixos-quickstart-guidemd-to-match-current-installation-flow
138 update nixos quickstart guidemd to match current installation flow
2025-07-14 17:30:13 +02:00
93094049+kraemr@users.noreply.github.com efe18e9d81 updated quick-start guide 2025-07-14 17:29:07 +02:00
Icedsoda cf20bb57b9 Update CHANGELOG.md 2025-07-14 14:24:44 +02:00
Icedsoda 284f427da0 Update CHANGELOG.md 2025-07-14 14:24:08 +02:00
Icedsoda 9f55171286 Merge pull request #121 from qitechgmbh/fix/7
Use Realtime Scheduling
2025-07-14 14:20:54 +02:00
Icedsoda d427f50037 put both realtime threads on separate cores 2025-07-14 14:19:11 +02:00
Icedsoda 5bfaf8178f fix realtime logging 2025-07-14 14:19:11 +02:00
Icedsoda d57866e1a6 move lock emmory after tracing
Only lock future memory

disable memory locking
2025-07-14 14:19:11 +02:00
Icedsoda 401f1bf7db # This is a combination of 2 commits.
# This is the 1st commit message:

move lock emmory after tracing

# This is the commit message #2:

Only lock future memory
2025-07-14 14:18:56 +02:00
Icedsoda a46dcbd0ae Set core affinity 2025-07-14 14:18:56 +02:00
Icedsoda d2fea5f23b Add logging 2025-07-14 14:18:21 +02:00
Icedsoda 11b78638ec Merge pull request #487 from qitechgmbh/195-remove-actor-trait
Remove `Actor` trait
2025-07-11 19:09:03 +02:00
Icedsoda d6b6f76b55 move serial_interface_actors to modbus_serial_interface 2025-07-11 18:14:58 +02:00
Icedsoda bfba0b1d62 refactor mondbus/mitsubishi stuff 2025-07-11 16:52:29 +02:00
Icedsoda 3b93e6ddc6 Remove Actor trait 2025-07-11 16:52:29 +02:00
Robin Krämer 2a0ba7f72a Merge pull request #392 from qitechgmbh/391-control-server-doesnt-shutdown-properly-on-a-panic
instead of panic_handling directly shutdown
2025-07-11 16:19:27 +02:00
93094049+kraemr@users.noreply.github.com ff9fbe6a6e instead of panic_handling directly shutdown 2025-07-11 16:02:59 +02:00
93094049+kraemr@users.noreply.github.com 7c712faff9 send pressure state event when pressure sensor disconnected 2025-07-11 14:56:12 +02:00
93094049+kraemr@users.noreply.github.com 5a4ca867a8 added wiring_error to AnalogInputState 2025-07-11 14:53:45 +02:00
93094049+kraemr@users.noreply.github.com 88633798e7 added frontend logic for pressure wiring error 2025-07-11 14:53:05 +02:00
Icedsoda 86df807b01 Merge pull request #490 from qitechgmbh/489-emit-default-state
Emit default state
2025-07-10 17:44:37 +02:00
Icedsoda 3ae04f5547 Emit default state 2025-07-10 17:40:54 +02:00
Icedsoda c1ad22266d Merge pull request #488 from qitechgmbh/refactor-mock-api
Use only a StateEvent and LiveValuesEvent on Laser V1
2025-07-10 16:52:14 +02:00
Icedsoda ec4f2517f6 Refactor API of MockMachine 2025-07-10 16:50:28 +02:00
Robin Krämer 4c73710665 Merge pull request #486 from qitechgmbh/485-extruder-requestresponse-out-of-sync-again
485 fixed out of sync error and sped up inverter reaction time
2025-07-08 11:49:04 +02:00
93094049+kraemr@users.noreply.github.com fcea61979f fixed out of sync error 2025-07-08 11:43:19 +02:00
Robin Krämer bc68644468 Merge pull request #484 from qitechgmbh/483-fix-extruder-timeout
483 fixed timeout issue
2025-07-07 19:37:27 +02:00
93094049+kraemr@users.noreply.github.com 652af28c76 fixed timeout issue 2025-07-07 19:34:09 +02:00
Robin Krämer 31d6c5c4b9 Merge pull request #470 from qitechgmbh/196-extract-act-function-from-mitsubishiinverterrs485actor-by-creating-serialinterfaceactor
196 extract act function from mitsubishiinverterrs485actor by creating serialinterfaceactor
2025-07-07 16:51:26 +02:00
93094049+kraemr@users.noreply.github.com 28fdbd8bce Fixed Error Msgs in New 2025-07-07 16:36:03 +02:00
93094049+kraemr@users.noreply.github.com 314f5b3eed added improved timeout 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 8983a67390 updated imports 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 74d67cc261 moved implementations of serial_interface and invertercontroller 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 2c4c64c0b7 prevented possible panics and finished refactoring 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 758816815e extracted functionality from mitsubishicontroller and refactored 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com bf52030506 refactored out the act functionanality 2025-07-07 16:26:59 +02:00
93094049+kraemr@users.noreply.github.com 29cf495dd0 started refactoring 2025-07-07 16:26:59 +02:00
Robin Krämer 500917937b Merge pull request #476 from qitechgmbh/475-inverter-waits-unnecessarily-long
475 inverter actor only waits when necessary
2025-07-04 16:57:43 +02:00
93094049+kraemr@users.noreply.github.com 812080341c inverter actor only waits when necessary 2025-07-04 16:54:02 +02:00
Robin Krämer 221fa7edb3 Merge pull request #474 from qitechgmbh/473-usb-disconnects-randomly-nixos
473 added usbcore autosuspend to os config
2025-07-04 15:47:00 +02:00
93094049+kraemr@users.noreply.github.com fd58786524 added usbcore autosuspend to os config 2025-07-04 15:43:21 +02:00
Robin Krämer a3398cbe34 Merge pull request #468 from qitechgmbh/467-pid-pressure-mode-overshoots-drastically-when-switching-from-rpm
467 pid pressure mode overshoots drastically when switching from rpm
2025-07-03 16:46:39 +02:00
Robin Krämer b4cb5916bd remove println from clamping_timeagnostic_pid.rs 2025-07-03 16:44:41 +02:00
93094049+kraemr@users.noreply.github.com b7e9c3c1eb removed print 2025-07-03 16:15:04 +02:00
93094049+kraemr@users.noreply.github.com c291c532ec fine tuned Pressure PID 2025-07-03 15:52:43 +02:00
Robin Krämer edded9e5a6 Merge pull request #466 from qitechgmbh/465-state-data-doesnt-work-properly-when-winder-is-in-use
465 now emits state on extruder startup
2025-07-03 14:03:50 +02:00
93094049+kraemr@users.noreply.github.com eb2f0f34a4 now emits state on startup 2025-07-03 14:00:01 +02:00
Icedsoda a1fca74d66 Merge pull request #464 from qitechgmbh/453-use-only-a-stateevent-and-livevaluesevent-on-extruder-v2
refactor(extruder2): Consolidate state and event handling
2025-07-01 19:06:19 +02:00
Icedsoda 36089c3e75 refactor(extruder2): Consolidate state and event handling
This patch refactors the state management for the Extruder v2 on both the frontend and backend to use a more centralized and robust model.

- **Backend (Rust):**
  - Replaced numerous individual events with two primary events: `StateEvent` for configuration and less frequent state, and `LiveValuesEvent` for high-frequency telemetry.
  - State-mutating API calls now emit the entire updated `StateEvent`.

- **Frontend (TypeScript):**
  - Replaced multiple specialized hooks (`useInverter`, `useMode`, `useMotor`, etc.) with a single, comprehensive `useExtruder2` hook.
  - Centralized all client-side state, including optimistic updates, into the new `useExtruder2` hook.
  - UI components are updated to consume all data and actions from the single hook, simplifying data flow.
  - Renamed action functions to a consistent verb-first convention (e.g., `extruderSetMode` -> `setExtruderMode`).
2025-07-01 18:52:25 +02:00
Icedsoda d27eecd54c Merge pull request #457 from qitechgmbh/454-use-only-a-stateevent-and-livevaluesevent-on-laser-v1
Use only a StateEvent and LiveValuesEvent on Laser V1
2025-07-01 17:29:12 +02:00
Icedsoda 183166d8d5 refactor(laser): Consolidate state and event handling
This commit refactors the state management and event handling for the laser machine on both the frontend and backend. The changes clarify the distinction between infrequently changing state and frequently updated live values.

- **State and Event Consolidation:**
    - Renamed `DiameterEvent` to `LiveValuesEvent` and `LaserStateEvent` to `StateEvent` for better clarity.
    - The `StateEvent` now nests laser properties under a `laser_state` object.
    - Frontend hook `useLaser1` now manages a single, consolidated state object with optimistic updates using `immer`.

- **API and Naming Simplification:**
    - Simplified mutation names by removing the "Target" prefix (e.g., `TargetSetTargetDiameter` is now `SetTargetDiameter`).
    - Renamed variables and functions in the frontend for better readability (e.g., `laserDiameter` to `diameter`, `laserSet...` to `set...`).

- **Improved Backend Efficiency:**
    - The backend now emits the full state only when it changes, rather than on every tick.
    - Live values (diameter) are still emitted at 60 FPS.
2025-07-01 16:59:00 +02:00
Icedsoda e01a8268aa Merge pull request #461 from qitechgmbh/459-serial-machines-wont-get-executed-if-ethercat-is-not-initalized
refactor: Run EtherCAT initialization in a separate thread
2025-07-01 16:48:31 +02:00
Icedsoda d4d1969ec3 refactor: Run EtherCAT initialization in a separate thread
This change refactors the EtherCAT initialization process to be non-blocking by moving it into its own dedicated thread.

- The `init_ethercat` function now spawns a named thread ("init_ethercat") to handle interface discovery and the setup loop.
- This prevents the main application from blocking during startup while searching for a valid EtherCAT interface.
- The initialization order in `main.rs` has been slightly adjusted.
- A minor optimization was made to the laser machine actor to avoid redundant `Instant::now()` calls.
2025-07-01 16:45:48 +02:00
Icedsoda 3dc371ce68 Merge pull request #458 from qitechgmbh/455-improve-or-replace-pressure-pid
455 improve or replace pressure pid
2025-07-01 16:12:45 +02:00
93094049+kraemr@users.noreply.github.com fba882adb8 updated Changelog 2025-07-01 16:08:32 +02:00
93094049+kraemr@users.noreply.github.com e8ca268f1c removed constants, uses uom now, smooth transition from pressure to rpm, rpm to pressure 2025-07-01 16:08:32 +02:00
93094049+kraemr@users.noreply.github.com 3a0840d758 Now set_regulation doesnt reset Pressure PID 2025-07-01 16:08:24 +02:00
93094049+kraemr@users.noreply.github.com 409460736d greatly improved the precision and stability of Pressure PID 2025-07-01 15:57:57 +02:00
Icedsoda 879de6ffcc Merge pull request #456 from qitechgmbh/452-use-only-a-stateevent-and-livevaluesevent-on-winder-v2
Use only a StateEvent and LiveValuesEvent on Winder V2
2025-07-01 14:31:52 +02:00
Icedsoda d513f1ad84 Rename Winder2 Mutations 2025-07-01 14:17:43 +02:00
Icedsoda 98b65fadb6 Simplify Winder2 Events 2025-07-01 14:17:34 +02:00
Icedsoda 832bafa217 Remove Autostop API 2025-07-01 10:39:23 +02:00
Icedsoda 769dc6220f Merge pull request #451 from qitechgmbh/450-laser-ui-stops-working-when-disconnecting-and-reconnecting
Disconnect all sockets when `Namespace` is `drop`ped
2025-07-01 10:03:24 +02:00
Icedsoda a15af859d4 Disconnect all sockets when Namespace is dropped 2025-07-01 10:01:53 +02:00
Robin Krämer 9a4d1e288d Merge pull request #449 from qitechgmbh/448-extruder-does-not-do-anything-in-pressure-pid-mode
replaced Timeagnostic PID with normal PID
2025-07-01 09:02:34 +02:00
93094049+kraemr@users.noreply.github.com e20791aa6b replaced Timeagnostic PID with normal PID 2025-07-01 08:58:23 +02:00
Robin Krämer 19c4a48150 Merge pull request #444 from qitechgmbh/440-add-clamping-to-all-error-values-for-timeagnosticpid
440 add clamping to all error values for timeagnosticpid
2025-06-30 16:56:41 +02:00
93094049+kraemr@users.noreply.github.com fba5c5c4f3 renamed timeagnostic to clamping_timeagnostic 2025-06-30 16:50:53 +02:00
93094049+kraemr@users.noreply.github.com 1f05edc79f added min_signal, max_signal and tests 2025-06-30 16:48:03 +02:00
93094049+kraemr@users.noreply.github.com f79c7ba58d added clamping to timeagnostic_pid 2025-06-30 16:47:27 +02:00
Icedsoda 5e7a6f1523 Merge pull request #443 from qitechgmbh/442-function-hashing-panics-sometimes
Replaces XOR-based hashing with byte folding methods
2025-06-30 16:42:24 +02:00
Icedsoda cc41a2e696 Replaces XOR-based hashing with byte folding methods
Replaces previous XOR-based hashing functions with new byte folding methods for improved clarity and flexibility. Introduces a djb2 hashing implementation for non-cryptographic use cases.

Updates affected modules to use the new hashing functions, simplifying logic and enhancing maintainability.

Fixes inconsistencies in hashing logic across different data types.
2025-06-30 16:40:45 +02:00
Icedsoda 32ba1b0f75 Merge pull request #441 from qitechgmbh/markdown-inline-styles-in-headings
Support inline styles in markdown headings
2025-06-30 16:15:22 +02:00
Icedsoda d38b7e8051 Support inline styles in markdown headings 2025-06-30 16:08:00 +02:00
Robin Krämer 353be9a4f8 Merge pull request #439 from qitechgmbh/436-extruder-doesnt-properly-stay-in-pressure-mode
added an integral windup prevention (clamp) for integral error
2025-06-30 15:33:35 +02:00
Icedsoda ff14618ec0 Update Changelog for 2.1.0 2025-06-30 14:50:50 +02:00
93094049+kraemr@users.noreply.github.com 208306cba5 added an integral windup prevention (clamp) for integral error 2025-06-30 14:46:28 +02:00
Darmenov Alisher 7ef2c95b32 Merge pull request #415 from qitechgmbh/fix/414
Enhance Big Graph
2025-06-30 14:26:06 +02:00
Anatomik5 a41c822618 enhanced animation, graph logic, buttons, excel and etc. 2025-06-30 14:25:37 +02:00
Icedsoda 69a3cdbef3 Merge pull request #429 from qitechgmbh/428-make-adaptivespoolspeedcontroller-core-parameters-customizable
Make `AdaptiveSpoolSpeedController` Core Parameters Customizable
2025-06-28 17:23:49 +02:00
Icedsoda ff51018d35 Make AdaptiveSpoolSpeedController Core Parameters Customizable 2025-06-28 17:22:18 +02:00
Icedsoda a228906eed Merge pull request #427 from qitechgmbh/426-show-estimated-spool-diameter
Show estimated diameter
2025-06-28 16:49:29 +02:00
Icedsoda a238fc9246 Show estimated diameter 2025-06-28 16:48:00 +02:00
dependabot[bot] bf3ee15562 Bump serialport from 4.7.1 to 4.7.2
Bumps [serialport](https://github.com/serialport/serialport-rs) from 4.7.1 to 4.7.2.
- [Changelog](https://github.com/serialport/serialport-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/serialport/serialport-rs/compare/v4.7.1...v4.7.2)

---
updated-dependencies:
- dependency-name: serialport
  dependency-version: 4.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-28 13:59:48 +00:00
dependabot[bot] 48fac7f86c Bump socketioxide from 0.17.1 to 0.17.2
Bumps [socketioxide](https://github.com/totodore/socketioxide) from 0.17.1 to 0.17.2.
- [Release notes](https://github.com/totodore/socketioxide/releases)
- [Changelog](https://github.com/Totodore/socketioxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/totodore/socketioxide/compare/socketioxide-v0.17.1...socketioxide-v0.17.2)

---
updated-dependencies:
- dependency-name: socketioxide
  dependency-version: 0.17.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-28 13:59:45 +00:00
Icedsoda 82a7e2fbfa Merge pull request #401 from qitechgmbh/388-improve-spool-speed-controller-again
Improve spool speed controller (again)
2025-06-28 15:58:33 +02:00
Icedsoda 23b2fa5faf Merge pull request #424 from qitechgmbh/403-cancel-update
Make an update cancelable
2025-06-28 15:10:35 +02:00
Icedsoda 1d78bacbe8 fix test errors 2025-06-28 14:36:40 +02:00
Icedsoda 043e781049 Improve AdaptiveSpoolSpeedController 2025-06-28 14:36:40 +02:00
Icedsoda 2d23d9db2f Make two spool speed algos available 2025-06-28 14:36:40 +02:00
Icedsoda c260403370 Make update store global 2025-06-28 14:07:35 +02:00
Icedsoda 8ac9b73bd5 Delete repo folder using fs 2025-06-28 12:41:14 +02:00
Icedsoda 609cce0a42 Make update cancelable
use tree-kill
2025-06-28 12:40:58 +02:00
Icedsoda ec304649bd Merge pull request #423 from qitechgmbh/422-warning-input-crane-has-an-override-for-a-non-existent-input-nixpkgs
Fix crane nixos issue
2025-06-28 12:12:21 +02:00
Icedsoda 217edd7db0 Add update-flake-lock.sh 2025-06-28 12:03:28 +02:00
Icedsoda 5d441b2b9a Fix crane nixos issue 2025-06-28 12:01:25 +02:00
Icedsoda 157989316e Merge branch 'rename-machine-presets' 2025-06-27 20:05:10 +02:00
Oshgnacknak 7fc5a35b6a Run formatter in ci to test 2025-06-27 20:02:32 +02:00
Oshgnacknak a93c863688 Move machine properties to their own file
- rename "Presets" to "Properties"

Script to start both dev servers using tmux

Rename MachinePresets to MachineProperties
2025-06-27 20:02:32 +02:00
Icedsoda 7735a76b1f Merge pull request #412 from qitechgmbh/411-make-nixos-rust-builds-faster
Make nixos rust builds faster
2025-06-27 19:11:48 +02:00
Icedsoda 58b0412c68 Use 2 cargo build jobs 2025-06-27 19:11:16 +02:00
Icedsoda 7ade900f46 Use crane for nixos rust builds 2025-06-27 19:11:16 +02:00
Icedsoda ad678e9c67 remove rust overlay 2025-06-27 19:11:16 +02:00
Icedsoda 6455a8b6f1 Merge pull request #419 from qitechgmbh/418-traverse-doesnt-move-at-low-spool-speeds
Change `steps_per_second` from `i32` to `f64`
2025-06-27 17:24:04 +02:00
Icedsoda b1b26fb5ae Change steps_per_second from i32 to f64 2025-06-27 17:22:10 +02:00
Robin Krämer d68be75be1 Merge pull request #417 from qitechgmbh/416-remove-arbitrary-limits-in-extruder-frontend
removed hardcoded limits from HeatingZone, now configurable
2025-06-27 16:30:43 +02:00
93094049+kraemr@users.noreply.github.com f40a5ac3af removed hardcoded limits from HeatingZone, now configurable 2025-06-27 16:04:08 +02:00
Robin Krämer 0c2cf6f460 Merge pull request #413 from qitechgmbh/186-fix-rust-warnings-in-modbus-implementation
fixed warnings in modbus and extruder
2025-06-27 12:56:35 +02:00
93094049+kraemr@users.noreply.github.com b5a989b7fe fixed warnings in modbus and extruder 2025-06-27 12:51:41 +02:00
Icedsoda ab9fad2514 Add Wireshark (#410)
* Add wireshark

* fix wireshark permision issue
2025-06-27 12:35:15 +02:00
Icedsoda e3e7cbb20f Increase PDU timeout 2025-06-26 17:40:23 +02:00
Icedsoda 7b1395b803 Merge pull request #405 from qitechgmbh/404-remove-hover-styling-of-buttons
Remove `hover:` styling
2025-06-26 17:32:18 +02:00
Icedsoda d67db133ea Remove hover: styling 2025-06-26 15:49:16 +02:00
Robin Krämer d74f3aad96 Merge pull request #400 from qitechgmbh/342-mode-button-switches-back-and-forth
Fixed Mode button and Regulation button switching back and forth
2025-06-26 13:44:55 +02:00
93094049+kraemr@users.noreply.github.com 7f2b24f075 fixed warning 2025-06-26 13:41:47 +02:00
93094049+kraemr@users.noreply.github.com d609d41628 Fixed Mode button and Regulation button switching back and forth 2025-06-26 13:35:47 +02:00
Darmenov Alisher 50c241dd59 Merge pull request #399 from qitechgmbh/fix/393
Fix/393
2025-06-25 19:10:18 +02:00
Darmenov Alisher 1a6e7da996 Merge branch 'master' into fix/393 2025-06-25 19:10:12 +02:00
Anatomik5 3a0eaf53f3 handlers fix 2025-06-25 19:04:44 +02:00
Anatomik5 16a952349f graphs enchanced 2025-06-25 18:47:55 +02:00
Anatomik5 c40f256df5 graph visual enchance 2025-06-25 18:42:30 +02:00
Anatomik5 8f0cc645ef small fix graphs 2025-06-25 17:57:18 +02:00
Anatomik5 1f436a6abe color fix 2025-06-25 17:15:32 +02:00
Anatomik5 477b44c8bb fix graph and button color 2025-06-25 17:15:04 +02:00
Anatomik5 4147528d0e fixed scale problem 2025-06-25 16:01:19 +02:00
Anatomik5 2b533f2c57 fix 2 2025-06-24 17:53:40 +02:00
Darmenov Alisher afaf8c919e Merge branch 'master' into fix/393 2025-06-24 17:08:57 +02:00
Anatomik5 2e87efdb63 update handlers and graph visual 2025-06-24 17:07:15 +02:00
Anatomik5 65fe3626ba graphs enchanced 2025-06-24 16:11:00 +02:00
Darmenov Alisher 36bae74613 Merge pull request #390 from qitechgmbh/fix/389
fix
2025-06-24 11:01:58 +02:00
Anatomik5 2e34fa11f8 fix 2025-06-24 11:01:38 +02:00
Robin Krämer 3410493c62 Merge pull request #387 from qitechgmbh/386-add-configuration-for-pressure-pid
386 add configuration for pressure pid
2025-06-23 18:33:12 +02:00
93094049+kraemr@users.noreply.github.com f13ad8fd0a fixed errors 2025-06-23 18:29:25 +02:00
93094049+kraemr@users.noreply.github.com 8208762da5 removed temperature pid kept pressure pid 2025-06-23 18:24:42 +02:00
93094049+kraemr@users.noreply.github.com 1beb01da98 Revert "Revert "frontend changes for api""
This reverts commit eb08493fdc.
2025-06-23 18:21:56 +02:00
93094049+kraemr@users.noreply.github.com e3dcfaec3b Revert "Revert "bakcend api""
This reverts commit 11e965ddeb.
2025-06-23 18:19:27 +02:00
Robin Krämer efbf7bc350 Merge pull request #378 from qitechgmbh/377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses
377 inverter state machine and el6021 incorrectly handle requestresponses
2025-06-23 17:23:54 +02:00
93094049+kraemr@users.noreply.github.com 6cf769891f removed unnecessary comment 2025-06-23 17:21:17 +02:00
Robin Krämer 840e4b8566 Merge pull request #385 from qitechgmbh/384-add-graph-to-extruder
added extruder graphs
2025-06-23 17:16:42 +02:00
93094049+kraemr@users.noreply.github.com 38341f6968 added extruder graphs 2025-06-23 17:13:26 +02:00
93094049+kraemr@users.noreply.github.com 80e9726ba3 simplified SerialInterface 2025-06-23 17:07:21 +02:00
93094049+kraemr@users.noreply.github.com 8afce68864 cleanup 2025-06-23 16:10:37 +02:00
Robin Krämer 5f7b7544c3 Merge pull request #381 from qitechgmbh/355-enable-inverter-timeout-when-communication-is-interrupted
355 enable inverter timeout when communication is interrupted
2025-06-23 15:20:59 +02:00
93094049+kraemr@users.noreply.github.com 27897182f9 updated docs 2025-06-23 15:17:54 +02:00
93094049+kraemr@users.noreply.github.com 2af1f0d79c updated Docs 2025-06-23 15:16:56 +02:00
Robin Krämer 4eeaebe355 Merge pull request #380 from qitechgmbh/379-dre-laser-graph-is-broken
fixed assignment error
2025-06-23 14:45:43 +02:00
93094049+kraemr@users.noreply.github.com 1dc85828d9 fixed assignment error 2025-06-23 14:42:59 +02:00
Darmenov Alisher bf5408cd18 Merge pull request #324 from qitechgmbh/fix/320
Group Excel Button
2025-06-23 12:39:13 +02:00
Anatomik5 d80b06256a nicer code + props sync 2025-06-23 12:36:16 +02:00
Anatomik5 aa0fb88fd2 nicer code + props sync 2025-06-23 12:36:16 +02:00
Darmenov Alisher fa85501578 Merge pull request #343 from qitechgmbh/fix/318
Add Graphs to Extruder
2025-06-23 12:28:14 +02:00
Anatomik5 500390e6dd extruder graph code enchancing 2025-06-23 12:22:48 +02:00
Anatomik5 bf73f996ca fix 2025-06-23 12:22:48 +02:00
Anatomik5 cb7fb902be extruder graph 2025-06-23 12:22:48 +02:00
Anatomik5 8fabc46ad4 fix padding 2025-06-23 12:22:48 +02:00
Anatomik5 38a7da7ee2 extruder graph var 1 2025-06-23 12:22:48 +02:00
Anatomik5 bbae14a727 Group Excel Button 2025-06-23 12:22:48 +02:00
93094049+kraemr@users.noreply.github.com f4609bfb3c Merge branch 'master' into 377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses 2025-06-23 12:04:27 +02:00
93094049+kraemr@users.noreply.github.com afa4663176 cleanup of EL6021 and inverter 2025-06-23 12:02:21 +02:00
93094049+kraemr@users.noreply.github.com 9b806ee8b3 Merge branch 'inverter-fix' into 377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses 2025-06-23 11:54:57 +02:00
93094049+kraemr@users.noreply.github.com 4975d2f6a7 fixed el6021 and inverter state machine 2025-06-23 11:54:30 +02:00
93094049+kraemr@users.noreply.github.com 1a675db872 Merge branch 'master' of github.com:qitechgmbh/control 2025-06-23 11:47:00 +02:00
Robin Krämer 0230ff4f81 Merge pull request #373 from qitechgmbh/360-show-inverter-errors-and-recover
360 show inverter errors and recover
2025-06-23 11:46:50 +02:00
Robin Krämer d8039156ae Update mod.rs 2025-06-23 11:45:09 +02:00
Darmenov Alisher e7105334c4 Merge pull request #358 from qitechgmbh/fix/319
Fix/319 Code Nicer
2025-06-20 15:44:22 +02:00
Darmenov Alisher ad04ea0a62 Merge branch 'master' into fix/319 2025-06-20 15:42:48 +02:00
Anatomik5 0128268719 nicer code + props sync 2025-06-20 15:28:14 +02:00
Anatomik5 7873975980 Group Excel Button 2025-06-20 15:17:29 +02:00
Icedsoda 57883fcb7e Add retry logic to syncToStore 2025-06-20 15:16:17 +02:00
Icedsoda 5979d9497e Ship Chrome DevTools in Production 2025-06-20 15:16:17 +02:00
Icedsoda 5dade5a633 Retry EtherCAT TX/RX if it fails 2025-06-20 15:16:17 +02:00
Icedsoda 694b46afbe Reduce logging of ethercrab 2025-06-20 15:16:17 +02:00
Icedsoda 5f81b2df5b increase namespace disconnect time 2025-06-20 15:16:17 +02:00
Icedsoda ff47ed3261 fix correct cacheing durations 2025-06-20 15:16:17 +02:00
Icedsoda da84e2a562 fix heating power value flickering 2025-06-20 15:16:17 +02:00
Icedsoda a272fd57bb Use 16.67 instead of 16ms for 60fps event updates
Emit events at 60FPS
2025-06-20 15:16:17 +02:00
Icedsoda e2fb623cbb Throttle Event Stores' setState to 60fps 2025-06-20 15:16:17 +02:00
Icedsoda d797584637 Use msgpack with SocketIO 2025-06-20 15:16:17 +02:00
Icedsoda 4549d806c9 Don't use parse to improve performance 2025-06-20 15:16:17 +02:00
Icedsoda a22d1d0ff1 Don't use produce to improve performance 2025-06-20 15:16:17 +02:00
Icedsoda adde259aa0 Reduce namespace logging 2025-06-20 15:16:17 +02:00
Icedsoda 2768609286 Fix some thread assigments 2025-06-20 15:16:17 +02:00
Icedsoda 1973d8841d Add global SocketIO Queue
add init_socketio_queue
2025-06-20 15:16:17 +02:00
Icedsoda b337031272 Pass GenericEvents using Arc
make socket queue faster
2025-06-20 15:16:17 +02:00
Icedsoda 507b7d507a Fix 0 division bug in Namespace 2025-06-20 15:16:17 +02:00
Icedsoda 2f231b203c Remove trait NamespaceInterface 2025-06-20 15:16:17 +02:00
Icedsoda 6e7a528d6f Workaround for socket performance issues (hopefully solved by #269) 2025-06-20 15:16:17 +02:00
Icedsoda 43dd37e8e0 Workaround for socket performance issues (hopefully solved by #269) 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 11e965ddeb Revert "bakcend api"
This reverts commit db039c7ccc.
2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com eb08493fdc Revert "frontend changes for api"
This reverts commit 7f7984ebe4.
2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 27b0386eac added comments to el6021 init 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com c4314a45dd refactored, clamped nozzle temp 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 02a436f053 el6021 properly handles receive_request 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com fea487c74e rpm now gets read perfectly again 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 7e317c4b23 temporary fix for rpm monitor 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 2c02d98890 bakcend api 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com bdbad1dca2 frontend changes for api 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 202ae24eea fixed pressure PID 2025-06-20 15:16:17 +02:00
Icedsoda 38db16468e Add Extruder Icon 2025-06-20 15:16:17 +02:00
Icedsoda 1f272f4bce Selection buttons can have a confirmation prompt 2025-06-20 15:16:17 +02:00
dependabot[bot] ed2b94ce05 Bump globals from 15.15.0 to 16.2.0 in /electron
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.2.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.2.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
dependabot[bot] 146365fae2 Bump crc from 3.2.1 to 3.3.0
Bumps [crc](https://github.com/mrhooray/crc-rs) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/mrhooray/crc-rs/releases)
- [Commits](https://github.com/mrhooray/crc-rs/commits)

---
updated-dependencies:
- dependency-name: crc
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
dependabot[bot] 3bf9956c41 Bump @vitejs/plugin-react from 4.3.4 to 4.5.1 in /electron
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.4 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.5.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
Icedsoda 7d8f833063 Add EL6021_IDENTITY_D 2025-06-20 15:16:17 +02:00
Icedsoda fcdac437eb Change Laser Icon 2025-06-20 15:16:17 +02:00
Icedsoda cb74dbc22b Reanem Dre to Laser 2025-06-20 15:16:17 +02:00
Icedsoda a749f98e81 Fix NixOS settings with wrong units 2025-06-20 15:15:11 +02:00
Icedsoda 634c00cdd1 Switches to tee for safer file redirection in script
Replaces `cat` with `sudo tee` to avoid potential permission issues and
ensure proper handling of file redirection when generating `installInfo.nix`.
Redirects output to `/dev/null` to suppress unnecessary output.
2025-06-20 15:15:11 +02:00
dependabot[bot] 5524be3ce0 Bump react-hook-form from 7.54.2 to 7.57.0 in /electron
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.2 to 7.57.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.2...v7.57.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:15:11 +02:00
Icedsoda 77bdb2f92c Bump OpenTelemetry Dependencies 2025-06-20 15:15:11 +02:00
nixos-install.sh 163fae21eb Remove KV pairs in logs 2025-06-20 15:15:11 +02:00
nixos-install.sh f58226aa27 Add Markdown TOC 2025-06-20 15:15:11 +02:00
nixos-install.sh 2742a296ef Revert "Add installInfo.nix file with current commit information"
This reverts commit d0a98557d3.
2025-06-20 15:15:11 +02:00
Icedsoda bb204d6efb delete summary.txt 2025-06-20 14:56:39 +02:00
Anatomik5 3a7c99cbbd fixed lint bugs 2025-06-18 17:51:47 +02:00
Anatomik5 7796d20984 nice code, modular 2025-06-18 16:40:00 +02:00
Anatomik5 9b3223c014 fixed problem with time and problem with mode switching 2025-06-18 16:40:00 +02:00
Anatomik5 a5c5ba4b87 nicer code + props sync 2025-06-18 16:40:00 +02:00
93094049+kraemr@users.noreply.github.com b53fea1812 frontendChanges for readStatus and resetInverter 2025-06-17 18:59:44 +02:00
93094049+kraemr@users.noreply.github.com b71a3456ce backend changes for readStatus and ResetInverter 2025-06-17 18:59:27 +02:00
93094049+kraemr@users.noreply.github.com 81cb44ed2b handle_response inverterstatus 2025-06-17 17:48:35 +02:00
Icedsoda 8c1fe6df1f Merge pull request #370 from qitechgmbh/add-retry-logic-to-sync-to-store
Add retry logic to `syncToStore`
2025-06-17 17:05:34 +02:00
Icedsoda 91fadd1c3d Merge pull request #372 from qitechgmbh/371-ship-chrome-devtools-in-production
Ship Chrome DevTools in Production
2025-06-17 16:51:02 +02:00
Icedsoda 28660a0873 Ship Chrome DevTools in Production 2025-06-17 16:49:57 +02:00
Icedsoda 5171ef5ef4 Add retry logic to syncToStore 2025-06-17 16:42:54 +02:00
Icedsoda 0cfd7eec9f Merge pull request #369 from qitechgmbh/367-retry-ethercat-txrx-if-it-fails
Retry EtherCAT TX/RX if it fails
2025-06-17 16:41:24 +02:00
Icedsoda 2ed2361fdf Retry EtherCAT TX/RX if it fails 2025-06-17 16:39:30 +02:00
Icedsoda 7c8b05241a Merge pull request #366 from qitechgmbh/365-reduce-logging-of-ethercrab
Reduce logging of ethercrab
2025-06-17 15:42:22 +02:00
Icedsoda 80e6edc474 Reduce logging of ethercrab 2025-06-17 15:29:06 +02:00
93094049+kraemr@users.noreply.github.com 9f13765db6 Merge branch 'master' of github.com:qitechgmbh/control 2025-06-17 14:13:56 +02:00
93094049+kraemr@users.noreply.github.com 465f8e9f07 added revision 0x10000 el6021 to frontend 2025-06-17 14:13:46 +02:00
Icedsoda ebbd4a5d20 Merge pull request #269 from qitechgmbh/improve-socket-perf
Improve SocketIO Performance
2025-06-16 18:19:42 +02:00
Icedsoda 727a87eb99 increase namespace disconnect time 2025-06-16 18:18:01 +02:00
Icedsoda dd754bdb60 Merge pull request #359 from qitechgmbh/175-fine-tune-pressure-pid-controller
175 fine tune pressure pid controller
2025-06-16 18:13:59 +02:00
Icedsoda 830a58874f fix correct cacheing durations 2025-06-16 18:04:35 +02:00
Icedsoda 2459d8236e fix heating power value flickering 2025-06-16 17:59:21 +02:00
Icedsoda 35c38dc28c Use 16.67 instead of 16ms for 60fps event updates
Emit events at 60FPS
2025-06-16 17:59:21 +02:00
Icedsoda 45d1bed990 Throttle Event Stores' setState to 60fps 2025-06-16 17:59:21 +02:00
Icedsoda 6c4f8ac37c Use msgpack with SocketIO 2025-06-16 17:59:21 +02:00
Icedsoda 22d131e9c4 Don't use parse to improve performance 2025-06-16 17:59:21 +02:00
Icedsoda 232e74c659 Don't use produce to improve performance 2025-06-16 17:59:21 +02:00
Icedsoda 85f90676e9 Reduce namespace logging 2025-06-16 17:59:21 +02:00
Icedsoda 6a46997f24 Fix some thread assigments 2025-06-16 17:59:21 +02:00
Icedsoda 69b34adb7f Add global SocketIO Queue
add init_socketio_queue
2025-06-16 17:59:21 +02:00
Icedsoda f7546058bf Pass GenericEvents using Arc
make socket queue faster
2025-06-16 17:59:21 +02:00
Icedsoda daf45d5990 Fix 0 division bug in Namespace 2025-06-16 17:59:21 +02:00
Icedsoda fa4db9c66d Remove trait NamespaceInterface 2025-06-16 17:59:21 +02:00
Icedsoda 9ce47bdfd1 Workaround for socket performance issues (hopefully solved by #269) 2025-06-16 17:59:21 +02:00
Icedsoda b0c43e9260 Workaround for socket performance issues (hopefully solved by #269) 2025-06-16 17:59:21 +02:00
Icedsoda 6f567a24b6 Merge pull request #362 from qitechgmbh/361-add-extruder-icon
Add Extruder Icon
2025-06-16 17:58:56 +02:00
Icedsoda 2680a59080 Add Extruder Icon 2025-06-16 17:51:32 +02:00
93094049+kraemr@users.noreply.github.com 0b3479bc16 Revert "bakcend api"
This reverts commit db039c7ccc.
2025-06-16 17:15:14 +02:00
Anatomik5 87112035fb nicer code + props sync 2025-06-16 17:13:05 +02:00
93094049+kraemr@users.noreply.github.com fbcae62e25 Revert "frontend changes for api"
This reverts commit 7f7984ebe4.
2025-06-16 17:00:04 +02:00
93094049+kraemr@users.noreply.github.com a15d708138 added comments to el6021 init 2025-06-16 16:58:05 +02:00
93094049+kraemr@users.noreply.github.com d4794055e1 refactored, clamped nozzle temp 2025-06-16 16:44:00 +02:00
Icedsoda eb273f928f Merge pull request #354 from qitechgmbh/282-add-a-warning-element-for-frontend
Selection buttons can have a confirmation prompt
2025-06-16 16:39:43 +02:00
Icedsoda f497a343fb Selection buttons can have a confirmation prompt 2025-06-16 16:36:31 +02:00
Anatomik5 ecb27ea068 sync enchance 2025-06-16 16:32:36 +02:00
93094049+kraemr@users.noreply.github.com 45c56e7d6e el6021 properly handles receive_request 2025-06-16 16:00:05 +02:00
Anatomik5 8c7a71a3f6 enchance 2025-06-16 15:41:38 +02:00
93094049+kraemr@users.noreply.github.com 40b167757a rpm now gets read perfectly again 2025-06-16 15:29:11 +02:00
93094049+kraemr@users.noreply.github.com 83c405abb6 temporary fix for rpm monitor 2025-06-16 13:16:58 +02:00
Anatomik5 4ea2d3bee2 button format changed 2025-06-16 01:17:06 +02:00
Icedsoda 4ac533548d Merge pull request #215 from qitechgmbh/dependabot/npm_and_yarn/electron/globals-16.2.0
Bump globals from 15.15.0 to 16.2.0 in /electron
2025-06-15 13:57:35 +02:00
dependabot[bot] 7b8525f795 Bump globals from 15.15.0 to 16.2.0 in /electron
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.2.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.2.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 11:55:43 +00:00
Icedsoda a6c65bab22 Merge pull request #156 from qitechgmbh/dependabot/cargo/crc-3.3.0
Bump crc from 3.2.1 to 3.3.0
2025-06-15 13:52:23 +02:00
dependabot[bot] eeb0733c63 Bump crc from 3.2.1 to 3.3.0
Bumps [crc](https://github.com/mrhooray/crc-rs) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/mrhooray/crc-rs/releases)
- [Commits](https://github.com/mrhooray/crc-rs/commits)

---
updated-dependencies:
- dependency-name: crc
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 11:50:24 +00:00
Icedsoda 62a10ace1c Merge pull request #303 from qitechgmbh/dependabot/npm_and_yarn/electron/vitejs/plugin-react-4.5.1
Bump @vitejs/plugin-react from 4.3.4 to 4.5.1 in /electron
2025-06-15 13:49:17 +02:00
dependabot[bot] fec87f1ba3 Bump @vitejs/plugin-react from 4.3.4 to 4.5.1 in /electron
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.4 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.5.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 11:46:51 +00:00
Icedsoda b1cfe581f6 Merge pull request #341 from qitechgmbh/340-add-el6021-revision-0x100000
Add `EL6021_IDENTITY_D`
2025-06-13 20:05:37 +02:00
Icedsoda 7993481d0f Add EL6021_IDENTITY_D 2025-06-13 20:05:09 +02:00
Icedsoda 7c74cedf32 Merge pull request #335 from qitechgmbh/332-rename-dre-to-laser
Rename `Dre` to `Laser`
2025-06-13 20:04:35 +02:00
Icedsoda 3ec3d0bbc7 Change Laser Icon 2025-06-13 20:03:57 +02:00
Icedsoda 1fca8821b5 Reanem Dre to Laser 2025-06-13 19:45:12 +02:00
Icedsoda 59b0cde17f Merge pull request #339 from qitechgmbh/336-never-dim-the-hmi-panel
Fix NixOS settings with wrong units
2025-06-13 19:44:29 +02:00
Icedsoda b2e1019222 Fix NixOS settings with wrong units 2025-06-13 19:32:20 +02:00
Icedsoda 66be2a143a Merge pull request #338 from qitechgmbh/337-installinfonix-is-empty
Switches to `tee` for safer file redirection in script
2025-06-13 19:23:04 +02:00
Icedsoda 1db27548fb Merge pull request #264 from qitechgmbh/dependabot/npm_and_yarn/electron/react-hook-form-7.57.0
Bump react-hook-form from 7.54.2 to 7.57.0 in /electron
2025-06-13 19:22:13 +02:00
Icedsoda cba84b93f0 Switches to tee for safer file redirection in script
Replaces `cat` with `sudo tee` to avoid potential permission issues and
ensure proper handling of file redirection when generating `installInfo.nix`.
Redirects output to `/dev/null` to suppress unnecessary output.
2025-06-13 19:10:08 +02:00
dependabot[bot] 8dbd6a3a91 Bump react-hook-form from 7.54.2 to 7.57.0 in /electron
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.2 to 7.57.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.2...v7.57.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:13:57 +00:00
Icedsoda 0bab7e53a7 Merge pull request #334 from qitechgmbh/bump-otel-deps
Bump OpenTelemetry Dependencies
2025-06-13 17:43:59 +02:00
Icedsoda fa0611b1bb Bump OpenTelemetry Dependencies 2025-06-13 17:41:44 +02:00
Icedsoda 156e69eb0d Merge pull request #333 from qitechgmbh/294-include-kv-traces-in-journald-log
Remove KV pairs in logs
2025-06-13 17:31:06 +02:00
nixos-install.sh 626115b284 Remove KV pairs in logs 2025-06-13 17:28:14 +02:00
Icedsoda 54c264d03f Merge pull request #331 from qitechgmbh/329-add-markdown-table-of-contents-component
Add Markdown TOC
2025-06-13 16:23:02 +02:00
nixos-install.sh f1399332bb Add Markdown TOC 2025-06-13 16:20:40 +02:00
93094049+kraemr@users.noreply.github.com 943f1081a8 Merge branch 'master' into 175-fine-tune-pressure-pid-controller 2025-06-13 15:09:52 +02:00
93094049+kraemr@users.noreply.github.com db039c7ccc bakcend api 2025-06-13 15:08:08 +02:00
93094049+kraemr@users.noreply.github.com 7f7984ebe4 frontend changes for api 2025-06-13 15:07:46 +02:00
nixos-install.sh 3752a69c78 Revert "Add installInfo.nix file with current commit information"
This reverts commit d0a98557d3.
2025-06-13 12:53:50 +02:00
Anatomik5 42867bf41c Group Excel Button 2025-06-12 17:34:59 +02:00
93094049+kraemr@users.noreply.github.com b3f06b4973 fixed pressure PID 2025-06-12 15:42:21 +02:00
Icedsoda 710c6cb2dd Merge pull request #322 from qitechgmbh/321-git-abbrevation-from-tag
Improve `install.sh` to properly capture tag names into `gitAbbrevation` and not `HEAD`
2025-06-10 21:04:22 +02:00
nixos-install.sh d0a98557d3 Add installInfo.nix file with current commit information 2025-06-10 21:00:14 +02:00
Icedsoda 416475aaea Improve install.sh 2025-06-10 20:54:56 +02:00
Icedsoda b2c7f6e0b1 Release Version 2.0.0
- Omit Winder V2 Manual
2025-06-10 20:28:41 +02:00
7e26bb787d Fix/33 (#317)
* Create `LoopThrottle`

* Use `Loopthrottle` for panic channel loop

* Use `LoopThrottle` to 1ms cycles.

* fixed timeseries in Extruder2

* Make sure that traverse inner and outer limit won't overlap

* Use `jemalloc`

* Add `useThrottle`

* fix Extuder control page

* Big Graph + Excel + length check during modbus responce added

* fix merge error

* Fix time window error

* Extract `GraphConfig`

* enchance graph

* enchance

---------

Co-authored-by: 93094049+kraemr@users.noreply.github.com <93094049+kraemr@users.noreply.github.com>
Co-authored-by: Anatomik5 <darmenovalisher@gmail.com>
Co-authored-by: Darmenov Alisher <115714693+Anatomik5@users.noreply.github.com>
2025-06-10 19:56:20 +02:00
Icedsoda e7d3581e94 Show NixOS generations on "Update" page (#310)
* Show NixOS generations on "Update" page

* use nixos-rebuild commands

* skip first line

* fix set issue

* set & reboot

* improve generation cards

* truncate generation name

* fix date line

* make text syle the same

* fix delete nixos generation

* change text
2025-06-10 17:10:44 +02:00
Icedsoda 7ca76accc4 Merge pull request #302 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-36.4.0
Bump electron from 34.0.2 to 36.4.0 in /electron
2025-06-10 16:37:42 +02:00
dependabot[bot] 2f41582e9d Bump electron from 34.0.2 to 36.4.0 in /electron
Bumps [electron](https://github.com/electron/electron) from 34.0.2 to 36.4.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v34.0.2...v36.4.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 36.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 14:36:04 +00:00
Icedsoda 7b09d1b42f Merge pull request #313 from qitechgmbh/fix/274
fix dre panic
2025-06-10 16:33:01 +02:00
Anatomik5 17056d61e9 fix dre panic 2025-06-10 16:06:22 +02:00
Icedsoda 49b25a017a Merge pull request #267 from qitechgmbh/dependabot/cargo/clap-4.5.39
Bump clap from 4.5.37 to 4.5.39
2025-06-10 12:18:47 +02:00
dependabot[bot] 2b9dc727ba Bump clap from 4.5.37 to 4.5.39
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.37 to 4.5.39.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.37...clap_complete-v4.5.39)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 10:18:25 +00:00
Icedsoda 229ac7cace Merge pull request #300 from qitechgmbh/dependabot/cargo/tower-http-0.6.6
Bump tower-http from 0.6.5 to 0.6.6
2025-06-10 12:17:06 +02:00
dependabot[bot] 73ec7ef2ab Bump tower-http from 0.6.5 to 0.6.6
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.5 to 0.6.6.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.5...tower-http-0.6.6)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 10:16:21 +00:00
Icedsoda 0558b9a125 Merge pull request #309 from qitechgmbh/fix/66-v2
Add troubleshoot page
2025-06-10 12:15:12 +02:00
Icedsoda bd373024f1 Add button to export <Terminal> log lines 2025-06-10 12:12:48 +02:00
Robin KrämerandIcedsoda 22eb39522b 193 use a hashmap instead of vecdequeue for modbus requests (#298)
* mitsubishiinverter now uses hashmap

* new bug no responses ...

* cleaned code up and fixed errors

* cleaned up unused imports etc

* remove `println`

---------

Co-authored-by: Icedsoda <29167146+1cedsoda@users.noreply.github.com>
2025-06-10 11:57:46 +02:00
Icedsoda 93c42078a9 Merge pull request #297 from qitechgmbh/290-add-a-wattage-graph-for-heatingzone
290 add a wattage graph for heatingzone
2025-06-10 11:48:32 +02:00
Icedsoda 29bf15a266 Add troubleshoot page 2025-06-10 11:36:02 +02:00
Icedsoda a3541e7906 log panics of main thread 2025-06-09 17:48:20 +02:00
Icedsoda fcfeb448d6 add test_calculate_traverse_speed 2025-06-09 14:41:45 +02:00
Icedsoda 4d8c15df31 Merge pull request #306 from qitechgmbh/fix/299
Wrap tension arm angle from 270:360 to -90:0
2025-06-09 13:36:56 +02:00
Icedsoda 9ff63b7389 Wrap tension arm angle from 270:360 to -90:0 2025-06-09 13:36:25 +02:00
93094049+kraemr@users.noreply.github.com 1df8504c4e fixed warnings 2025-06-06 17:53:25 +02:00
93094049+kraemr@users.noreply.github.com 886a5a2a2a removed heating bool from frontend and api 2025-06-06 17:51:19 +02:00
93094049+kraemr@users.noreply.github.com 34142b8063 addewd backend logic for wattage graphs 2025-06-06 15:46:32 +02:00
93094049+kraemr@users.noreply.github.com c8eb44b697 added frontend logic for heating wattage timeseries 2025-06-06 15:46:16 +02:00
Icedsoda f0dd0c5a74 Merge pull request #293 from qitechgmbh/fix/292
Increase Mode Buttons Size
2025-06-06 14:34:30 +02:00
Icedsoda a108384305 Increase Mode Buttons Size 2025-06-06 14:34:06 +02:00
Icedsoda 37a82b1bb8 Puller target speed 1 decimal point 2025-06-06 14:28:20 +02:00
93094049+kraemr@users.noreply.github.com df03e4c2a2 added wattage unit and removed flame from HeatingZone 2025-06-06 14:24:50 +02:00
Icedsoda 4b0321975b Merge pull request #291 from qitechgmbh/fix/289
fix brach version cards are always disabled
2025-06-06 14:06:39 +02:00
Icedsoda 43d495db38 fix brach version cards are always disabled 2025-06-06 14:06:12 +02:00
Icedsoda 46aff0e080 Merge pull request #288 from qitechgmbh/improve-interface-discovery-logging
Improve Interface discovery logging
2025-06-06 13:41:36 +02:00
Icedsoda a65ec0b920 Improve Interface discovery logging 2025-06-06 13:41:18 +02:00
Icedsoda a3ef0f5bbe Merge pull request #230 from qitechgmbh/fix/224
Swap Machine and Serial Word Addresses
2025-06-06 13:38:06 +02:00
Icedsoda d91d413aaf swap machine and serial word addresses 2025-06-06 13:36:36 +02:00
Icedsoda 4e7cb845d6 Merge pull request #287 from qitechgmbh/fix/260-traverse-turnaround
In traversing mode turn around based on theshhold not point
2025-06-05 16:57:09 +02:00
Icedsoda 677276a2b7 In traversing mode turn around based on theshhold not point 2025-06-05 16:55:35 +02:00
Icedsoda a846928ff5 Merge pull request #286 from qitechgmbh/fix/249-redo-homing
Redo traverse homing routine if it failed
2025-06-05 16:38:28 +02:00
Icedsoda f760d3567a Redo traverse homing routine if it failed 2025-06-05 16:35:14 +02:00
Icedsoda e2ebaf69a0 remove log 2025-06-05 16:19:07 +02:00
Icedsoda 0b160581b9 Merge pull request #280 from qitechgmbh/251-add-a-configurable-limit-for-pressure
251 add a configurable limit for pressure
2025-06-05 16:13:09 +02:00
Icedsoda 1a7fe8b93d Merge pull request #281 from qitechgmbh/217-add-hard-limit-of-300c-for-heating
added a hardcoded limit for extruder temperature_controllers
2025-06-05 16:12:28 +02:00
Icedsoda b745524257 Merge pull request #285 from qitechgmbh/fix/276-traverse-too-fast
Fix Traverse Speed Formula
2025-06-05 16:11:44 +02:00
Icedsoda 14cd458d60 Fix traverse speed formula 2025-06-05 16:09:25 +02:00
Icedsoda 224a3cb66f Merge pull request #284 from qitechgmbh/fix/275-part-2
Improve Spool Acceleration Profile v2
2025-06-05 16:09:04 +02:00
Icedsoda 25be1678dc Remove most of Spool API 2025-06-05 16:00:20 +02:00
Icedsoda 7a05ce93b5 Improve spool speed syncing used puller speed 2025-06-05 16:00:20 +02:00
Icedsoda d556f0225b Merge pull request #283 from qitechgmbh/fix/275-spool-accel
Improve Spool Acceleration Profile
2025-06-04 22:07:54 +02:00
Icedsoda 4845036256 Use FilamentTensionCalculator in SpoolSpeedController 2025-06-04 21:51:38 +02:00
Icedsoda 0cf761f529 Add FilamentTensionCalculator 2025-06-04 21:04:07 +02:00
Icedsoda 3b414192e9 Add TimeagnosticPidController 2025-06-04 21:04:07 +02:00
93094049+kraemr@users.noreply.github.com 736f75f413 added a hardcoded limit for extruder temperature_controllers 2025-06-03 17:25:25 +02:00
93094049+kraemr@users.noreply.github.com db5046f7e1 fixed minor issues in backend and frontend 2025-06-03 16:56:41 +02:00
93094049+kraemr@users.noreply.github.com eb6758bf3b Merge branch 'master' into 251-add-a-configurable-limit-for-pressure 2025-06-03 15:52:14 +02:00
Robin Krämer 7a77d3ba73 Merge pull request #244 from qitechgmbh/241-pressure-readout-not-showing-up-on-frontend
pressure readout is now accurate and shows up in frontend, also added min_raw and max_raw to AnalogInputRange to define what raw value corresponds to what physical unit.
2025-06-03 15:48:24 +02:00
93094049+kraemr@users.noreply.github.com eba54a4acb fixed comments 2025-06-03 15:45:33 +02:00
93094049+kraemr@users.noreply.github.com 1049dd22b3 added logic for motor shutoff at specified pressure 2025-06-03 15:30:52 +02:00
93094049+kraemr@users.noreply.github.com cee244da23 frontend is now interactable 2025-06-03 15:30:21 +02:00
93094049+kraemr@users.noreply.github.com fcf6000e8d added api logic front and backend 2025-06-03 15:29:47 +02:00
Icedsoda 74ce4ad340 Merge pull request #273 from qitechgmbh/fix/285
Fix initial acceleration on `SpoolSpeedController`
2025-06-02 20:07:32 +02:00
Icedsoda 73c5de46a5 Merge pull request #272 from qitechgmbh/fix/270-panic
Improve panic logging
2025-06-02 20:06:46 +02:00
Icedsoda 36bbd92ee1 Improve panic logging 2025-06-02 20:04:22 +02:00
Icedsoda 30cab528f9 Fix initial acceleration on SpoolSpeedController 2025-06-02 18:20:06 +02:00
Icedsoda 3dfe362983 Merge pull request #271 from qitechgmbh/fix/256
Fix terminal colors
2025-06-02 18:14:32 +02:00
Icedsoda a711542863 Fix ternimal colors 2025-06-02 18:09:01 +02:00
Icedsoda bc00962d8e Merge pull request #266 from qitechgmbh/dependabot/cargo/tower-http-0.6.5
Bump tower-http from 0.6.2 to 0.6.5
2025-06-02 09:47:45 +02:00
dependabot[bot] 0776506e30 Bump tower-http from 0.6.2 to 0.6.5
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.2 to 0.6.5.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.2...tower-http-0.6.5)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 07:45:36 +00:00
Icedsoda c8fdfc79ee Merge pull request #181 from qitechgmbh/dependabot/cargo/socketioxide-0.17.1
Bump socketioxide from 0.17.0 to 0.17.1
2025-06-02 09:44:21 +02:00
dependabot[bot] 51b797f9dd Bump socketioxide from 0.17.0 to 0.17.1
Bumps [socketioxide](https://github.com/totodore/socketioxide) from 0.17.0 to 0.17.1.
- [Release notes](https://github.com/totodore/socketioxide/releases)
- [Changelog](https://github.com/Totodore/socketioxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/totodore/socketioxide/commits)

---
updated-dependencies:
- dependency-name: socketioxide
  dependency-version: 0.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 07:40:27 +00:00
Icedsoda 6f50515144 Merge pull request #268 from qitechgmbh/log-memory
Log memory usage
2025-06-02 09:39:17 +02:00
Icedsoda 1911604fc3 Log memory usage 2025-06-02 09:37:01 +02:00
Icedsoda f507f27d13 Merge pull request #263 from qitechgmbh/improve-logging
Use `tracing` instead of `log` & add Opentelemetry
2025-06-01 21:43:52 +02:00
Icedsoda 68260bc379 Add Opentelemetry logging 2025-06-01 21:39:51 +02:00
Icedsoda b63adbb601 use tracing crate instead of log
remove module path from logs

setup tracing subscriber
2025-06-01 18:17:29 +02:00
Icedsoda 754533834b Merge pull request #262 from qitechgmbh/fix/261
Continuously change the value when pressing down a [-] [+] button
2025-06-01 13:21:25 +02:00
Icedsoda 0d8fb2789f Continuously change the value when pressing down a [-] [+] button 2025-06-01 13:19:50 +02:00
Icedsoda 680ecd9483 Improve TouchNumpad UI 2025-05-31 17:13:38 +02:00
Icedsoda 92414e42b1 Merge pull request #200 from qitechgmbh/fix/198
Improve `EditValue`
2025-05-31 15:58:25 +02:00
Icedsoda 74fbb8ee4b Improve EditValue UI 2025-05-31 15:56:08 +02:00
Icedsoda 0361e04e2e Add Numpad 2025-05-31 14:58:28 +02:00
Icedsoda 2587ebb63c fix Extuder control page 2025-05-30 18:26:45 +02:00
93094049+kraemr@users.noreply.github.com 8f3894f68e now tests should pass 2025-05-29 22:12:31 +02:00
93094049+kraemr@users.noreply.github.com cd9b034ef2 now tests should pass again 2025-05-29 21:53:07 +02:00
93094049+kraemr@users.noreply.github.com 5d79eaff5b tests should all compile now 2025-05-29 21:49:44 +02:00
Icedsoda e77827ef6a Merge pull request #254 from qitechgmbh/fix/253
Add `useThrottle`
2025-05-29 20:27:23 +02:00
Icedsoda d00a195599 Add useThrottle 2025-05-29 20:25:34 +02:00
Icedsoda 2b6c7be71d Merge pull request #248 from qitechgmbh/fix/247-jemalloc
Use `jemalloc`
2025-05-29 18:19:37 +02:00
93094049+kraemr@users.noreply.github.com 2c8f1e6989 refactored get_pressure 2025-05-29 18:12:57 +02:00
93094049+kraemr@users.noreply.github.com 17934bdf08 added min_raw max_raw to AnalogInputRange 2025-05-29 17:58:35 +02:00
Icedsoda 06f7759321 Use jemalloc 2025-05-29 17:36:23 +02:00
Icedsoda 25075c2cfd Merge pull request #237 from qitechgmbh/fix/234
Make sure that traverse inner and outer limit won't overlap
2025-05-29 16:13:03 +02:00
93094049+kraemr@users.noreply.github.com 0438a6e641 Merge branch 'master' into 241-pressure-readout-not-showing-up-on-frontend 2025-05-29 15:32:36 +02:00
Robin Krämer e92a0421f4 Merge pull request #246 from qitechgmbh/245-extruder-page-fails-to-parse-events-from-api-because-of-timeseries
fixed timeseries and by extension graphs in Extruder2ControlPage
2025-05-29 15:31:48 +02:00
Icedsoda 2a09a74703 Merge pull request #243 from qitechgmbh/fix/242
Throttle Loops agains high CPU usage
2025-05-29 15:29:02 +02:00
93094049+kraemr@users.noreply.github.com 7023fe76fc fixed timeseries in Extruder2 2025-05-29 15:28:23 +02:00
Icedsoda 0f0e3258ce Merge pull request #236 from qitechgmbh/fix/187
Use nixpkgs `25.05`
2025-05-29 15:28:22 +02:00
Icedsoda 374a93b0f2 Use LoopThrottle to 1ms cycles. 2025-05-29 14:02:24 +02:00
93094049+kraemr@users.noreply.github.com 0019db3394 pressure readout is now accurate and shows up in frontend 2025-05-29 14:02:13 +02:00
Icedsoda b58f300171 Use Loopthrottle for panic channel loop 2025-05-29 13:46:48 +02:00
Icedsoda 75d01146ac Create LoopThrottle 2025-05-29 13:46:16 +02:00
Icedsoda d231e30f20 Merge pull request #240 from qitechgmbh/fix/239
Improve `<TimeSeriesValue/>`
2025-05-28 23:11:50 +02:00
Icedsoda 484be08a60 Improve <TimeSeriesValue/> 2025-05-28 23:10:00 +02:00
Icedsoda 326653dc71 Merge pull request #208 from qitechgmbh/fix/32
Add Mini Graph
2025-05-28 20:32:04 +02:00
Icedsoda 82c005f2c3 Mini Graph implementation with history with uplot+ enchance of timeseries 2025-05-28 20:24:43 +02:00
Icedsoda c88a44aef7 Merge pull request #238 from qitechgmbh/fix/232
Keep track of disconnections
2025-05-28 20:05:27 +02:00
Icedsoda 400bfe6dfd Keep track of diconnections 2025-05-28 17:59:10 +02:00
Icedsoda 9d11d9914d Make sure that traverse inner and outer limit won't overlap 2025-05-28 17:08:05 +02:00
Icedsoda ed68f7a5eb Use nigpkgs 25.05 2025-05-28 16:46:03 +02:00
Icedsoda 0090d3decf fix electron lints 2025-05-28 16:21:30 +02:00
Icedsoda 019f7ddacd Merge pull request #228 from qitechgmbh/fix/97
Add Electron Pipeline
2025-05-28 16:15:09 +02:00
Icedsoda 36c662a201 Merge pull request #235 from qitechgmbh/fix/231
Add Mock Machine 0x7
2025-05-28 16:14:56 +02:00
Icedsoda e7617d2ac2 Add Mock Machine 0x7 2025-05-28 16:11:38 +02:00
Icedsoda 9b721c19ba fix lints 2025-05-28 13:14:12 +02:00
Icedsoda 755b6fd69a Specify Nodejs 22 2025-05-28 13:11:59 +02:00
Icedsoda 317fdce027 fix linted errors 2025-05-28 13:11:59 +02:00
Icedsoda 90d81ee7df Add Electron Pipeline 2025-05-28 13:11:59 +02:00
Icedsoda 2241c7f996 Merge pull request #229 from qitechgmbh/fix/227
Add Event Queue with Retry Logic
2025-05-27 21:25:45 +02:00
Icedsoda 170c46563b disconnect properly 2025-05-27 21:17:12 +02:00
Icedsoda 6d67ff9843 Reemit State events first 2025-05-27 21:17:02 +02:00
Icedsoda d5b2b1e76e Improve SocketQueue 2025-05-27 21:16:48 +02:00
Icedsoda a3cb746d15 Move SocketQueue to seperate module 2025-05-27 21:09:12 +02:00
Icedsoda 4cb6d684f8 Increase native Socket.IO event buffer 2025-05-27 21:08:54 +02:00
Icedsoda bcd131238a Improve pushing many events onto SocketQueue 2025-05-27 20:04:14 +02:00
Icedsoda 65d49b3d81 Add event queue with retry logic 2025-05-27 19:54:07 +02:00
Icedsoda f2550897a1 Merge pull request #226 from qitechgmbh/fix/225
Disable Laserpointer Buttons if `traverseState` is Not Available
2025-05-27 17:34:46 +02:00
Icedsoda e9de191593 Disable Laserpointer buttons if traverseState is not available 2025-05-27 17:33:16 +02:00
Icedsoda e3047b3141 Disable DigitalOutputSetter by default 2025-05-27 17:32:46 +02:00
Icedsoda 38b220e4c9 remove some warnings 2025-05-27 16:28:05 +02:00
Icedsoda a64a2b812c Merge pull request #223 from qitechgmbh/fix/222
Only Process IO of `EthercatDevice`s If They are Used by a Machine
2025-05-27 16:25:11 +02:00
Icedsoda c11a70a0b7 Only process io of ethercat devices if they are used by a machine 2025-05-27 16:22:18 +02:00
Icedsoda 215857b372 Merge pull request #221 from qitechgmbh/fix/218
Reconnect Socket.IO after server restart
2025-05-27 13:57:03 +02:00
Icedsoda 89e028d3c4 Add client reconnection logic 2025-05-27 13:52:54 +02:00
Robin Krämer b81b9ce6c0 Merge pull request #220 from qitechgmbh/191-gearbox-is-inverted
fix/191
2025-05-26 19:40:42 +02:00
93094049+kraemr@users.noreply.github.com 4fa0d389b6 when rotation is set to forward the inverter now rotates clockwise 2025-05-26 19:29:46 +02:00
Robin Krämer 1090ce7f97 Merge pull request #205 from qitechgmbh/extruder_v2
Extruder v2
2025-05-26 18:36:08 +02:00
93094049+kraemr@users.noreply.github.com c8cf92bc73 removed i from temp pid and fixed target rpm bug in frontend 2025-05-26 18:26:16 +02:00
93094049+kraemr@users.noreply.github.com 2acd0f73d9 fixed oversight in Controlpage 2025-05-26 17:32:00 +02:00
93094049+kraemr@users.noreply.github.com 36462865f6 fixed naming now 2025-05-26 17:30:38 +02:00
93094049+kraemr@users.noreply.github.com ffeb2b6494 added transmission_converter 2025-05-26 17:15:37 +02:00
Icedsoda 339e461d2f Merge pull request #216 from qitechgmbh/fix/137
Exit program if loop fails
2025-05-26 17:11:57 +02:00
Icedsoda 4748b4c332 Exit program if loop fails 2025-05-26 17:09:58 +02:00
93094049+kraemr@users.noreply.github.com 125efe0e63 hopefully fixed naming for useExtruder2 2025-05-26 16:54:52 +02:00
93094049+kraemr@users.noreply.github.com 7aa3222efd fixed compile error 2025-05-26 16:38:56 +02:00
93094049+kraemr@users.noreply.github.com ee305b4254 fixed set_target_screw_rpm 2025-05-26 16:36:54 +02:00
93094049+kraemr@users.noreply.github.com 007dab9ed9 fixed compile error 2025-05-26 16:35:24 +02:00
93094049+kraemr@users.noreply.github.com 18adf63937 non static pwm_period 2025-05-26 16:33:13 +02:00
93094049+kraemr@users.noreply.github.com 2e538fdad5 pid factors are now inside the controller for screw_speed 2025-05-26 16:27:37 +02:00
93094049+kraemr@users.noreply.github.com 58c1880488 removed useles change 2025-05-26 16:25:26 +02:00
93094049+kraemr@users.noreply.github.com 7cca56e20a removed empty line from HeatingZone 2025-05-26 16:24:34 +02:00
93094049+kraemr@users.noreply.github.com 50ac55c2c1 removed rpm.rs 2025-05-26 16:22:20 +02:00
93094049+kraemr@users.noreply.github.com 926c8f7f4d use Frequency::Zero and fixed cargo.toml 2025-05-26 16:21:26 +02:00
93094049+kraemr@users.noreply.github.com ac037bf977 removed unused imports and warnings 2025-05-26 15:37:33 +02:00
93094049+kraemr@users.noreply.github.com 0c3d26a08b switched almost everything to uom ... 2025-05-26 15:27:08 +02:00
93094049+kraemr@users.noreply.github.com 70e96a0fa2 changed Flame to lu:flame 2025-05-26 13:54:44 +02:00
93094049+kraemr@users.noreply.github.com 22ec270561 renamed mutations to match winder 2025-05-26 13:23:31 +02:00
93094049+kraemr@users.noreply.github.com e8d9c231dc renamed RpmStateEvent to ScrewStateEvent 2025-05-26 13:13:15 +02:00
93094049+kraemr@users.noreply.github.com 45c708909b renamed get_rpm to get_screw_rpm 2025-05-26 13:11:54 +02:00
93094049+kraemr@users.noreply.github.com 3e963d8c22 renamed set_target_rpm to set_screw_speed 2025-05-26 13:11:12 +02:00
93094049+kraemr@users.noreply.github.com 45038fc9c3 added transmission into calculation of Target RPM 2025-05-26 13:06:20 +02:00
93094049+kraemr@users.noreply.github.com 4ad8abb88f replaced member acces to temperature with getters 2025-05-26 12:51:23 +02:00
93094049+kraemr@users.noreply.github.com 13610f6612 forward instead of forwards 2025-05-26 12:47:40 +02:00
93094049+kraemr@users.noreply.github.com 173861bbd5 fixed errors, refactored and simplified temperature control 2025-05-26 12:42:55 +02:00
93094049+kraemr@users.noreply.github.com 4b952f55ab removed unused Mutations 2025-05-26 12:12:22 +02:00
93094049+kraemr@users.noreply.github.com 00af80f531 rewrote act and extruder, added ScrewSpeedController 2025-05-26 12:07:17 +02:00
93094049+kraemr@users.noreply.github.com 7caad4bb32 only one update needed for temperature control now 2025-05-26 11:22:57 +02:00
93094049+kraemr@users.noreply.github.com a0667f6bc7 extruder now uses the correct id 2025-05-26 11:19:46 +02:00
93094049+kraemr@users.noreply.github.com 1d28abe664 removed empty lines 2025-05-26 11:13:57 +02:00
93094049+kraemr@users.noreply.github.com 029edb2e54 removed empty line 2025-05-26 11:11:55 +02:00
93094049+kraemr@users.noreply.github.com 27959f0411 fixed el1002 description 2025-05-26 11:10:29 +02:00
93094049+kraemr@users.noreply.github.com cd52a99fe5 renamed role_labels in frontend 2025-05-26 11:07:50 +02:00
93094049+kraemr@users.noreply.github.com 691f03e905 removed no Modbus response error log 2025-05-26 11:02:01 +02:00
93094049+kraemr@users.noreply.github.com ae93075dc8 removed current_rpm from inverter 2025-05-26 11:00:41 +02:00
93094049+kraemr@users.noreply.github.com e6b99dbb51 refactored MitsubishiInverterRS485Actor into different sections 2025-05-26 10:57:08 +02:00
93094049+kraemr@users.noreply.github.com 3539193f46 removed unused file 2025-05-26 10:53:25 +02:00
93094049+kraemr@users.noreply.github.com 9a140d803c removed empty line 2025-05-26 10:51:44 +02:00
93094049+kraemr@users.noreply.github.com 3afa2d0cb6 Merge branch 'extruder_v2' of github.com:qitechgmbh/control into extruder_v2 2025-05-26 10:48:23 +02:00
Robin KrämerandCopilot 757c529b18 Update electron/src/machines/extruder/HeatingZone.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-26 10:45:33 +02:00
Robin KrämerandCopilot e3e3839dbc Update electron/src/setup/DeviceEepromDialog.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-26 10:43:50 +02:00
Robin KrämerandCopilot f8fdcbcf39 Update electron/src/setup/DeviceEepromDialog.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-26 10:43:38 +02:00
Icedsoda 0d3a9be546 Merge pull request #212 from qitechgmbh/fix/211
fix `test_steps_to_velocity_conversion` and `test_velocity_to_steps_c…
2025-05-26 01:05:36 +02:00
Icedsoda 7b5251e80f fix test_steps_to_velocity_conversion and test_velocity_to_steps_conversion 2025-05-26 01:04:21 +02:00
Icedsoda 70b4a417ff Merge pull request #209 from qitechgmbh/fix/203
Improve acceleration of winder steppers
2025-05-26 00:49:28 +02:00
Icedsoda 5dada9a06c Ignore doc tests 2025-05-26 00:46:48 +02:00
Icedsoda 95758fe71d Improve `AccelerationSpeedControllerz and its wrappers 2025-05-26 00:43:57 +02:00
Icedsoda 21fdc4dbab Use LinearJerkSpeedController in SpoolSpeedController 2025-05-25 23:46:37 +02:00
Icedsoda c80100c38b Increase acceleration on SpoolSpeedController 2025-05-25 22:47:22 +02:00
Icedsoda b358fe6ab1 Use second degree motion controller with puller 2025-05-25 17:12:49 +02:00
Icedsoda 80c7b3d0e7 Add second degree motion controllers 2025-05-25 17:12:49 +02:00
Icedsoda 9455d7fc47 Merge pull request #207 from qitechgmbh/fix/197
Fix #197 and #202
2025-05-23 17:57:20 +02:00
Icedsoda 27f7d8568f Add capability checks for traverse and winding operations
Introduces detailed capability checks for traverse operations
(inner/outer limit, home) and winding mode to ensure safer
and more precise control. Updates frontend and backend logic
to utilize these checks, improving synchronization and user
feedback. Also emits mode state updates when traverse state
or tension arm state changes to maintain frontend consistency.
2025-05-23 17:56:20 +02:00
Icedsoda 08b149a4d1 disable winding if not homed 2025-05-23 17:21:04 +02:00
Icedsoda 39148d6b18 Merge pull request #206 from qitechgmbh/fix/204
Add button to reverse puller direction
2025-05-23 17:16:28 +02:00
Icedsoda aed1960ce4 Add button to reverse puller direction 2025-05-23 17:15:34 +02:00
93094049+kraemr@users.noreply.github.com 07d765da48 cleanup 2025-05-22 18:37:40 +02:00
93094049+kraemr@users.noreply.github.com 24e33e504b fixing errors caused by merge 2025-05-22 18:29:53 +02:00
93094049+kraemr@users.noreply.github.com 93a8215a22 Merge remote-tracking branch 'origin/master' into extruder_v2 2025-05-22 18:21:23 +02:00
93094049+kraemr@users.noreply.github.com b7ea958034 removed dead code 2025-05-22 18:16:10 +02:00
93094049+kraemr@users.noreply.github.com 95e16d13e0 refactor 2025-05-22 18:15:20 +02:00
93094049+kraemr@users.noreply.github.com 7f958210fd fixed Reset Inverter request 2025-05-22 16:28:53 +02:00
Icedsoda 998d0010af Merge pull request #201 from qitechgmbh/fix/199
Reverse `<Spool/>` animation direction
2025-05-21 19:03:56 +02:00
Icedsoda 7a29b74d05 Reverse <Spool/> animation direction 2025-05-21 19:03:31 +02:00
Icedsoda 1020bd2438 Merge pull request #185 from qitechgmbh/fix/29
Add Traverse Functionality
2025-05-21 18:04:06 +02:00
Icedsoda 6de0ba238d Fix ControlGrid columns property 2025-05-21 18:03:07 +02:00
Icedsoda 28d871f45e Remove "Homed: badge when homed 2025-05-21 17:59:22 +02:00
Icedsoda 675aa0daf6 Emit TraverseStateEvent more often 2025-05-21 17:57:10 +02:00
Icedsoda ce98e1b71b Add traversing functionality 2025-05-21 17:36:57 +02:00
Icedsoda 7887860a91 Improve test_round_propabilistic 2025-05-21 08:46:24 +02:00
Icedsoda 67ec75a09a Set Traverse Limit to 180mm 2025-05-20 22:46:39 +02:00
Icedsoda eba6656cf2 Improve traverse speed when GoingIn and GoingOut 2025-05-20 22:46:09 +02:00
Icedsoda fbe0d46b82 Merge branch 'master' into fix/29 2025-05-20 22:32:06 +02:00
Icedsoda 6cc70b97ba Fix TraverseBar visualization 2025-05-20 22:27:31 +02:00
Icedsoda b527bbe11e Fix position conversion from stepper terminal (consider microstepping) 2025-05-20 21:48:45 +02:00
Icedsoda eee13c0dcb Merge pull request #189 from qitechgmbh/fix/188
DRE Frontend
2025-05-20 21:39:17 +02:00
Darmenov Alisher e141111ad4 Merge branch 'master' into fix/188 2025-05-20 21:37:15 +02:00
Anatomik5 83f247f8e9 Solved async problem in dre act and enchanced useDre with useStateOptimistic 2025-05-20 21:35:55 +02:00
Icedsoda 8d46ee0af8 Fix CounterWrapperU16U128 2025-05-20 20:35:47 +02:00
Icedsoda a6b41bf530 IMprove traverse homing routine 2025-05-20 18:53:29 +02:00
Icedsoda a73eec2d5a Able to home traverse (fix #29) 2025-05-20 15:07:32 +02:00
Icedsoda aafb37324d Fix EL70x1VelocityConverter 2025-05-20 14:41:59 +02:00
Icedsoda cc78e84615 Add docs to AngularStepConverter and LinearStepConverter 2025-05-20 13:24:21 +02:00
Icedsoda c8968c1d3d Add DigitalInputGetter 2025-05-20 13:22:54 +02:00
Icedsoda 0062f420a4 Add propabilistic rounding to EL70x1VelocityConverter 2025-05-20 13:22:19 +02:00
Icedsoda ceb8c3ed16 Merge pull request #190 from qitechgmbh/134-nixos-installsh-not-working-on-fresh-nixos-electron-build-hangs
fix for electron build hanging
2025-05-20 12:32:17 +02:00
93094049+kraemr@users.noreply.github.com f21858d54f fix for electron build hanging 2025-05-20 10:33:42 +02:00
Anatomik5 cde791e01d dre frontend + backend enchance 2025-05-19 20:53:46 +02:00
Icedsoda 9919439bf6 Rename step_converter.rs to angular_step_converter.rs 2025-05-19 17:24:14 +02:00
Icedsoda 033986af9e Move EL70x1VelocityCalculator to ethercat-hal 2025-05-19 17:22:48 +02:00
Icedsoda 02a7d30e35 Change StepperVelocityEL70x1 interface 2025-05-19 17:20:42 +02:00
Icedsoda 9ebc0f352c reforctor helper location in ethercat-hal 2025-05-19 17:12:51 +02:00
Icedsoda 8e23884679 Add Trait EthercatDeviceProcessing 2025-05-19 15:38:11 +02:00
Anatomik5 c88b44f712 fix compile error 2025-05-19 14:12:53 +02:00
Darmenov Alisher f1dbe23131 Merge pull request #110 from qitechgmbh/dre-usb-driver
Dre usb driver
2025-05-19 14:04:08 +02:00
Darmenov Alisher d2a80b07c2 Merge branch 'master' into dre-usb-driver 2025-05-19 14:03:52 +02:00
Icedsoda 74b98195da Implement Traverse API 2025-05-19 13:49:46 +02:00
Icedsoda 551161c690 Rename some occurences from Winder1 to Winder2 2025-05-19 12:45:34 +02:00
Icedsoda 34a914dd50 Add LinearStepConverter 2025-05-19 12:38:51 +02:00
Icedsoda ad1657b283 Merge pull request #184 from qitechgmbh/fix/183
Reverse Puller Direction
2025-05-19 11:14:16 +02:00
Icedsoda fe37ea63a0 Reverse Puller Direction 2025-05-19 11:13:47 +02:00
Anatomik5 5dbaa01b7a dre enchance 2025-05-18 16:59:57 +02:00
Anatomik5 f259b8cb06 Added dre machine configuration (target diameter, lower tolerance, higher tolerance) and added mutation 2025-05-18 16:59:57 +02:00
Anatomik5 19ccb67a27 Enhance DreMachine functionality, create act() fucntion for DreMachine 2025-05-18 16:59:57 +02:00
Anatomik5 48a1adb672 Refactor Modbus functions for improved CRC validation 2025-05-18 16:59:57 +02:00
Anatomik5 bc9f795129 Implement hashing functions and integrate them into the Dre device identification process 2025-05-18 16:59:57 +02:00
Icedsoda f0b18f4b15 Merge pull request #178 from qitechgmbh/fix/177
Implement Puller with RPM Control
2025-05-16 12:10:03 +02:00
Icedsoda 15445870e3 fix spool_set_speed_max and spool_set_speed_min 2025-05-16 12:09:51 +02:00
Icedsoda 8d47ec48ad Implement Puller with RPM Control 2025-05-16 11:51:11 +02:00
Icedsoda 3cc6e3b465 Merge pull request #173 from qitechgmbh/fix/172
Use Exponential Interpolation in `SpoolSpeedController`
2025-05-15 22:17:50 +02:00
Icedsoda 7e7c70276f Use Exponential Interpolation in SpoolSpeedController 2025-05-15 22:06:07 +02:00
Icedsoda ed9d9d1425 Merge pull request #171 from qitechgmbh/fix/71
Fix Spool Speed
2025-05-15 22:03:26 +02:00
Icedsoda 7caa0eb117 Fix Spool Speed 2025-05-15 20:44:15 +02:00
Icedsoda cbe7421f9f Merge pull request #170 from qitechgmbh/fix/63
Add `LinearAngleAccelerationController`
2025-05-15 20:19:49 +02:00
Icedsoda 056ad8e6fb SpoolSpeedController now uses LinearAngularAccelerationController istead of PID controller for acceleration 2025-05-15 20:18:50 +02:00
Icedsoda 03a258f583 Add LinearAccelerationController 2025-05-15 15:17:01 +02:00
Icedsoda af721b867c Merge pull request #168 from qitechgmbh/fix/167
Subsitute `El3001` with `EL7031-0030`
2025-05-15 14:47:31 +02:00
Icedsoda 9957d512a5 Remove EL3001 from Winder2 2025-05-15 14:46:02 +02:00
Icedsoda 0af5d9ef05 Fix EL70x1 bugs & add role 4 & 5 to Winder2 2025-05-15 13:58:49 +02:00
Icedsoda 545888ecd6 Merge pull request #166 from qitechgmbh/fix/130-2
Move a `send_panic out of a loop to reduce memory
2025-05-14 13:56:47 +02:00
Icedsoda 52a9e7c1d7 fix dre-usb-driver branch 2025-05-13 23:46:51 +02:00
Darmenov Alisher a936b0e139 Create README.md
Update README.md

Add initial diagram for serial detection process and create serial documentation

Update serial.md

Update serial.md

Update serial.md

Update serial.md

Update serial.md

Update serial.md

Add serial device detection diagram

Add serial device to machine diagram

png of serial detection loop
2025-05-13 23:27:45 +02:00
Anatomik5 8d156f0af9 1
Add dre-usb-driver

Created Modul for separate usb detection and validation without tokio

TODO: rmodbus is working directly with frames, no higher level abstraction -> IDEA to write own abstraction to avoid tokio complexity and size

Modbus communication established

abstract begin

Abstraction of serial device detection

Extended the modbus for serialports demand

added dre

working dre detection with update

control-core - serial registry update

Working DRE

test serial events

add <Markdown> component

add fullscreen button

add <Markdown> component

add fullscreen button

1

Add dre-usb-driver

abstract begin

s

d

solved some errors

add <Markdown> component

add fullscreen button

add fullscreen button

add <Markdown> component

add fullscreen button

1

Add dre-usb-driver

abstract begin

Working DRE

add <Markdown> component

Add dre-usb-driver

d

solved some errors

up

c

123

Update rust.yml

dre dev

dre implemented
2025-05-13 23:27:42 +02:00
Darmenov Alisher a2384d61e0 stash 2025-05-10 18:09:02 +02:00
578 changed files with 116077 additions and 11853 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
+29
View File
@@ -0,0 +1,29 @@
This is a machine control software project.
## Repository structure
### Frontend
`/electron`: Frontend code for the control software built with React and Electron.
### Backend
`/server`: Implements machine setup & loop. Exposes a REST & Socket.io API for the electron frontend.
`/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
`/ethercat-hal-derive`: Macros for ethercat-hal
`/control-core`: Generic logic for the server.
### HMI Panel Operating System
`/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
### Other
/docs: Documentation for the project.
## Key Guidelines
1. Follow Rust & Typescript best practices and idiomatic patterns
2. Maintain existing code structure and organization
3. Write unit tests for new functionality where applicable.
4. Document public APIs and complex logic in code. Suggest changes to the `docs/` folder when appropriate
+14 -11
View File
@@ -1,16 +1,19 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/electron" # Location of package manifests
- package-ecosystem: "npm"
directory: "/electron"
schedule:
interval: "weekly"
interval: "monthly"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-minor"
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-minor"
+52
View File
@@ -0,0 +1,52 @@
name: Electron
on:
push:
branches:
- "**"
pull_request:
branches:
- "*"
jobs:
build-electron:
runs-on: ubuntu-latest
defaults:
run:
working-directory: electron
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18" # or '20' for latest LTS
- uses: actions/checkout@v4
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules/
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install
run: npm i
- name: Build
run: tsc
- name: Format
run: npm run format
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Package
run: npm run package
+9
View File
@@ -16,9 +16,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install libudev development package
run: sudo apt-get update && sudo apt-get install -y libudev-dev
- uses: actions/checkout@v4
- name: Set Rust version
run: rustup override set ${{ env.RUST_VERSION }}
- name: Install Formatter
run: rustup component add rustfmt
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
@@ -33,7 +37,12 @@ jobs:
${{ runner.os }}-${{ env.RUST_VERSION }}-cargo-
- name: Build
env:
RUSTFLAGS: "-D warnings"
run: cargo build --verbose
- name: Format
run: cargo fmt --check --verbose
- name: Run tests
run: cargo test --verbose
+3 -1
View File
@@ -2,4 +2,6 @@ target/
dist/
.DS_Store
Cargo.lock
dhat-heap.json
dhat-heap.json
build.sh
+49
View File
@@ -0,0 +1,49 @@
# Canonical identities on the left, aliases on the right.
# Format options:
# Canon Name <canon@example.com> <alias@example.com>
# Canon Name <canon@example.com> Alias Name <alias@example.com>
# Icedsoda -> Philipp
Philipp <29167146+1cedsoda@users.noreply.github.com> <29167146+1cedsoda@users.noreply.github.com>
# kraemr / Robin Krämer -> Robin
Robin <93094049+kraemr@users.noreply.github.com> <93094049+kraemr@users.noreply.github.com>
Robin <93094049+kraemr@users.noreply.github.com> Robin Krämer <93094049+kraemr@users.noreply.github.com>
# Oshgnacknak -> Josh
Josh <osh@oshgnacknak.de> <osh@oshgnacknak.de>
# Milan
Milan <61654753+MilanVDB@users.noreply.github.com>
# dependabot stays as bot
Dependabot <49699333+dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
# Medformatik / Florian -> Florian
Florian <dev@fwcr.de> <dev@fwcr.de>
Florian <dev@fwcr.de> Medformatik <dev@fwcr.de>
# Anatomik5 -> Alisher (merge noreply + gmail under Alisher)
Alisher <115714693+Anatomik5@users.noreply.github.com> <115714693+Anatomik5@users.noreply.github.com>
Alisher <115714693+Anatomik5@users.noreply.github.com> Anatomik5 <darmenovalisher@gmail.com>
Alisher <115714693+Anatomik5@users.noreply.github.com> Darmenov Alisher <115714693+Anatomik5@users.noreply.github.com>
# snacj -> Melvin
Melvin <0xsnacj@proton.me> <0xsnacj@proton.me>
Melvin <0xsnacj@proton.me> snacj <bbmelvin@gmx.de>
Melvin <0xsnacj@proton.me> Melvin Spieß <0xsnacj@proton.me>
# Janhegerq -> Jan
Jan <219994445+Janhegerq@users.noreply.github.com> <219994445+Janhegerq@users.noreply.github.com>
Jan <219994445+Janhegerq@users.noreply.github.com> <19994445+Janhegerq@users.noreply.github.com>
# Christian / TheBest6337 -> christian
Christian <christian.zschoegner@outlook.de> <christian.zschoegner@outlook.de>
Christian <christian.zschoegner@outlook.de> TheBest6337 <christian.zschoegner@outlook.de>
Christian <christian.zschoegner@outlook.de> Christian <80114847+TheBest6337@users.noreply.github.com>
# nixos-install.sh
nixos-install.sh <nixos-install.sh@localhost> <nixos-install.sh@localhost>
# copilot-swe-agent bot
Copilot <198982749+Copilot@users.noreply.github.com> <198982749+Copilot@users.noreply.github.com>
+3 -2
View File
@@ -1,5 +1,6 @@
{
"typescript.inlayHints.enumMemberValues.enabled": false,
"git.detectSubmodules": true,
"git.detectSubmodulesLimit": 10,
"git.detectSubmodules": true,
"git.detectSubmodulesLimit": 10,
"rust-analyzer.check.command": "clippy",
}
+229 -1
View File
@@ -1 +1,229 @@
# Unreleased
# `2.11.0`
_21.10.2025_
## General
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
## Winder V2
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
## Extruder V2
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
# `2.10.0`
_16.10.2025_
## Winder V2
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
## General
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
# `2.9.1`
_10.10.2025_
## Winder V2
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
## General
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
# `2.9.0`
_06.10.2025_
## Winder V2
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
## General
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
# `2.8.0`
_30.09.2025_
## Laser V1
- [#741](https://github.com/qitechgmbh/control/pull/741) Added roundness measurement to two-axis Laser
## Winder V2
- [#745](https://github.com/qitechgmbh/control/pull/745), [#756](https://github.com/qitechgmbh/control/pull/756) improved Winder Frontend
## General
- [#447](https://github.com/qitechgmbh/control/pull/447) added AquaPath Machine
# `2.7.0`
_17.09.2025_
## Extruder V2
- [#695](https://github.com/qitechgmbh/control/pull/695) Pressure and Rpm Regulation properly switch again
## Winder V2
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
## Laser V1
- [#731](https://github.com/qitechgmbh/control/pull/731) Added Laser with two-axis measurements
## General
- [#679](https://github.com/qitechgmbh/control/pull/679),[#677](https://github.com/qitechgmbh/control/pull/677),[#698](https://github.com/qitechgmbh/control/pull/698), [#684](https://github.com/qitechgmbh/control/pull/684),[#706](https://github.com/qitechgmbh/control/pull/706) Greatly improved overall system latency and stability
- [#720](https://github.com/qitechgmbh/control/pull/720), [#686](https://github.com/qitechgmbh/control/pull/686) Improved User Interface
# `2.6.0`
_03.09.2025_
## Extruder V2
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
## Laser V1
- [#642](https://github.com/qitechgmbh/control/pull/642) Improved stability of laser connection
## General
- [#657](https://github.com/qitechgmbh/control/pull/657) Time frame for graphs is now remembered
- [#641](https://github.com/qitechgmbh/control/pull/641) Removed duplicate scrollbar from manuals
# `2.5.0`
_27.08.2025_
## Extruder V2
- [#595](https://github.com/qitechgmbh/control/pull/600) Simplified power graphs
- [#593](https://github.com/qitechgmbh/control/pull/599) Added identifiers for temperature sensors in temperature graph
## Winder V2
- [#598](https://github.com/qitechgmbh/control/pull/601) Images now load properly in the manual
## General
- [#540](https://github.com/qitechgmbh/control/pull/592) Improved readability of the presets
- [#591](https://github.com/qitechgmbh/control/pull/626) Significantly improved user interface performance and stability
# `2.4.0`
_14.08.2025_
## Winder V2
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
## General
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
- [#588](https://github.com/qitechgmbh/control/pull/589) Improved frontend performance
# `2.3.0`
_06.08.2025_
## Extruder V2
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
## Winder V2
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
## Laser V1
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
## General
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
# `2.2.0`
_15.07.2025_
## Extruder V2
- [#455](https://github.com/qitechgmbh/control/pull/458), Greatly improved stability and precision of pressure-based regulation for the motor
- [#467](https://github.com/qitechgmbh/control/pull/468), [#448](https://github.com/qitechgmbh/control/pull/449), [#485](https://github.com/qitechgmbh/control/pull/486) Improved the Responsiveness for motor control and monitoring
- [#138](https://github.com/qitechgmbh/control/pull/495), Added a Warning in case the Pressure sensor is disconnected or incorrectly wired
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
## Winder V2
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
## General
- [#121](https://github.com/qitechgmbh/control/pull/121), Use Linux realtime threads for EtherCAT and control loop (PREEMPT_RT)
- [#138](https://github.com/qitechgmbh/control/pull/495), Updated Quick Start Guide
# `2.1.0`
_30.06.2025_
## Extruder V2
- Intial Stable Release of **Extruder V2**
## Winder V2
- [#420](https://github.com/qitechgmbh/control/pull/429), [#401](https://github.com/qitechgmbh/control/pull/401) Add _Adaptive_ spool speed algorithm, the old algorithm is now called _Minmax_ ans tillavailable.
- [#427](https://github.com/qitechgmbh/control/pull/427) Add _Estimated Spool Diameter_ graph
- [#419](https://github.com/qitechgmbh/control/pull/419) Fix Issue where traverse won't work at low spool speed
## Laser V1
- Initial Stable Release of **Laser V1**
## HMI Panel
- [#339](https://github.com/qitechgmbh/control/pull/338) Don't go into sleep Mmde
- [#410](https://github.com/qitechgmbh/control/pull/410) Ship with Wireshark
- [#424](https://github.com/qitechgmbh/control/pull/424) Updates can be canceled
## General
- [#269](https://github.com/qitechgmbh/control/pull/269), [#370](https://github.com/qitechgmbh/control/pull/370) Improved performace of live values
- [#358](https://github.com/qitechgmbh/control/pull/358), [#343](https://github.com/qitechgmbh/control/pull/343), [#324](https://github.com/qitechgmbh/control/pull/324), [#390](https://github.com/qitechgmbh/control/pull/390), [#299](https://github.com/qitechgmbh/control/pull/299) Improved functionality for graphs
- [#405](https://github.com/qitechgmbh/control/pull/405) Remove weird color behaviour on tocuhscreens
- [#369](https://github.com/qitechgmbh/control/pull/369) More failsafe EtherCAT loop
# `2.0.1`
_10.06.2025_
# `2.0.0`
_10.06.2025_
## General
- Initial stable release of QiTech Control.
## Winder V2
- Initial stable release of **Winder V2**
## Features
- Stabilized **Winder V2**
Generated
+1335 -178
View File
File diff suppressed because it is too large. Load diff
+17
View File
@@ -5,6 +5,23 @@ members = [
"ethercat-hal-derive",
"control-core",
"ethercat-eeprom-dump",
"control-core-derive",
]
exclude = []
resolver = "2"
[workspace.lints.clippy]
# Individual lints (priority 0)
# don't lint poissbilities for let chains because they were introduces after Rust 1.86
collapsible_if = "allow"
# Lint groups (priority -1)
correctness = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
style = { level = "warn", priority = -1 }
pendantic = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
+17 -5
View File
@@ -4,6 +4,12 @@
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
[![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
# Repo Structure
Frontend
@@ -29,7 +35,7 @@ Other
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
@@ -41,6 +47,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
## Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
@@ -59,10 +67,12 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
# More Docs
@@ -87,7 +97,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- Write Device Identification
- Server
- [X] [Threading](./docs/control-loop.md)
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
+25 -5
View File
@@ -1,13 +1,33 @@
#!/bin/bash
if [ $1 == "release" ]; then
set -e
FEATURE=""
# Check if "mock-machine" was passed as an argument
if [[ "$1" == "mock-machine" ]]; then
echo "Building Debug Code with mock-machine feature"
FEATURE="--features mock-machine"
fi
if [[ "$1" == "release" ]]; then
echo "building Release Code"
cargo build --release
else
echo "building Debug Code"
cargo build
echo "Building Debug Code"
cargo build $FEATURE --features development-build
fi
# set cap for server executable
sudo setcap cap_net_raw=eip ./target/debug/server
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
if compgen -G "/dev/ttyUSB*" > /dev/null; then
echo "Setting permissions for serial ports"
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
fi
# run
./target/debug/server
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Nix store uses sqlite ... SO HAHAHAHAHH NO CONCURRENT ACCESS AHAHAHHAHAHAHHAHAHAHAH
time nix build .#packages.x86_64-linux.server --impure --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/server-result
serverstore=$(readlink -f /tmp/server-result)
time nix build .#packages.x86_64-linux.electron --impure --option sandbox false --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
electronstore=$(readlink -f /tmp/electron-result)
nix copy --to "file:///opt/dev-cache" \
$(readlink -f /tmp/server-result) \
$(readlink -f /tmp/electron-result) --extra-experimental-features nix-command
nix store sign \
--key-file /opt/dev-cache/cache-private-key.pem \
--store "file:///opt/dev-cache" \
--extra-experimental-features nix-command \
"$serverstore" "$electronstore"
nix path-info "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
# nix verify --store "file:///opt/dev-cache" \
# --trusted-public-keys "$(cat /opt/dev-cache/cache-public-key.pem)" --extra-experimental-features nix-command --extra-experimental-features flakes \
# "$FIRST_PATH" || echo "Verification failed!"
#time nix build .#packages.x86_64-linux.electron --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
#electronstore=$(readlink -f /tmp/electron-result)
#nix-store --generate-binary-cache-key cache.key cache.pem --extra-experimental-features nix-command
#nix store sign --key-file cache.key "$serverstore" --extra-experimental-features nix-command
#nix path-info --json "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
#export
#nix-store --export /nix/store/<hash>-server > nix-cache/server.nar
#nix-store --export /nix/store/<hash>-electron > nix-cache/electron.nar
# Any http server
# cd ~/nix-cache
# python3 -m http.server 8080
#
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "control_core_derive"
version = "0.1.0"
edition = "2024"
[lints]
workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
File diff suppressed because it is too large. Load diff
+65
View File
@@ -0,0 +1,65 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DeriveInput, Error};
#[proc_macro_derive(BuildEvent)]
pub fn build_event_derive(item: TokenStream) -> TokenStream {
build_event_derive2(item.into()).unwrap().into()
}
fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
// Parse the input tokens into a syntax tree
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
// Generate the BuildEvent impl
let expanded = quote! {
impl #impl_generics control_core::socketio::event::BuildEvent for #ident #ty_generics #where_clause {
#[doc = "Implemented by the BuildEvent derive macro"]
fn build(&self) -> control_core::socketio::event::Event<Self> {
control_core::socketio::event::Event::new(stringify!(#ident), self.clone())
}
}
};
Ok(expanded)
}
#[proc_macro_derive(Machine)]
pub fn machine_derive(item: TokenStream) -> TokenStream {
machine_derive2(item.into()).unwrap().into()
}
fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
// Parse the input tokens into a syntax tree
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
}
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
Ok(expanded)
}
+33 -7
View File
@@ -3,19 +3,45 @@ name = "control_core"
version = "0.1.0"
edition = "2024"
[lints]
workspace = true
[dependencies]
anyhow = "1.0.98"
axum = "0.8.4"
anyhow = "1.0.100"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = "0.6"
interfaces = "0.0.9"
log = "0.4.27"
serde = "1.0.219"
serde_json = "1.0.140"
serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.0"
uom = "0.36.0"
socketioxide = "0.17.2"
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.2.1"
crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.8"
core_affinity = "0.8.3"
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
tokio = { version = "1.45.1", features = ["sync"], optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.176"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8"
tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "time"] }
[features]
default = []
video-streaming = ["dep:tokio"]
[package.metadata.cargo-machete]
ignored = ["axum","bitvec","control_core_derive"]
@@ -1,91 +0,0 @@
use super::Actor;
use ethercat_hal::io::analog_output::AnalogOutput;
use std::{
f64::consts::PI,
future::Future,
pin::Pin,
time::{Duration, Instant},
};
use uom::si::{angle::radian, f64::Angle, ratio::ratio};
pub type AnalogFunction = Box<dyn Fn(Duration) -> f64 + Send + Sync>;
/// Can module analog output with a function
pub struct AnalogFunctionGenerator {
output: AnalogOutput,
function: AnalogFunction,
start_ts: Instant,
}
impl AnalogFunctionGenerator {
pub fn new(output: AnalogOutput, function: AnalogFunction) -> Self {
Self {
output,
function,
start_ts: Instant::now(),
}
}
}
impl Actor for AnalogFunctionGenerator {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let diff = now - self.start_ts;
let value = (self.function)(diff);
(self.output.write)((value as f32).into()).await;
})
}
}
pub fn analog_sine(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
// Convert to seconds as f64 for calculation
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let angle = Angle::new::<radian>(2.0 * PI * phase_secs / period_secs);
let value = angle.sin().get::<ratio>();
value * (amplitude / 2.0) + normal + (amplitude / 2.0)
})
}
pub fn analog_multiply<const N: usize>(functions: [AnalogFunction; N]) -> AnalogFunction {
Box::new(move |dt: Duration| functions.iter().fold(1.0, |acc, func| acc * func(dt)))
}
pub fn analog_sawtooth(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = phase_secs / period_secs;
value * amplitude + normal
})
}
pub fn analog_square(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
1.0
} else {
-1.0
};
value * amplitude + normal
})
}
pub fn analog_triangle(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
phase_secs / period_secs * 2.0
} else {
2.0 - phase_secs / period_secs * 2.0
};
value * amplitude + normal
})
}
@@ -1,40 +0,0 @@
use super::Actor;
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a analog input
#[derive(Debug)]
pub struct AnalogInputGetter {
input: AnalogInput,
normalized: Option<f32>,
}
impl AnalogInputGetter {
pub fn new(input: AnalogInput) -> Self {
Self {
input,
normalized: None,
}
}
/// Value from -1.0 to 1.0
pub fn get_normalized(&self) -> Option<f32> {
self.normalized
}
pub fn get_physical(&self) -> Option<AnalogInputValue> {
match self.get_normalized() {
Some(normalized) => Some(self.input.range.normalized_to_physical(normalized)),
None => return None,
}
}
}
impl Actor for AnalogInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
self.normalized = Some(state.input.normalized);
})
}
}
@@ -1,25 +0,0 @@
use super::Actor;
use ethercat_hal::io::digital_input::DigitalInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a digital input
pub struct DigitalInputLogger {
input: DigitalInput,
}
impl DigitalInputLogger {
pub fn new(input: DigitalInput) -> Self {
Self { input }
}
}
impl Actor for DigitalInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)()
.await
.expect("Failed to get digital input state");
println!("DigitalInputLogger: {}", state.input.value);
})
}
}
@@ -1,52 +0,0 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputBlinker {
last_toggle: Instant,
output: DigitalOutput,
interval: Duration,
enabled: bool,
}
impl DigitalOutputBlinker {
pub fn new(output: DigitalOutput, interval: Duration) -> Self {
Self {
last_toggle: Instant::now(),
output,
interval,
enabled: true,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinker {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
let state = (self.output.state)().await;
if now - self.last_toggle > self.interval {
match state.output.into() {
true => (self.output.write)(false.into()).await,
false => (self.output.write)(true.into()).await,
}
self.last_toggle = now;
}
}
})
}
}
@@ -1,57 +0,0 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a series of digital outputs high in sequence with a given interval
pub struct DigitalOutputBlinkers {
last_toggle: Instant,
outputs: Vec<Option<DigitalOutput>>,
index: usize,
interval: Duration,
enabled: bool,
amount: usize,
}
impl DigitalOutputBlinkers {
pub fn new(outputs: Vec<Option<DigitalOutput>>, interval: Duration, amount: usize) -> Self {
Self {
last_toggle: Instant::now(),
outputs,
interval,
enabled: true,
index: 0,
amount,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinkers {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if now - self.last_toggle > self.interval {
if let Some(output) = &self.outputs[self.index] {
(output.write)(true.into()).await;
}
let index_end =
(self.index + self.outputs.len() - self.amount) % self.outputs.len();
if let Some(output) = &self.outputs[index_end] {
(output.write)(false.into()).await;
}
self.last_toggle = now;
self.index = (self.index + 1) % self.outputs.len();
}
})
}
}
@@ -1,38 +0,0 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{future::Future, pin::Pin, time::Instant};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputSetter {
output: DigitalOutput,
enabled: bool,
}
impl DigitalOutputSetter {
pub fn new(output: DigitalOutput) -> Self {
Self {
output,
enabled: true,
}
}
pub fn set(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn get(&self) -> bool {
self.enabled
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
}
impl Actor for DigitalOutputSetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
(self.output.write)(self.enabled.into()).await;
}
})
}
}
@@ -1 +0,0 @@
// In this file create all Requests that are needed to int the inverter
@@ -1,630 +0,0 @@
use super::Actor;
use crate::{
converters::motor_converter::MotorConverter,
modbus::{ModbusFunctionCode, ModbusRequest, ModbusResponse, calculate_modbus_rtu_timeout},
};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
pin::Pin,
time::{Duration, Instant},
};
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Resuest we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub enum OperationMode {
PU,
EXT,
NET,
}
impl MitsubishiInverterRS485Actor {
pub fn convert_hz_float_to_word(&mut self, value: f32, little_endian: bool) -> u16 {
let scaled = value * 100.0; // Convert Hz to 0.01 Hz units
scaled.round() as u16
}
pub fn set_running_rpm_target(&mut self, rpm: f32) {
let mut request: MitsubishiModbusRequest =
MitsubishiControlRequests::WriteRunningFrequency.into();
let hz = MotorConverter::rpm_to_hz(rpm); // convert rpm to hz
let result: u16 = self.convert_hz_float_to_word(hz, true); // convert hz float to short
request.request.data[2] = result.to_le_bytes()[1];
request.request.data[3] = result.to_le_bytes()[0];
self.add_request(request);
}
pub fn read_running_frequency() {
// Check if the current element pushed to front is freq event
}
pub fn switch_operation_mode() {}
}
/// Specifies all System environmet Variables
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
enum MitsubishiSystemRegister {
/// Register 40002
InverterReset,
/// Register 40003
ParameterClear,
/// Register 40004
AllParameterClear,
/// Register 40006
ParamClearNonCommunication,
/// Register 40007
AllParameterClearNonCommunication,
/// Register 40009
InverterStatusAndControl,
/// Register 40010
OperationModeAndSetting,
/// Register 40014
RunningFrequencyRAM,
/// Register 40015
RunningFrequencyEEPROM,
/// Register 40201
MotorFrequency,
}
impl MitsubishiControlRequests {
fn get_system_register(value: MitsubishiSystemRegister) -> u16 {
match value {
MitsubishiSystemRegister::InverterReset => 0x1,
MitsubishiSystemRegister::ParameterClear => 0x2,
MitsubishiSystemRegister::AllParameterClear => 0x3,
MitsubishiSystemRegister::ParamClearNonCommunication => 0x5,
MitsubishiSystemRegister::AllParameterClearNonCommunication => 0x6,
MitsubishiSystemRegister::InverterStatusAndControl => 0x8,
MitsubishiSystemRegister::OperationModeAndSetting => 0x9,
MitsubishiSystemRegister::RunningFrequencyRAM => 0x0d,
MitsubishiSystemRegister::RunningFrequencyEEPROM => 0x0e,
MitsubishiSystemRegister::MotorFrequency => 0x00C8,
}
}
}
impl From<MitsubishiControlRequests> for MitsubishiModbusRequest {
fn from(request: MitsubishiControlRequests) -> Self {
match request {
MitsubishiControlRequests::WriteRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::WriteFrequency,
request_type: MitsubishiControlRequests::WriteRunningFrequency,
}
}
MitsubishiControlRequests::ReadInverterStatus => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::InverterStatus,
request_type: MitsubishiControlRequests::ReadInverterStatus,
}
}
MitsubishiControlRequests::StopMotor => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::InverterControl,
request_type: MitsubishiControlRequests::StopMotor,
}
}
MitsubishiControlRequests::StartForwardRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::InverterControl,
request_type: MitsubishiControlRequests::StartForwardRotation,
}
}
MitsubishiControlRequests::StartReverseRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::InverterControl,
request_type: MitsubishiControlRequests::StartReverseRotation,
}
}
MitsubishiControlRequests::ReadRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::ReadFrequency,
request_type: MitsubishiControlRequests::ReadRunningFrequency,
}
}
MitsubishiControlRequests::ReadMotorFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::MotorFrequency,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::ReadMotorFrequency,
request_type: MitsubishiControlRequests::ReadMotorFrequency,
}
}
MitsubishiControlRequests::ResetInverter => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterReset,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
},
request_category: RequestCategory::OperationCommand,
expected_response_type: ResponseType::NoResponse,
request_type: MitsubishiControlRequests::ResetInverter,
}
}
MitsubishiControlRequests::ClearAllParameters => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameter => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameters => todo!(),
}
}
}
/// These Requests Serve as Templates for controling the inverter
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum MitsubishiControlRequests {
/// Register 40002, Reset/Restart the Inverter
ResetInverter,
/// Register 40004, Clear ALL parameters
ClearAllParameters,
/// Register 40006, Clear a non communication parameter
ClearNonCommunicationParameter,
/// Register 40007, Clear all Non Communication related Parameters
ClearNonCommunicationParameters,
/// Register 40009, Read Inverter Status
ReadInverterStatus,
/// Register 40009, Stops the Motor
StopMotor,
/// Register 40009, Starts the Motor in Forward Rotation
StartForwardRotation,
/// Register 40009, Starts the Motor in Reverse Rotation
StartReverseRotation,
/// Register 40014, Read the current frequency the motor runs at (RAM)
ReadRunningFrequency,
/// Register 40014, Write the frequency
WriteRunningFrequency,
/// Read Register 40201, This contains the actual output frequency
ReadMotorFrequency,
}
pub enum Priority {
High,
Medium,
Low,
}
impl MitsubishiControlRequests {
pub fn get_priority(&self) -> Priority {
match self {
MitsubishiControlRequests::ResetInverter => Priority::High,
MitsubishiControlRequests::ClearAllParameters => Priority::Low,
MitsubishiControlRequests::ClearNonCommunicationParameter => Priority::Low,
MitsubishiControlRequests::ClearNonCommunicationParameters => Priority::Low,
MitsubishiControlRequests::ReadInverterStatus => Priority::High,
MitsubishiControlRequests::StopMotor => Priority::High,
MitsubishiControlRequests::StartForwardRotation => Priority::Medium,
MitsubishiControlRequests::StartReverseRotation => Priority::Medium,
MitsubishiControlRequests::ReadRunningFrequency => Priority::Medium,
MitsubishiControlRequests::WriteRunningFrequency => Priority::Medium,
MitsubishiControlRequests::ReadMotorFrequency => Priority::Medium,
}
}
}
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
#[derive(Debug, Clone)]
pub struct MitsubishiModbusRequest {
request: ModbusRequest,
request_category: RequestCategory,
request_type: MitsubishiControlRequests,
expected_response_type: ResponseType,
}
#[derive(Debug, Clone, Copy)]
pub enum RotationDirection {
Forward,
Backwards,
Stopped,
}
#[derive(Debug, Clone, Copy)]
pub enum ResponseType {
NoResponse,
ReadFrequency,
ReadMotorFrequency, // Motor Frequency is the actual frequency, that the motor is running at right now
ReadInverterStatus,
WriteFrequency,
InverterStatus,
InverterControl,
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum RequestType {
NoResponse,
ReadFrequency,
ReadMotorFrequency, // Motor Frequency is the actual frequency, that the motor is running at right now
ReadInverterStatus,
WriteFrequency,
InverterStatus,
InverterControl,
}
#[derive(Debug)]
pub struct MitsubishiInverterRS485Actor {
pub serial_interface: SerialInterface,
pub last_ts: Instant,
pub last_message_size: usize,
pub last_request_category: RequestCategory,
pub last_request_type: RequestType,
pub last_request: MitsubishiControlRequests,
pub state: State,
pub baudrate: Option<u32>,
pub encoding: Option<SerialEncoding>,
pub request_map: HashMap<MitsubishiControlRequests, MitsubishiModbusRequest>,
pub response: Option<ModbusResponse>,
pub forward_rotation: bool,
pub next_response_type: ResponseType,
pub current_freq: f32,
pub current_rpm: f32,
}
impl MitsubishiInverterRS485Actor {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
last_ts: Instant::now(),
state: State::Uninitialized,
request_map: HashMap::new(),
response: None,
forward_rotation: true,
next_response_type: ResponseType::ReadMotorFrequency,
last_request_type: RequestType::ReadMotorFrequency,
last_request_category: RequestCategory::OperationCommand,
last_message_size: 0,
baudrate: None,
encoding: None,
current_freq: 0.0,
current_rpm: 0.0,
last_request: MitsubishiControlRequests::ResetInverter,
}
}
/// This would get called by the api to add a new request to the inverter
pub fn add_request(&mut self, request: MitsubishiModbusRequest) {
self.request_map.insert(request.request_type, request);
}
/// This is used by the Api to pop off the Response of our Request
pub fn get_response(&mut self) -> Option<ModbusResponse> {
return self.response.clone();
}
/// This is used internally to read the receive buffer of the el6021
fn read_modbus_response(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<ModbusResponse, anyhow::Error>> + Send + '_>> {
Box::pin(async move {
if !(self.serial_interface.has_message)().await {
// return;
}
let res: Option<Vec<u8>> = (self.serial_interface.read_message)().await;
let raw_response = match res {
Some(res) => res,
None => {
log::error!("ERROR: No Modbus Response");
vec![]
}
};
let response: Result<ModbusResponse, _> =
ModbusResponse::try_from(raw_response.clone());
println!("response {:?}", raw_response);
match response {
Ok(result) => {
self.last_message_size = result.clone().data.len() + 4;
self.state = State::ReadyToSend;
self.response = Some(result.clone());
Ok(result)
}
Err(_) => {
//log::error!("Error Parsing ModbusResponse!");
self.state = State::ReadyToSend;
self.response = None;
Err(anyhow::anyhow!("Error Parsing ModbusResponse!"))
}
}
})
}
fn remove_request(&mut self, request_type: MitsubishiControlRequests) {
self.request_map.remove(&request_type);
}
/// This is used internally to fill the write buffer of the el6021
fn send_modbus_request(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if self.request_map.is_empty() {
return;
};
let mut high_prio = None;
let mut medium_prio = None;
let mut low_prio = None;
// Iterate over the keys
// Highest priority is StopMotor then ResetInverter, then
for key in self.request_map.keys() {
let prio = key.get_priority();
match prio {
Priority::High => high_prio = self.request_map.get(key),
Priority::Medium => medium_prio = self.request_map.get(key),
Priority::Low => low_prio = self.request_map.get(key),
}
}
let request: &MitsubishiModbusRequest;
let mut modbus_request: Vec<u8> = vec![];
if !high_prio.is_none() {
request = high_prio.unwrap();
self.next_response_type = request.expected_response_type;
self.last_request_category = request.request_category;
self.last_request = request.request_type;
modbus_request = request.request.clone().into();
self.remove_request(request.request_type);
} else if !medium_prio.is_none() {
request = medium_prio.unwrap();
self.next_response_type = request.expected_response_type;
self.last_request_category = request.request_category;
self.last_request = request.request_type;
modbus_request = request.request.clone().into();
} else if !low_prio.is_none() {
request = low_prio.unwrap();
self.next_response_type = request.expected_response_type;
self.last_request_category = request.request_category;
self.last_request = request.request_type;
modbus_request = request.request.clone().into();
}
let res = (self.serial_interface.write_message)(modbus_request.clone()).await;
match res {
Ok(_) => (),
Err(_) => log::error!("ERROR: serial_interface.write_message has failed"),
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
})
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum RequestCategory {
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
OperationCommand,
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
ReadWrite,
/// Less than 5 seconds timeout for Response
ParamClear,
/// no Timeout for Response
Reset,
}
impl RequestCategory {
fn timeout_duration(self) -> Duration {
match self {
RequestCategory::OperationCommand => Duration::from_millis(12),
RequestCategory::ReadWrite => Duration::from_millis(30),
RequestCategory::ParamClear => Duration::from_millis(5000),
RequestCategory::Reset => Duration::from_millis(30),
}
}
}
pub enum MitsubishiModbusExceptionCode {
IllegalFunction,
IllegalDataAddress,
IllegalDataValue,
None,
}
impl From<MitsubishiModbusExceptionCode> for u8 {
fn from(value: MitsubishiModbusExceptionCode) -> Self {
match value {
MitsubishiModbusExceptionCode::None => 0,
MitsubishiModbusExceptionCode::IllegalFunction => 1,
MitsubishiModbusExceptionCode::IllegalDataAddress => 2,
MitsubishiModbusExceptionCode::IllegalDataValue => 3,
}
}
}
impl From<u8> for MitsubishiModbusExceptionCode {
fn from(value: u8) -> Self {
match value {
1 => MitsubishiModbusExceptionCode::IllegalFunction,
2 => MitsubishiModbusExceptionCode::IllegalDataAddress,
3 => MitsubishiModbusExceptionCode::IllegalDataValue,
_ => MitsubishiModbusExceptionCode::None,
}
}
}
// Handle different Response types
impl MitsubishiInverterRS485Actor {
// When we get respone from Pr. 40014 (Running Frequency) Convert to rpm and save it
fn handle_motor_frequency(&mut self, resp: ModbusResponse) {
if resp.data.len() < 4 {
//return;
}
println!("mot freq");
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
self.current_freq = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f32 / 100.0;
self.current_rpm = MotorConverter::hz_to_rpm(self.current_freq);
}
fn handle_inverter_status(&mut self, resp: ModbusResponse) {}
fn handle_inverter_frequency(&mut self, resp: ModbusResponse) {}
fn handle_response(&mut self, resp: ModbusResponse) {
match self.next_response_type {
ResponseType::ReadFrequency => self.handle_inverter_frequency(resp),
ResponseType::ReadInverterStatus => (),
ResponseType::WriteFrequency => (),
ResponseType::ReadMotorFrequency => self.handle_motor_frequency(resp),
ResponseType::InverterStatus => self.handle_inverter_status(resp),
ResponseType::InverterControl => (),
ResponseType::NoResponse => (),
}
}
}
impl Actor for MitsubishiInverterRS485Actor {
fn act(&mut self, now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let elapsed: Duration = now_ts.duration_since(self.last_ts);
// State is uninitialized until serial interface init returns true, which takes a few cycles on the el6021
if let State::Uninitialized = self.state {
let res = (self.serial_interface.initialize)().await;
if res == true {
self.state = State::ReadyToSend;
// every time when our inverter is "Uninitialzed" reset it first to clear any error states it may have
self.add_request(MitsubishiControlRequests::ResetInverter.into());
self.baudrate = (self.serial_interface.get_baudrate)().await;
self.encoding = (self.serial_interface.get_serial_encoding)().await;
}
return;
}
let encoding = match self.encoding {
Some(encoding) => encoding,
None => return,
};
let baudrate = match self.baudrate {
Some(baudrate) => baudrate,
None => return,
};
let timeout = calculate_modbus_rtu_timeout(
encoding.total_bits(),
self.last_request_category.timeout_duration(),
baudrate,
self.last_message_size,
);
// if we have no requests add ReadMotorFrequency
if self.request_map.is_empty() {
self.add_request(MitsubishiControlRequests::ReadMotorFrequency.into());
}
if elapsed < timeout {
return;
}
self.last_ts = now_ts;
match self.state {
State::WaitingForResponse => {
let ret = self.read_modbus_response().await;
match ret {
Ok(ret) => self.handle_response(ret),
Err(_) => println!("Error"), // Do nothing for now
}
}
State::ReadyToSend => {
self.send_modbus_request().await;
self.remove_request(self.last_request);
}
State::WaitingForReceiveAccept => self.state = State::ReadyToSend,
State::WaitingForRequestAccept => self.state = State::WaitingForResponse,
_ => (),
}
})
}
}
-18
View File
@@ -1,18 +0,0 @@
pub mod analog_function_generator;
pub mod analog_input_getter;
pub mod digital_input_logger;
pub mod digital_output_blinker;
pub mod digital_output_blinkers;
pub mod digital_output_setter;
pub mod mitsubishi_inverter_rs485;
pub mod stepper_driver_el70x1;
pub mod stepper_driver_max_speed;
pub mod stepper_driver_pulse_train;
pub mod temperature_input_getter;
pub mod temperature_input_logger;
use std::{pin::Pin, time::Instant};
pub trait Actor: Send + Sync {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
@@ -1,88 +0,0 @@
use ethercat_hal::shared_config::el70x1::EL70x1SpeedRange;
#[derive(Debug)]
pub struct EL70x1VelocityCalculator {
max_steps_per_seconds: u16,
}
const MAX_VELOCITY: i16 = 10_000; // 100% of max speed
impl EL70x1VelocityCalculator {
pub fn new(speed_range: &EL70x1SpeedRange) -> Self {
let speed_range_value = match speed_range {
EL70x1SpeedRange::Steps1000 => 1000,
EL70x1SpeedRange::Steps2000 => 2000,
EL70x1SpeedRange::Steps4000 => 4000,
EL70x1SpeedRange::Steps8000 => 8000,
EL70x1SpeedRange::Steps16000 => 16000,
EL70x1SpeedRange::Steps32000 => 32000,
};
Self {
max_steps_per_seconds: speed_range_value,
}
}
/// Convert steps per second to the i16 velocity value used by the EL7031
pub fn steps_to_velocity(&self, steps_per_second: i32) -> i16 {
// Calculate the velocity value (10000 = 100% of max speed)
let velocity_f64 =
(steps_per_second as f64 / self.max_steps_per_seconds as f64) * MAX_VELOCITY as f64;
// Clamp to i16 range to be safe
velocity_f64.round() as i16
}
/// Convert i16 velocity value back to steps per second
pub fn velocity_to_steps(&self, velocity: i16) -> i16 {
// Calculate steps per second from velocity value
let steps_per_second =
(velocity as f64 / MAX_VELOCITY as f64) * self.max_steps_per_seconds as f64;
steps_per_second.round() as i16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_steps_to_velocity_conversion() {
let calc = EL70x1VelocityCalculator::new(&EL70x1SpeedRange::Steps4000); // 4000 steps/s
// 0 sps = 0% velocity
assert_eq!(calc.steps_to_velocity(0), 0);
// 1000 sps = 25% velocity = 2500
let v1 = calc.steps_to_velocity(1000);
assert!((v1 - 2500i16).abs() <= 1, "Expected 2500±1, got {}", v1);
// 2000 sps = 50% velocity = 5000
let v2 = calc.steps_to_velocity(2000);
assert!((v2 - 5000i16).abs() <= 1, "Expected 5000±1, got {}", v2);
// 3000 sps = 75% velocity = 7500
let v3 = calc.steps_to_velocity(3000);
assert!((v3 - 7500i16).abs() <= 1, "Expected 7500±1, got {}", v3);
// 4000 sps = 100% velocity = 10000
let v4 = calc.steps_to_velocity(4000);
assert!((v4 - 10000i16).abs() <= 1, "Expected 10000±1, got {}", v4);
}
#[test]
fn test_velocity_to_steps_conversion() {
let calc = EL70x1VelocityCalculator::new(&EL70x1SpeedRange::Steps4000); // 4000 steps/s
// 0% velocity = 0i16 = 0 sps
assert_eq!(calc.velocity_to_steps(0), 0);
// 25% velocity = 2500i16 = 1000 sps
let s1 = calc.velocity_to_steps(2500);
assert_eq!((s1 - 1000i16).abs(), 0, "Expected 1000±0, got {}", s1);
// 50% velocity = 5000i16 = 2000 sps
let s2 = calc.velocity_to_steps(5000);
assert_eq!((s2 - 2000i16).abs(), 0, "Expected 2000±0, got {}", s2);
// 75% velocity = 7500i16 = 3000 sps
let s3 = calc.velocity_to_steps(7500);
assert_eq!((s3 - 3000i16).abs(), 0, "Expected 3000±0, got {}", s3);
// 100% velocity = 10000i16 = 4000 sps
let s4 = calc.velocity_to_steps(10000);
assert_eq!((s4 - 4000i16).abs(), 0, "Expected 4000±0, got {}", s4);
}
}
@@ -1,75 +0,0 @@
use super::Actor;
use core::panic;
use el70xx_velocity_converter::EL70x1VelocityCalculator;
use ethercat_hal::{
io::stepper_velocity_el70x1::StepperVelocityEL70x1, shared_config::el70x1::EL70x1SpeedRange,
};
use std::{future::Future, pin::Pin, time::Instant};
pub mod el70xx_velocity_converter;
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverEL70x1 {
stepper: StepperVelocityEL70x1,
enabled: bool,
velocity: i16,
pub converter: EL70x1VelocityCalculator,
}
impl StepperDriverEL70x1 {
pub fn new(stepper: StepperVelocityEL70x1, speed_range: &EL70x1SpeedRange) -> Self {
Self {
stepper,
enabled: false,
velocity: 0,
converter: EL70x1VelocityCalculator::new(speed_range),
}
}
/// Set the speed in steps per second
pub fn set_speed(&mut self, steps_per_second: i32) {
self.velocity = self.converter.steps_to_velocity(steps_per_second)
}
/// Get the speed in steps per second
pub fn get_speed(&self) -> i32 {
self.converter.velocity_to_steps(self.velocity) as i32
}
/// Enable or disable the stepper
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Get the enabled state of the stepper
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
impl Actor for StepperDriverEL70x1 {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = match (self.stepper.state)().await {
Ok(state) => state,
Err(e) => {
panic!("Error while reading StepperVelocity {:?}", e);
}
};
let mut output = state.output.clone();
// set the output
output.stm_control.enable = self.enabled;
output.stm_velocity.velocity = self.velocity;
// write the output
match (self.stepper.write)(output).await {
Ok(_) => {}
Err(e) => {
panic!("Error while writing StepperVelocity {:?}", e);
}
}
})
}
}
@@ -1,37 +0,0 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock},
time::Instant,
};
/// Set a digital output high and low with a given interval
pub struct StepperDriverMaxSpeed {
pulse: DigitalOutput,
}
impl StepperDriverMaxSpeed {
pub fn new(output: DigitalOutput) -> Self {
Self { pulse: output }
}
}
impl Actor for StepperDriverMaxSpeed {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
match state.output.into() {
true => (self.pulse.write)(false.into()).await,
false => (self.pulse.write)(true.into()).await,
}
})
}
}
impl From<StepperDriverMaxSpeed> for Arc<RwLock<StepperDriverMaxSpeed>> {
fn from(actor: StepperDriverMaxSpeed) -> Self {
Arc::new(RwLock::new(actor))
}
}
@@ -1,222 +0,0 @@
/// Translates the encoder value to a larger space and handle underflow/overflow situations
#[derive(Debug)]
pub struct Encoder {
pub position: i64,
pub last_encoder_position: u32,
}
impl Encoder {
pub fn new() -> Self {
Self {
position: 0,
last_encoder_position: 0,
}
}
pub fn update(&mut self, counter_value: u32, counter_overflow: bool, counter_underflow: bool) {
// Handle overflow case
if counter_overflow {
// When overflow occurs, the counter wraps from MAX to 0
// Calculate how much it moved past MAX
let delta = counter_value as i64;
// Add the remaining distance from last position to MAX, plus the overflow amount
self.position += (u32::MAX - self.last_encoder_position) as i64 + 1 + delta;
}
// Handle underflow case
else if counter_underflow {
// When underflow occurs, the counter wraps from 0 to MAX
// Calculate how much it moved below 0
let delta = (u32::MAX - counter_value) as i64;
// Subtract the distance from last position to 0, plus the underflow amount
self.position -= self.last_encoder_position as i64 + 1 + delta;
}
// Normal case - no overflow/underflow
else {
// Calculate the signed difference between current and last position
let delta = counter_value as i64 - self.last_encoder_position as i64;
// Update the position
self.position += delta;
}
// Update the last encoder position for next time
self.last_encoder_position = counter_value;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests that the position tracker correctly handles normal forward movement
/// without any overflow conditions.
///
/// This test verifies:
/// 1. Initial state is zeroed
/// 2. Moving forward updates both position and last_encoder_position
/// 3. Consecutive forward movements accumulate correctly
#[test]
fn test_normal_forward_movement() {
let mut tracker = Encoder::new();
// Initial state
assert_eq!(tracker.position, 0);
assert_eq!(tracker.last_encoder_position, 0);
// Move forward by 100
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
assert_eq!(tracker.last_encoder_position, 100);
// Move forward by another 50
tracker.update(150, false, false);
assert_eq!(tracker.position, 150);
assert_eq!(tracker.last_encoder_position, 150);
}
/// Tests that the position tracker correctly handles normal backward movement
/// without any underflow conditions.
///
/// This test verifies:
/// 1. Starting from a non-zero position
/// 2. Moving backward updates both position and last_encoder_position
/// 3. Consecutive backward movements accumulate correctly
#[test]
fn test_normal_backward_movement() {
let mut tracker = Encoder::new();
// Start at position 1000
tracker.position = 1000;
tracker.last_encoder_position = 1000;
// Move backward by 100
tracker.update(900, false, false);
assert_eq!(tracker.position, 900);
assert_eq!(tracker.last_encoder_position, 900);
// Move backward by another 50
tracker.update(850, false, false);
assert_eq!(tracker.position, 850);
assert_eq!(tracker.last_encoder_position, 850);
}
/// Tests that the position tracker correctly handles overflow conditions
/// where the encoder counter wraps from near its maximum value to a small value.
///
/// This test verifies:
/// 1. When overflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_overflow() {
let mut tracker = Encoder::new();
// Set position near the maximum u32 value
tracker.position = 100;
tracker.last_encoder_position = u32::MAX - 10;
// Overflow: counter goes from (MAX-10) to 5
// This means we moved forward by 10 steps to MAX, then 1 step to overflow,
// then 5 more steps to reach the current position
tracker.update(5, true, false);
// Expected: position += (10 + 1 + 5) = position += 16
assert_eq!(tracker.position, 116);
assert_eq!(tracker.last_encoder_position, 5);
}
/// Tests that the position tracker correctly handles underflow conditions
/// where the encoder counter wraps from near zero to a value near its maximum.
///
/// This test verifies:
/// 1. When underflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled (in negative direction)
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_underflow() {
let mut tracker = Encoder::new();
// Set position near zero
tracker.position = 100;
tracker.last_encoder_position = 10;
// Underflow: counter goes from 10 to (MAX-5)
// This means we moved backward by 10 steps to 0, then 1 step to underflow,
// then 5 more steps to reach the current position
tracker.update(u32::MAX - 5, false, true);
// Expected: position -= (10 + 1 + 5) = position -= 16
assert_eq!(tracker.position, 84);
assert_eq!(tracker.last_encoder_position, u32::MAX - 5);
}
/// Tests that the position tracker maintains correct behavior across
/// a sequence of different operations including normal movement,
/// overflow, and underflow.
///
/// This test verifies:
/// 1. The tracker maintains state correctly between operations
/// 2. Different types of movements can be handled in sequence
/// 3. Position calculations remain accurate throughout various scenarios
#[test]
fn test_multiple_operations() {
let mut tracker = Encoder::new();
// Normal forward
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
// Normal backward
tracker.update(50, false, false);
assert_eq!(tracker.position, 50);
// Overflow
tracker.last_encoder_position = u32::MAX - 5;
tracker.position = 50; // Reset for clarity
tracker.update(10, true, false);
assert_eq!(tracker.position, 66); // 50 + (5 + 1 + 10)
// Underflow
tracker.last_encoder_position = 5;
tracker.position = 100; // Reset for clarity
tracker.update(u32::MAX - 10, false, true);
assert_eq!(tracker.position, 84); // 100 - (5 + 1 + 10)
}
/// Tests that the position tracker can handle multiple overflow and underflow
/// events, maintaining an accurate position count that exceeds the u32 range.
///
/// This test verifies:
/// 1. Multiple overflow events accumulate correctly in the i64 position
/// 2. Multiple underflow events correctly subtract from the position
/// 3. The i64 position field properly extends the range beyond u32 limits
#[test]
fn test_large_movements() {
let mut tracker = Encoder::new();
// Test tracking beyond u32 limits with multiple overflows
for _ in 0..5 {
// Simulate overflow: move from (MAX-100) to 100
tracker.last_encoder_position = u32::MAX - 100;
tracker.update(100, true, false);
// Each iteration adds (100 + 1 + 100) = 201 to position
// We move 100 steps to MAX, 1 step to overflow, and 100 more steps to reach 100
}
// After 5 iterations, position should be 5 * 201 = 1005
assert_eq!(tracker.position, 1005);
// Test tracking with multiple underflows
for _ in 0..3 {
// Simulate underflow: move from 100 to (MAX-100)
tracker.last_encoder_position = 100;
tracker.update(u32::MAX - 100, false, true);
// Each iteration subtracts (100 + 1 + 100) = 201 from position
// We move 100 steps to 0, 1 step to underflow, and 100 more steps to reach MAX-100
}
// Final position: 1005 - (3 * 201) = 1005 - 603 = 402
assert_eq!(tracker.position, 402);
}
}
@@ -1,51 +0,0 @@
use super::Actor;
use encoder::Encoder;
use ethercat_hal::io::pulse_train_output::PulseTrainOutput;
use std::{future::Future, pin::Pin, time::Instant};
pub mod encoder;
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverPulseTrain {
pulse: PulseTrainOutput,
encoder: Encoder,
frequency: i32,
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn new(output: PulseTrainOutput) -> Self {
Self {
pulse: output,
encoder: Encoder::new(),
frequency: 0,
}
}
}
impl<'ptodevice> Actor for StepperDriverPulseTrain {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
let mut output = state.output.clone();
// sync the counter to the encoder
self.encoder.update(
state.input.counter_value,
state.input.counter_overflow,
state.input.counter_underflow,
);
// write frequency
output.frequency_value = self.frequency;
(self.pulse.write)(output).await;
})
}
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn set_frequency(&mut self, frequency: i32) {
self.frequency = frequency;
}
}
@@ -1,35 +0,0 @@
use std::{pin::Pin, time::Instant};
use ethercat_hal::io::temperature_input::TemperatureInput;
use super::Actor;
#[derive(Debug)]
pub struct TemperatureInputGetter {
input: TemperatureInput,
pub temperature: f32,
pub wiring_error: bool,
}
impl TemperatureInputGetter {
pub fn new(input: TemperatureInput) -> Self {
Self {
input,
temperature: 0.0,
wiring_error: false,
}
}
}
impl Actor for TemperatureInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let input = (self.input.state)().await.input;
self.temperature = input.temperature;
self.wiring_error = input.overvoltage | input.undervoltage;
if self.wiring_error {
self.temperature = 0.0;
}
})
}
}
@@ -1,23 +0,0 @@
use super::Actor;
use ethercat_hal::io::temperature_input::TemperatureInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a temperature input
pub struct TemperatureInputLogger {
input: TemperatureInput,
}
impl TemperatureInputLogger {
pub fn new(input: TemperatureInput) -> Self {
Self { input }
}
}
impl Actor for TemperatureInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
println!("TemperatureInputLogger: {:?}C", state.input.temperature);
})
}
}
@@ -0,0 +1,218 @@
use std::time::Instant;
#[derive(Debug)]
pub struct ClampingTimeagnosticPidController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Derivative error
ed: f64,
min_ep: Option<f64>,
max_ep: Option<f64>,
min_ei: Option<f64>,
max_ei: Option<f64>,
min_ed: Option<f64>,
max_ed: Option<f64>,
min_signal: Option<f64>,
max_signal: Option<f64>,
last: Option<Instant>,
}
impl ClampingTimeagnosticPidController {
pub const fn new(
kp: f64,
ki: f64,
kd: f64,
min_ep: Option<f64>,
max_ep: Option<f64>,
min_ei: Option<f64>,
max_ei: Option<f64>,
min_ed: Option<f64>,
max_ed: Option<f64>,
min_signal: Option<f64>,
max_signal: Option<f64>,
) -> Self {
Self {
kp,
ki,
kd,
ep: 0.0,
ei: 0.0,
ed: 0.0,
last: None,
min_ep,
max_ep,
min_ei,
max_ei,
min_ed,
max_ed,
min_signal,
max_signal,
}
}
pub const fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
kd,
ep: 0.0,
ei: 0.0,
ed: 0.0,
min_ep: None,
max_ep: None,
min_ei: None,
max_ei: None,
min_ed: None,
max_ed: None,
last: None,
min_signal: None,
max_signal: None,
}
}
pub const fn get_kp(&self) -> f64 {
self.kp
}
pub const fn get_ki(&self) -> f64 {
self.ki
}
pub const fn get_kd(&self) -> f64 {
self.kd
}
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub const fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
match self.last {
// First update
None => {
// Calculate error
let ep = error;
// Calculate signal
let signal = self.kp * ep;
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
// Set values
self.ep = Self::optional_clamp(ep, self.min_ep, self.max_ep);
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
clamped_signal
}
// Subsequent updates
Some(last) => {
// Calculate the time delta in seconds
let dt = t.duration_since(last).as_secs_f64();
// Calculate errors
let ep = Self::optional_clamp(error, self.min_ep, self.max_ep);
let ei = Self::optional_clamp(ep.mul_add(dt, self.ei), self.min_ei, self.max_ei);
let ed = Self::optional_clamp((ep - self.ep) / dt, self.min_ed, self.max_ed);
// Make factors timeagnostic
let kp = self.kp * dt;
let ki = self.ki * dt;
let kd = self.kd * dt;
// Calculate signal
let signal = kd.mul_add(ed, kp.mul_add(ep, ki * ei));
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
// Set values
self.ep = ep;
self.ei = ei;
self.ed = ed;
self.last = Some(t);
clamped_signal
}
}
}
pub const fn reset(&mut self) {
self.ep = 0.0;
self.ei = 0.0;
self.ed = 0.0;
self.last = None;
}
}
#[cfg(test)]
mod tests {
use crate::controllers::clamping_timeagnostic_pid::ClampingTimeagnosticPidController;
#[test]
fn test_optional_clamp_with_both_bounds() {
let val = 5.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(1.0), Some(4.0));
assert_eq!(clamped, 4.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(6.0), Some(10.0));
assert_eq!(clamped, 6.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), Some(6.0));
assert_eq!(clamped, 5.0);
}
#[test]
fn test_optional_clamp_with_only_min() {
let val = 3.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(5.0), None);
assert_eq!(clamped, 5.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), None);
assert_eq!(clamped, 3.0);
}
#[test]
fn test_optional_clamp_with_only_max() {
let val = 7.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(6.0));
assert_eq!(clamped, 6.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(8.0));
assert_eq!(clamped, 7.0);
}
#[test]
fn test_optional_clamp_with_no_bounds() {
let val = 42.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, None);
assert_eq!(clamped, 42.0);
}
}
@@ -0,0 +1,124 @@
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct TimeAgnosticDeadTimePiController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Dead Time
dead: Duration,
last: Option<Instant>,
}
impl TimeAgnosticDeadTimePiController {
pub fn new(kp: f64, ki: f64, dead: Duration) -> Self {
Self {
kp,
ki,
dead,
ep: 0.0,
ei: 0.0,
last: None,
}
}
pub fn configure(&mut self, kp: f64, ki: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
}
pub fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_dead(&self) -> Duration {
self.dead
}
pub fn set_kp(&mut self, kp: f64) {
self.kp = kp;
}
pub fn set_ki(&mut self, ki: f64) {
self.ki = ki;
}
pub fn set_dead(&mut self, dead: Duration) {
self.dead = dead;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let kp = self.kp / 10000.0;
let ki = self.ki / 10000.0;
let signal = match self.last {
// First update
None => {
let ep = error;
// Initialize integral error
self.ei = 0.0;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
// Subsequent updates
Some(last) => {
// Calculate elapsed time
let dt = t.duration_since(last).as_secs_f64();
// Dead-time scaling factor
let dead_secs = self.dead.as_secs_f64();
let scale = (dt / dead_secs).min(1.0);
// Smoothed proportional error
let ep = self.ep + (error - self.ep) * scale;
// Integrate error over time
self.ei += error * dt;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
};
-signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
//self.last = None;
}
}
@@ -0,0 +1,434 @@
use std::time::Instant;
#[derive(Debug)]
pub struct AccelerationSpeedController {
/// Maximum deceleration in units per second (positive value)
max_acceleration: f64,
/// Maximum acceleratoin in units per second (negative value)
min_acceleration: f64,
/// Minimum speed limit (None for no limit)
min_speed: Option<f64>,
/// Maximum speed limit (None for no limit)
max_speed: Option<f64>,
/// Calculated speed at the last update
last_speed: f64,
/// Last update time
last_t: Option<Instant>,
}
impl AccelerationSpeedController {
pub const fn new(
min_speed: Option<f64>,
max_speed: Option<f64>,
min_acceleration: f64,
max_acceleration: f64,
initial_speed: f64,
) -> Self {
Self {
min_acceleration,
max_acceleration,
min_speed,
max_speed,
last_speed: initial_speed,
last_t: None,
}
}
/// Creates a new acceleration speed controller with simplified parameters.
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
/// No speed limits are applied.
pub fn new_simple(max_acceleration: f64, initial_speed: f64) -> Self {
Self::new(
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
initial_speed,
)
}
pub fn update(&mut self, target_speed: f64, t: Instant) -> f64 {
// Calculate time delta
let dt = match self.last_t {
Some(last) => {
let duration = t.duration_since(last);
duration.as_secs_f64()
}
None => 0.0, // First update, no acceleration applied
};
// Update the last update time
self.last_t = Some(t);
// Get acceleration
let acceleration = if target_speed > self.last_speed {
// We are accelerating
self.max_acceleration
} else if target_speed < self.last_speed {
// We are decelerating
self.min_acceleration
} else {
0.0
};
let speed_change = acceleration * dt;
let new_speed = self.last_speed + speed_change;
// Prevent overshooting the target speed
let new_speed = if acceleration > 0.0 {
// Limit speed when accelerating
new_speed.min(target_speed)
} else if acceleration < 0.0 {
// Limit speed when decelerating
new_speed.max(target_speed)
} else {
new_speed
};
// Apply speed limits
let new_speed = self.apply_speed_limits(new_speed);
self.last_speed = new_speed;
new_speed
}
pub const fn reset(&mut self, initial_speed: f64) {
self.last_speed = initial_speed;
self.last_t = None; // Reset the last update time
}
pub const fn set_max_acceleration(&mut self, acceleration: f64) {
self.max_acceleration = acceleration;
}
const fn apply_speed_limits(&self, speed: f64) -> f64 {
let mut limited_speed = speed;
if let Some(min) = self.min_speed {
limited_speed = limited_speed.max(min);
}
if let Some(max) = self.max_speed {
limited_speed = limited_speed.min(max);
}
limited_speed
}
pub const fn get_min_speed(&self) -> Option<f64> {
self.min_speed
}
pub const fn get_max_speed(&self) -> Option<f64> {
self.max_speed
}
pub const fn set_min_speed(&mut self, min_speed: Option<f64>) {
self.min_speed = min_speed;
}
pub const fn set_max_speed(&mut self, max_speed: Option<f64>) {
self.max_speed = max_speed;
}
pub const fn set_min_acceleration(&mut self, deceleration: f64) {
self.min_acceleration = deceleration;
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use std::time::Duration;
const EPSILON: f64 = 0.001;
// Custom function to create a "future" instant for testing
fn future_instant(now: Instant, seconds: f64) -> Instant {
now + Duration::from_secs_f64(seconds)
}
#[test]
fn test_initialization() {
let controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
assert_relative_eq!(controller.max_acceleration, 10.0, epsilon = EPSILON);
assert_relative_eq!(controller.min_acceleration, -15.0, epsilon = EPSILON);
assert_relative_eq!(controller.last_speed, 5.0, epsilon = EPSILON);
assert!(controller.last_t.is_none());
}
#[test]
fn test_first_update() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
let now = Instant::now();
let speed = controller.update(5.0, now);
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
assert_relative_eq!(controller.last_speed, 5.0, epsilon = EPSILON);
assert_eq!(controller.last_t.unwrap(), now);
}
#[test]
fn test_acceleration() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
let t1 = Instant::now();
controller.update(0.0, t1); // Initialize last_t
// Use 0.1 seconds as our simulated time difference
let dt = 0.1;
let t2 = future_instant(t1, dt);
let expected_speed = (10.0 * dt).min(5.0); // Should accelerate by max_acceleration * dt, capped at target
let actual_speed = controller.update(5.0, t2);
assert!(actual_speed > 0.0);
assert!(actual_speed <= 5.0);
assert_relative_eq!(actual_speed, expected_speed, epsilon = EPSILON);
}
#[test]
fn test_deceleration() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 10.0);
let t1 = Instant::now();
controller.update(10.0, t1); // Initialize last_t
// Use 0.1 seconds as our simulated time difference
let dt = 0.1;
let t2 = future_instant(t1, dt);
let expected_speed = (10.0 + (-15.0 * dt)).max(5.0); // Should decelerate by max_deceleration * dt, floored at target
let actual_speed = controller.update(5.0, t2);
assert!(actual_speed < 10.0);
assert!(actual_speed >= 5.0);
assert_relative_eq!(actual_speed, expected_speed, epsilon = EPSILON);
}
#[test]
fn test_constant_speed() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 7.0);
let t1 = Instant::now();
controller.update(7.0, t1); // Initialize last_t
let t2 = future_instant(t1, 0.1);
let speed = controller.update(7.0, t2);
assert_relative_eq!(speed, 7.0, epsilon = EPSILON);
}
#[test]
fn test_acceleration_limit() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
let t1 = Instant::now();
controller.update(0.0, t1); // Initialize last_t
let dt = 0.1;
let t2 = future_instant(t1, dt);
let max_possible_change = 10.0 * dt;
// Target is far away, should be limited by max_acceleration
let actual_speed = controller.update(100.0, t2);
assert_relative_eq!(actual_speed, max_possible_change, epsilon = EPSILON);
}
#[test]
fn test_deceleration_limit() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 20.0);
let t1 = Instant::now();
controller.update(20.0, t1); // Initialize last_t
let dt = 0.1;
let t2 = future_instant(t1, dt);
let max_possible_change = -15.0 * dt;
// Target is far away, should be limited by max_deceleration
let actual_speed = controller.update(0.0, t2);
assert_relative_eq!(actual_speed, 20.0 + max_possible_change, epsilon = EPSILON);
}
#[test]
fn test_zero_time_delta() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
let now = Instant::now();
controller.update(5.0, now); // Initialize last_t
// Update again with the same time stamp
let speed = controller.update(10.0, now);
// With zero time delta, speed shouldn't change
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
}
#[test]
fn test_acceleration_capped_at_target() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 100.0, 3.0);
let t1 = Instant::now();
controller.update(3.0, t1); // Initialize last_t
// With a high acceleration of 100.0 and dt of 0.1,
// we could go up by 10.0 units, from 3.0 to 13.0
// But target is only 5.0, so we should cap at 5.0
let dt = 0.1;
let t2 = future_instant(t1, dt);
let speed = controller.update(5.0, t2);
// Speed should be capped at target_speed (5.0), not 13.0
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
}
#[test]
fn test_deceleration_capped_at_target() {
let mut controller = AccelerationSpeedController::new(None, None, -100.0, 10.0, 8.0);
let t1 = Instant::now();
controller.update(8.0, t1); // Initialize last_t
// With a high deceleration of -100.0 and dt of 0.1,
// we could go down by 10.0 units, from 8.0 to -2.0
// But target is 5.0, so we should cap at 5.0
let dt = 0.1;
let t2 = future_instant(t1, dt);
let speed = controller.update(5.0, t2);
// Speed should be capped at target_speed (5.0), not -2.0
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
}
#[test]
fn test_exact_acceleration_to_target() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 20.0, 3.0);
let t1 = Instant::now();
controller.update(3.0, t1); // Initialize last_t
// With acceleration of 20.0 and dt of 0.1,
// we would go up exactly 2.0 units, from 3.0 to 5.0
// Target is 5.0, so we should hit it exactly
let dt = 0.1;
let t2 = future_instant(t1, dt);
let speed = controller.update(5.0, t2);
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
}
#[test]
fn test_exact_deceleration_to_target() {
let mut controller = AccelerationSpeedController::new(None, None, -30.0, 10.0, 8.0);
let t1 = Instant::now();
controller.update(8.0, t1); // Initialize last_t
// With deceleration of -30.0 and dt of 0.1,
// we would go down exactly 3.0 units, from 8.0 to 5.0
// Target is 5.0, so we should hit it exactly
let dt = 0.1;
let t2 = future_instant(t1, dt);
let speed = controller.update(5.0, t2);
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
}
#[test]
fn test_speed_limits_initialization() {
let controller =
AccelerationSpeedController::new(Some(-50.0), Some(100.0), -10.0, 20.0, 5.0);
assert_eq!(controller.get_min_speed(), Some(-50.0));
assert_eq!(controller.get_max_speed(), Some(100.0));
}
#[test]
fn test_max_speed_limit() {
let mut controller = AccelerationSpeedController::new(None, Some(50.0), -10.0, 100.0, 0.0);
let t1 = Instant::now();
controller.update(0.0, t1); // Initialize last_t
let dt = 1.0; // 1 second
let t2 = future_instant(t1, dt);
// Try to reach 80.0, but should be limited to 50.0
let speed = controller.update(80.0, t2);
assert_eq!(speed, 50.0);
}
#[test]
fn test_min_speed_limit() {
let mut controller = AccelerationSpeedController::new(Some(-30.0), None, -100.0, 10.0, 0.0);
let t1 = Instant::now();
controller.update(0.0, t1); // Initialize last_t
let dt = 1.0; // 1 second
let t2 = future_instant(t1, dt);
// Try to reach -50.0, but should be limited to -30.0
let speed = controller.update(-50.0, t2);
assert_eq!(speed, -30.0);
}
#[test]
fn test_speed_within_limits() {
let mut controller =
AccelerationSpeedController::new(Some(-50.0), Some(50.0), -20.0, 20.0, 0.0);
let t1 = Instant::now();
controller.update(0.0, t1); // Initialize last_t
let dt = 0.5; // 0.5 seconds
let t2 = future_instant(t1, dt);
// Target 10.0, with acceleration 20.0, should reach 10.0 in 0.5s
let speed = controller.update(10.0, t2);
assert_relative_eq!(speed, 10.0, epsilon = EPSILON);
}
#[test]
fn test_speed_limit_setters() {
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
// Initially no limits
assert_eq!(controller.get_min_speed(), None);
assert_eq!(controller.get_max_speed(), None);
// Set limits
controller.set_min_speed(Some(-100.0));
controller.set_max_speed(Some(100.0));
assert_eq!(controller.get_min_speed(), Some(-100.0));
assert_eq!(controller.get_max_speed(), Some(100.0));
// Remove limits
controller.set_min_speed(None);
controller.set_max_speed(None);
assert_eq!(controller.get_min_speed(), None);
assert_eq!(controller.get_max_speed(), None);
}
#[test]
fn test_new_simple_constructor() {
let controller = AccelerationSpeedController::new_simple(20.0, 5.0);
// Check that acceleration values are symmetric
assert_eq!(controller.min_acceleration, -20.0);
assert_eq!(controller.max_acceleration, 20.0);
assert_eq!(controller.last_speed, 5.0);
// Check that speed limits are None
assert_eq!(controller.get_min_speed(), None);
assert_eq!(controller.get_max_speed(), None);
}
}
@@ -0,0 +1,94 @@
use std::time::Instant;
use uom::si::{
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularVelocity},
};
use super::acceleration_speed_controller::AccelerationSpeedController;
/// [`LinearAngularAccelerationController`] wraps [`LinearAccelerationController`]
/// to handle angular velocities and accelerations.
#[derive(Debug)]
pub struct AngularAccelerationSpeedController {
pub controller: AccelerationSpeedController,
}
impl AngularAccelerationSpeedController {
pub fn new(
min_speed: Option<AngularVelocity>,
max_speed: Option<AngularVelocity>,
min_acceleration: AngularAcceleration,
max_acceleration: AngularAcceleration,
initial_speed: AngularVelocity,
) -> Self {
Self {
controller: AccelerationSpeedController::new(
min_speed.map(|s| s.get::<radian_per_second>()),
max_speed.map(|s| s.get::<radian_per_second>()),
min_acceleration.get::<radian_per_second_squared>(),
max_acceleration.get::<radian_per_second_squared>(),
initial_speed.get::<radian_per_second>(),
),
}
}
/// Creates a new angular acceleration speed controller with simplified parameters.
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
/// No speed limits are applied.
pub fn new_simple(
max_acceleration: AngularAcceleration,
initial_speed: AngularVelocity,
) -> Self {
Self::new(
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
initial_speed,
)
}
pub fn update(&mut self, target_speed: AngularVelocity, t: Instant) -> AngularVelocity {
let target_speed = target_speed.get::<radian_per_second>();
let new_speed = self.controller.update(target_speed, t);
AngularVelocity::new::<radian_per_second>(new_speed)
}
pub fn reset(&mut self, initial_speed: AngularVelocity) {
let initial_speed = initial_speed.get::<radian_per_second>();
self.controller.reset(initial_speed);
}
pub fn set_max_acceleration(&mut self, acceleration: AngularAcceleration) {
self.controller
.set_max_acceleration(acceleration.get::<radian_per_second_squared>());
}
pub fn set_min_acceleration(&mut self, deceleration: AngularAcceleration) {
self.controller
.set_min_acceleration(deceleration.get::<radian_per_second_squared>());
}
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_min_speed()
.map(AngularVelocity::new::<radian_per_second>)
}
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_max_speed()
.map(AngularVelocity::new::<radian_per_second>)
}
pub fn set_min_speed(&mut self, min_speed: Option<AngularVelocity>) {
self.controller
.set_min_speed(min_speed.map(|s| s.get::<radian_per_second>()));
}
pub fn set_max_speed(&mut self, max_speed: Option<AngularVelocity>) {
self.controller
.set_max_speed(max_speed.map(|s| s.get::<radian_per_second>()));
}
}
@@ -0,0 +1,83 @@
use std::time::Instant;
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Velocity},
velocity::meter_per_second,
};
use super::acceleration_speed_controller::AccelerationSpeedController;
/// [`LinearAccelerationController`] wraps [`LinearF64AccelerationController`]
/// to handle linear velocities and accelerations.
#[derive(Debug)]
pub struct LinearAccelerationLimitingController {
pub controller: AccelerationSpeedController,
}
impl LinearAccelerationLimitingController {
pub fn new(
min_speed: Option<Velocity>,
max_speed: Option<Velocity>,
acceleration: Acceleration,
deceleration: Acceleration,
initial_speed: Velocity,
) -> Self {
Self {
controller: AccelerationSpeedController::new(
min_speed.map(|s| s.get::<meter_per_second>()),
max_speed.map(|s| s.get::<meter_per_second>()),
acceleration.get::<meter_per_second_squared>(),
deceleration.get::<meter_per_second_squared>(),
initial_speed.get::<meter_per_second>(),
),
}
}
/// Creates a new linear acceleration speed controller with simplified parameters.
/// Sets deceleration to -acceleration for symmetric behavior.
/// No speed limits are applied.
pub fn new_simple(acceleration: Acceleration, initial_speed: Velocity) -> Self {
Self::new(
None, // min_speed
None, // max_speed
acceleration, // acceleration
-acceleration, // deceleration (negative for deceleration)
initial_speed,
)
}
pub fn update(&mut self, target_speed: Velocity, t: Instant) -> Velocity {
let target_speed = target_speed.get::<meter_per_second>();
let new_speed = self.controller.update(target_speed, t);
Velocity::new::<meter_per_second>(new_speed)
}
pub fn reset(&mut self, initial_speed: Velocity) {
let initial_speed = initial_speed.get::<meter_per_second>();
self.controller.reset(initial_speed);
}
pub fn set_acceleration(&mut self, acceleration: Acceleration) {
self.controller
.set_max_acceleration(acceleration.get::<meter_per_second_squared>());
}
pub fn set_deceleration(&mut self, deceleration: Acceleration) {
self.controller
.set_min_acceleration(deceleration.get::<meter_per_second_squared>());
}
pub fn get_min_speed(&self) -> Option<Velocity> {
self.controller
.get_min_speed()
.map(Velocity::new::<meter_per_second>)
}
pub fn get_max_speed(&self) -> Option<Velocity> {
self.controller
.get_max_speed()
.map(Velocity::new::<meter_per_second>)
}
pub fn set_min_speed(&mut self, min_speed: Option<Velocity>) {
self.controller
.set_min_speed(min_speed.map(|s| s.get::<meter_per_second>()));
}
pub fn set_max_speed(&mut self, max_speed: Option<Velocity>) {
self.controller
.set_max_speed(max_speed.map(|s| s.get::<meter_per_second>()));
}
}
@@ -0,0 +1,3 @@
pub mod acceleration_speed_controller;
pub mod angular_acceleration_speed_controller;
pub mod linear_acceleration_speed_controller;
+4
View File
@@ -1 +1,5 @@
pub mod clamping_timeagnostic_pid;
pub mod deadtime_pi_controller;
pub mod first_degree_motion;
pub mod pid;
pub mod second_degree_motion;
+26 -9
View File
@@ -9,7 +9,6 @@ pub struct PidController {
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
@@ -22,7 +21,7 @@ pub struct PidController {
}
impl PidController {
pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
pub const fn new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
@@ -34,8 +33,27 @@ impl PidController {
}
}
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub const fn get_kp(&self) -> f64 {
self.kp
}
pub const fn get_ki(&self) -> f64 {
self.ki
}
pub const fn get_kd(&self) -> f64 {
self.kd
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let signal = match self.last {
match self.last {
// First update
None => {
// Calculate error
@@ -47,6 +65,7 @@ impl PidController {
// Set values
self.ep = ep;
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
@@ -59,11 +78,11 @@ impl PidController {
// Calculate errors
let ep = error;
let ei = self.ei + ep * dt;
let ei = ep.mul_add(dt, self.ei);
let ed = (ep - self.ep) / dt;
// Calculate signal
let signal = self.kp * ep + self.ki * ei + self.kd * ed;
let signal = self.kd.mul_add(ed, self.kp.mul_add(ep, self.ki * ei));
// Set values
self.ep = ep;
@@ -73,12 +92,10 @@ impl PidController {
signal
}
};
signal
}
}
pub fn reset(&mut self) {
pub const fn reset(&mut self) {
self.ep = 0.0;
self.ei = 0.0;
self.ed = 0.0;
@@ -0,0 +1,219 @@
use std::time::Instant;
use uom::si::{
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{Angle, AngularAcceleration, AngularVelocity},
};
use super::acceleration_position_controller::{
AccelerationPositionController, MotionControllerError,
};
/// Angular Acceleration Position Controller with proper physical units
#[derive(Debug)]
pub struct AngularAccelerationPositionController {
controller: AccelerationPositionController,
last_update: Option<Instant>,
}
impl AngularAccelerationPositionController {
/// Create a new angular position controller with acceleration limits
pub fn new(
min_position: Option<Angle>,
max_position: Option<Angle>,
min_speed: AngularVelocity,
max_speed: AngularVelocity,
min_acceleration: AngularAcceleration,
max_acceleration: AngularAcceleration,
) -> Self {
Self {
controller: AccelerationPositionController::new(
min_speed.get::<radian_per_second>(), // min_speed
max_speed.get::<radian_per_second>(), // max_speed
min_acceleration.get::<radian_per_second_squared>(), // min_acceleration
max_acceleration.get::<radian_per_second_squared>(), // max_acceleration
min_position.map(|angle| angle.get::<radian>()), // min_position
max_position.map(|angle| angle.get::<radian>()), // max_position
1e-6, // position_tolerance
1e-6, // speed_tolerance
)
.expect("Failed to create AccelerationPositionController"),
last_update: None,
}
}
/// Create a new angular position controller with simple symmetric limits
///
/// This is a convenience constructor that creates symmetric limits:
/// - Position limits: [-position, +position] (if position is Some)
/// - Speed limits: [-speed, +speed]
/// - Acceleration limits: [-acceleration, +acceleration]
/// - Default tolerances: 1e-6 for both position and speed
///
/// # Parameters (ordered: position, speed, acceleration)
/// - `position`: Optional maximum position magnitude (None for no limits, Some(x) creates [-x, +x])
/// - `speed`: Maximum angular velocity magnitude (creates limits [-speed, +speed])
/// - `acceleration`: Maximum angular acceleration magnitude (creates limits [-acceleration, +acceleration])
pub fn new_simple(
position: Option<Angle>,
speed: AngularVelocity,
acceleration: AngularAcceleration,
) -> Self {
Self {
controller: AccelerationPositionController::new_simple(
position.map(|angle| angle.get::<radian>()),
speed.get::<radian_per_second>(),
acceleration.get::<radian_per_second_squared>(),
)
.expect("Failed to create AccelerationPositionController"),
last_update: None,
}
}
/// Update the controller with a new target angle
pub fn update(&mut self, target_angle: Angle, t: Instant) -> Angle {
// Calculate dt from the last update
let dt = if let Some(last_t) = self.last_update {
t.duration_since(last_t).as_secs_f64()
} else {
0.0 // First update, no time has passed
};
self.last_update = Some(t);
// Update controller with raw angle values
let result = self
.controller
.update(target_angle.get::<radian>(), dt)
.expect("Failed to update AccelerationPositionController");
Angle::new::<radian>(result)
}
/// Get the current angle position
pub fn get_position(&self) -> Angle {
Angle::new::<radian>(self.controller.get_position())
}
/// Get the target angle
pub fn get_target_position(&self) -> Angle {
Angle::new::<radian>(self.controller.get_target_position())
}
/// Get the minimum angle position limit
pub fn get_min_position(&self) -> Option<Angle> {
self.controller.get_min_position().map(Angle::new::<radian>)
}
/// Get the maximum angle position limit
pub fn get_max_position(&self) -> Option<Angle> {
self.controller.get_max_position().map(Angle::new::<radian>)
}
/// Set the minimum angle position limit
pub fn set_min_position(
&mut self,
min_position: Option<Angle>,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_position(min_position.map(|angle| angle.get::<radian>()))
}
/// Set the maximum angle position limit
pub fn set_max_position(
&mut self,
max_position: Option<Angle>,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_position(max_position.map(|angle| angle.get::<radian>()))
}
/// Get the current angular velocity
pub fn get_speed(&self) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(self.controller.get_speed())
}
/// Set the minimum angular velocity
pub fn set_min_speed(
&mut self,
min_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_speed(min_speed.get::<radian_per_second>())
}
/// Set the maximum angular velocity
pub fn set_max_speed(
&mut self,
max_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_speed(max_speed.get::<radian_per_second>())
}
/// Get the current angular acceleration
pub fn get_acceleration(&self) -> AngularAcceleration {
AngularAcceleration::new::<radian_per_second_squared>(self.controller.get_acceleration())
}
/// Set the minimum angular acceleration
pub fn set_min_acceleration(
&mut self,
min_acceleration: AngularAcceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_acceleration(min_acceleration.get::<radian_per_second_squared>())
}
/// Set the maximum angular acceleration
pub fn set_max_acceleration(
&mut self,
max_acceleration: AngularAcceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_acceleration(max_acceleration.get::<radian_per_second_squared>())
}
/// Resets the controller to a specific angular position
///
/// This method completely reinitializes the controller state, setting:
/// - Current angular position to the specified value
/// - Target angular position to the specified value (stops any ongoing motion)
/// - Current angular velocity to 0
/// - Current angular acceleration to 0
/// - Motion phase to Idle
/// - Clears any motion planning state and timing information
///
/// Use this method when you need to teleport the controlled object to a new angular position
/// or when recovering from errors/emergency stops.
///
/// # Arguments
/// * `position` - The new angular position to reset to
///
/// # Returns
/// Returns `Ok(())` on success, or `Err(MotionControllerError)` if the position is outside configured limits.
///
/// # Errors
/// - `InvalidPositionLimits`: If the specified position is outside the configured min/max position limits
///
/// # Example
/// ```ignore
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
/// use uom::si::f64::Angle;
/// use uom::si::angle::degree;
///
/// let mut controller = AngularAccelerationPositionController::new_simple(
/// Some(Angle::new::<degree>(180.0)),
/// // ... other parameters
/// )?;
///
/// // Reset to 45 degrees
/// let new_position = Angle::new::<degree>(45.0);
/// controller.reset(new_position)?;
/// ```
pub fn reset(&mut self, position: Angle) -> Result<(), MotionControllerError> {
self.controller.reset(position.get::<radian>())?;
self.last_update = None;
Ok(())
}
}
@@ -0,0 +1,327 @@
use std::time::Instant;
use uom::si::{
angular_acceleration::radian_per_second_squared,
angular_jerk::radian_per_second_cubed,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularJerk, AngularVelocity},
};
use super::acceleration_position_controller::MotionControllerError;
use super::jerk_speed_controller::JerkSpeedController;
/// Angular Jerk Speed Controller with proper physical units
///
/// This controller provides angular motion control with proper SI units for rotational systems.
/// It wraps the core JerkSpeedController and provides unit-typed interfaces for angular velocity
/// (AngularVelocity), angular acceleration (AngularAcceleration), and angular jerk (AngularJerk).
///
/// The controller manages smooth rotational speed profiles with configurable limits and tolerances,
/// ensuring that angular velocity, acceleration, and jerk constraints are respected during motion.
/// This is particularly useful for motor control, servo systems, and any rotational machinery
/// requiring smooth speed transitions.
///
/// # Example
/// ```ignore
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
/// };
///
/// let max_speed = AngularVelocity::new::<revolution_per_minute>(3000.0);
/// let max_accel = AngularAcceleration::new::<revolution_per_minute_per_second>(1000.0);
/// let max_jerk = AngularJerk::new::<revolution_per_minute_per_second_squared>(500.0);
///
/// let controller = AngularJerkSpeedController::new_simple(
/// Some(max_speed),
/// max_accel,
/// max_jerk,
/// );
/// ```
#[derive(Debug)]
pub struct AngularJerkSpeedController {
controller: JerkSpeedController,
last_update: Option<Instant>,
}
impl AngularJerkSpeedController {
/// Create a new angular speed controller with jerk limits
///
/// This constructor allows full control over all angular velocity, acceleration, and jerk limits.
/// Use this when you need asymmetric limits or fine-grained control over the rotational motion profile.
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `min_speed` - Optional minimum allowed angular velocity (None for no limit)
/// * `max_speed` - Optional maximum allowed angular velocity (None for no limit)
/// * `min_acceleration` - Minimum angular acceleration (typically negative for deceleration)
/// * `max_acceleration` - Maximum angular acceleration (typically positive)
/// * `min_jerk` - Minimum angular jerk for decreasing acceleration (typically negative)
/// * `max_jerk` - Maximum angular jerk for increasing acceleration (typically positive)
///
/// # Returns
/// A new AngularJerkSpeedController instance
///
/// # Example
/// ```ignore
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
/// };
///
/// // Create controller with asymmetric limits
/// let controller = AngularJerkSpeedController::new(
/// Some(AngularVelocity::new::<revolution_per_minute>(-1000.0)), // Min: -1000 RPM
/// Some(AngularVelocity::new::<revolution_per_minute>(3000.0)), // Max: +3000 RPM
/// AngularAcceleration::new::<revolution_per_minute_per_second>(-800.0), // Min accel
/// AngularAcceleration::new::<revolution_per_minute_per_second>(1200.0), // Max accel
/// AngularJerk::new::<revolution_per_minute_per_second_squared>(-400.0), // Min jerk
/// AngularJerk::new::<revolution_per_minute_per_second_squared>(600.0), // Max jerk
/// );
/// ```
pub fn new(
min_speed: Option<AngularVelocity>,
max_speed: Option<AngularVelocity>,
min_acceleration: AngularAcceleration,
max_acceleration: AngularAcceleration,
min_jerk: AngularJerk,
max_jerk: AngularJerk,
) -> Self {
Self {
controller: JerkSpeedController::new(
min_speed.map(|speed| speed.get::<radian_per_second>()),
max_speed.map(|speed| speed.get::<radian_per_second>()),
min_acceleration.get::<radian_per_second_squared>(),
max_acceleration.get::<radian_per_second_squared>(),
min_jerk.get::<radian_per_second_cubed>(),
max_jerk.get::<radian_per_second_cubed>(),
),
last_update: None,
}
}
/// Create a new simple angular jerk speed controller with symmetric limits
///
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
/// This is the most common use case and simplifies controller setup for typical rotational applications.
///
/// The created limits are:
/// - Angular velocity limits: [-speed, +speed] (if speed is Some), no limits if None
/// - Angular acceleration limits: [-acceleration, +acceleration]
/// - Angular jerk limits: [-jerk, +jerk]
/// - Default tolerances: 1e-6 for both position and speed
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `speed` - Optional maximum angular velocity magnitude (None for no limits, Some(x) creates [-x, +x])
/// * `acceleration` - Maximum angular acceleration magnitude (creates limits [-acceleration, +acceleration])
/// * `jerk` - Maximum angular jerk magnitude (creates limits [-jerk, +jerk])
///
/// # Returns
/// A new AngularJerkSpeedController instance
///
/// # Panics
/// Panics if acceleration or jerk values are negative or zero
///
/// # Example
/// ```ignore
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
/// };
///
/// // Create controller with symmetric limits
/// let max_speed = AngularVelocity::new::<revolution_per_minute>(2000.0);
/// let max_accel = AngularAcceleration::new::<revolution_per_minute_per_second>(1000.0);
/// let max_jerk = AngularJerk::new::<revolution_per_minute_per_second_squared>(500.0);
///
/// let controller = AngularJerkSpeedController::new_simple(
/// Some(max_speed), // Angular velocity limits: [-2000, +2000] RPM
/// max_accel, // Angular acceleration limits: [-1000, +1000] RPM/s
/// max_jerk, // Angular jerk limits: [-500, +500] RPM/s²
/// );
///
/// // Create controller with no speed limits
/// let unlimited_controller = AngularJerkSpeedController::new_simple(
/// None, // No angular velocity limits
/// max_accel, // Angular acceleration limits: [-1000, +1000] RPM/s
/// max_jerk, // Angular jerk limits: [-500, +500] RPM/s²
/// );
/// ```
pub fn new_simple(
speed: Option<AngularVelocity>,
acceleration: AngularAcceleration,
jerk: AngularJerk,
) -> Self {
let controller = JerkSpeedController::new_simple(
speed.map(|s| s.get::<radian_per_second>()),
acceleration.get::<radian_per_second_squared>(),
jerk.get::<radian_per_second_cubed>(),
);
Self {
controller,
last_update: None,
}
}
/// Update the controller with a new target angular velocity
///
/// Calculates the next angular velocity in the motion profile based on the target and elapsed time.
/// The controller automatically manages angular acceleration and jerk to reach the target angular
/// velocity while respecting all configured limits.
///
/// # Parameters
/// * `target_speed` - The desired target angular velocity
/// * `t` - Current timestamp for calculating time delta
///
/// # Returns
/// The current angular velocity after applying the motion profile for this time step
///
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
///
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
///
/// let current_speed = controller.update(target, Instant::now());
/// ```
pub fn update(&mut self, target_speed: AngularVelocity, t: Instant) -> AngularVelocity {
// Calculate dt from the last update
let dt = if let Some(last_t) = self.last_update {
t.duration_since(last_t).as_secs_f64()
} else {
0.0 // First update, no time has passed
};
self.last_update = Some(t);
// Convert target to raw value, update controller, and convert result back to AngularVelocity
let target_raw = target_speed.get::<radian_per_second>();
let speed_raw = self.controller.update(target_raw, dt);
AngularVelocity::new::<radian_per_second>(speed_raw)
}
/// Get the current angular velocity
///
/// Returns the controller's current angular velocity in the motion profile.
///
/// # Returns
/// Current angular velocity as an AngularVelocity value
pub fn get_speed(&self) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(self.controller.get_speed())
}
/// Get the target angular velocity
///
/// Returns the current target angular velocity that the controller is trying to reach.
///
/// # Returns
/// Target angular velocity as an AngularVelocity value
pub fn get_target_speed(&self) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(self.controller.get_target_speed())
}
/// Get the minimum angular velocity limit
///
/// Returns the minimum angular velocity constraint, if one is set.
///
/// # Returns
/// Minimum angular velocity limit as an Option<AngularVelocity>, None if no limit is set
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_min_speed()
.map(AngularVelocity::new::<radian_per_second>)
}
/// Get the maximum angular velocity limit
///
/// Returns the maximum angular velocity constraint, if one is set.
///
/// # Returns
/// Maximum angular velocity limit as an Option<AngularVelocity>, None if no limit is set
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_max_speed()
.map(AngularVelocity::new::<radian_per_second>)
}
/// Set the minimum angular velocity limit
pub fn set_min_speed(
&mut self,
min_speed: Option<AngularVelocity>,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_speed(min_speed.map(|speed| speed.get::<radian_per_second>()))
}
/// Set the maximum angular velocity limit
pub fn set_max_speed(
&mut self,
max_speed: Option<AngularVelocity>,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_speed(max_speed.map(|speed| speed.get::<radian_per_second>()))
}
/// Get the current angular acceleration
pub fn get_acceleration(&self) -> AngularAcceleration {
AngularAcceleration::new::<radian_per_second_squared>(self.controller.get_acceleration())
}
/// Set the minimum angular acceleration
pub fn set_min_acceleration(
&mut self,
min_acceleration: AngularAcceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_acceleration(min_acceleration.get::<radian_per_second_squared>())
}
/// Set the maximum angular acceleration
pub fn set_max_acceleration(
&mut self,
max_acceleration: AngularAcceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_acceleration(max_acceleration.get::<radian_per_second_squared>())
}
/// Get the current angular jerk
pub fn get_jerk(&self) -> AngularJerk {
AngularJerk::new::<radian_per_second_cubed>(self.controller.get_jerk())
}
/// Set the minimum angular jerk
pub fn set_min_jerk(&mut self, min_jerk: AngularJerk) -> Result<(), MotionControllerError> {
self.controller
.set_min_jerk(min_jerk.get::<radian_per_second_cubed>())
}
/// Set the maximum angular jerk
pub fn set_max_jerk(&mut self, max_jerk: AngularJerk) -> Result<(), MotionControllerError> {
self.controller
.set_max_jerk(max_jerk.get::<radian_per_second_cubed>())
}
/// Reset the controller to a new angular velocity and acceleration
///
/// This resets all internal state including:
/// - Current angular velocity to the provided value
/// - Current angular acceleration to 0
/// - Current angular jerk to 0
/// - Target angular velocity to the current velocity
///
/// # Parameters
/// - `velocity`: The new current angular velocity
pub fn reset(&mut self, velocity: AngularVelocity) -> Result<(), MotionControllerError> {
self.controller.reset(velocity.get::<radian_per_second>())?;
self.last_update = None;
Ok(())
}
}
@@ -0,0 +1,378 @@
use super::acceleration_position_controller::{
AccelerationPositionController, MotionControllerError,
};
/// Controller for speed-based motion with jerk limits.
///
/// This controller manages motion profiles where speed is the primary controlled variable,
/// with constraints on acceleration (rate of change of speed) and jerk (rate of change of acceleration).
/// It wraps an AccelerationPositionController but remaps the parameters to represent one higher
/// degree of derivation in the motion hierarchy.
///
/// The control hierarchy is:
/// - **Speed**: The primary controlled variable (analogous to position in position control)
/// - **Acceleration**: First derivative of speed (analogous to velocity in position control)
/// - **Jerk**: Second derivative of speed, rate of change of acceleration (analogous to acceleration in position control)
///
/// This controller is particularly useful for applications where you need to control rotational
/// or linear speed with smooth acceleration profiles, such as motor control, conveyor systems,
/// or any application requiring smooth speed transitions.
///
/// # Example
/// ```ignore
/// // Create a controller for speed control with jerk limiting
/// let controller = JerkSpeedController::new_simple(
/// Some(100.0), // Max speed ±100 units/s
/// 50.0, // Max acceleration ±50 units/s²
/// 25.0, // Max jerk ±25 units/s³
/// );
///
/// // Update towards target speed
/// let current_speed = controller.update(80.0, 0.01); // Target 80 units/s, dt=10ms
/// ```
#[derive(Debug)]
pub struct JerkSpeedController {
base_controller: AccelerationPositionController,
}
impl JerkSpeedController {
/// Create a new speed controller with the given constraints
///
/// This constructor allows full control over all speed, acceleration, and jerk limits.
/// Use this when you need asymmetric limits or fine-grained control over the motion profile.
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `min_speed` - Optional minimum allowed speed (None for no limit)
/// * `max_speed` - Optional maximum allowed speed (None for no limit)
/// * `min_acceleration` - Minimum acceleration (typically negative for deceleration)
/// * `max_acceleration` - Maximum acceleration (typically positive)
/// * `min_jerk` - Minimum jerk for decreasing acceleration (typically negative)
/// * `max_jerk` - Maximum jerk for increasing acceleration (typically positive)
///
/// # Returns
/// A new JerkSpeedController instance
///
/// # Panics
/// Panics if the underlying AccelerationPositionController cannot be created with the given parameters
///
/// # Example
/// ```ignore
/// // Create controller with asymmetric limits
/// let controller = JerkSpeedController::new(
/// Some(-50.0), // Min speed: -50 units/s
/// Some(100.0), // Max speed: +100 units/s
/// -30.0, // Min acceleration: -30 units/s²
/// 40.0, // Max acceleration: +40 units/s²
/// -20.0, // Min jerk: -20 units/s³
/// 25.0, // Max jerk: +25 units/s³
/// );
/// ```
pub fn new(
min_speed: Option<f64>,
max_speed: Option<f64>,
min_acceleration: f64,
max_acceleration: f64,
min_jerk: f64,
max_jerk: f64,
) -> Self {
// Create the base controller with renamed parameters
let base_controller = AccelerationPositionController::new(
min_jerk, // min_speed in the base controller
max_jerk, // max_speed in the base controller
min_acceleration, // min_acceleration in the base controller
max_acceleration, // max_acceleration in the base controller
min_speed, // min_position in the base controller
max_speed, // max_position in the base controller
1e-6, // position_tolerance
1e-6, // speed_tolerance
)
.expect("Failed to create AccelerationPositionController");
Self { base_controller }
}
/// Create a new speed controller with simple symmetric limits
///
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
/// This is the most common use case and simplifies controller setup for typical applications.
///
/// The created limits are:
/// - Speed limits: [-speed, +speed] (if speed is Some), no limits if None
/// - Acceleration limits: [-acceleration, +acceleration]
/// - Jerk limits: [-jerk, +jerk]
/// - Default tolerances: 1e-6 for both position and speed
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `speed` - Optional maximum speed magnitude (None for no limits, Some(x) creates [-x, +x])
/// * `acceleration` - Maximum acceleration magnitude (creates limits [-acceleration, +acceleration])
/// * `jerk` - Maximum jerk magnitude (creates limits [-jerk, +jerk])
///
/// # Returns
/// A new JerkSpeedController instance
///
/// # Panics
/// Panics if acceleration or jerk values are negative or zero
///
/// # Example
/// ```ignore
/// // Create controller with symmetric limits
/// let controller = JerkSpeedController::new_simple(
/// Some(100.0), // Speed limits: [-100, +100] units/s
/// 50.0, // Acceleration limits: [-50, +50] units/s²
/// 25.0, // Jerk limits: [-25, +25] units/s³
/// );
///
/// // Create controller with no speed limits
/// let unlimited_controller = JerkSpeedController::new_simple(
/// None, // No speed limits
/// 30.0, // Acceleration limits: [-30, +30] units/s²
/// 15.0, // Jerk limits: [-15, +15] units/s³
/// );
/// ```
pub fn new_simple(speed: Option<f64>, acceleration: f64, jerk: f64) -> Self {
if acceleration <= 0.0 || jerk <= 0.0 {
panic!("Acceleration and jerk must be positive values");
}
Self::new(
speed.map(|s| -s), // min_speed
speed, // max_speed
-acceleration, // min_acceleration
acceleration, // max_acceleration
-jerk, // min_jerk
jerk, // max_jerk
)
}
/// Update the controller state based on the target speed and time step
///
/// Calculates the next speed in the motion profile based on the target speed and elapsed time.
/// The controller automatically manages acceleration and jerk to reach the target speed
/// while respecting all configured limits.
///
/// # Parameters
/// * `target_speed` - The desired target speed to reach
/// * `dt` - Time step in seconds since the last update
///
/// # Returns
/// The current speed after applying the motion profile for this time step
///
/// # Panics
/// Panics if the underlying controller update fails (should not happen in normal operation)
///
/// # Example
/// ```ignore
/// let mut controller = JerkSpeedController::new_simple(Some(100.0), 50.0, 25.0);
///
/// // Control loop
/// loop {
/// let current_speed = controller.update(target_speed, 0.01); // 10ms update cycle
/// // Use current_speed for your application
/// }
/// ```
pub fn update(&mut self, target_speed: f64, dt: f64) -> f64 {
// Call the base controller's update method with the target speed
self.base_controller
.update(target_speed, dt)
.expect("Failed to update controller")
}
/// Get the current speed
///
/// Returns the controller's current speed in the motion profile.
///
/// # Returns
/// Current speed as a floating-point value
pub const fn get_speed(&self) -> f64 {
self.base_controller.get_position()
}
/// Get the current acceleration
///
/// Returns the controller's current acceleration (rate of change of speed).
///
/// # Returns
/// Current acceleration as a floating-point value
pub const fn get_acceleration(&self) -> f64 {
self.base_controller.get_speed()
}
/// Get the current jerk
///
/// Returns the controller's current jerk (rate of change of acceleration).
///
/// # Returns
/// Current jerk as a floating-point value
pub const fn get_jerk(&self) -> f64 {
self.base_controller.get_acceleration()
}
/// Get the target speed
///
/// Returns the current target speed that the controller is trying to reach.
///
/// # Returns
/// Target speed as a floating-point value
pub const fn get_target_speed(&self) -> f64 {
self.base_controller.get_target_position()
}
/// Get the minimum speed limit
///
/// Returns the minimum speed constraint, if one is set.
///
/// # Returns
/// Minimum speed limit as an Option<f64>, None if no limit is set
pub const fn get_min_speed(&self) -> Option<f64> {
self.base_controller.get_min_position()
}
/// Get the maximum speed limit
///
/// Returns the maximum speed constraint, if one is set.
///
/// # Returns
/// Maximum speed limit as an Option<f64>, None if no limit is set
pub const fn get_max_speed(&self) -> Option<f64> {
self.base_controller.get_max_position()
}
/// Set the minimum speed limit
///
/// Updates the minimum speed constraint for the controller.
///
/// # Parameters
/// * `min_speed` - New minimum speed limit (None to remove limit)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum speed limit)
pub fn set_min_speed(&mut self, min_speed: Option<f64>) -> Result<(), MotionControllerError> {
self.base_controller.set_min_position(min_speed)
}
/// Set the maximum speed limit
///
/// Updates the maximum speed constraint for the controller.
///
/// # Parameters
/// * `max_speed` - New maximum speed limit (None to remove limit)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum speed limit)
pub fn set_max_speed(&mut self, max_speed: Option<f64>) -> Result<(), MotionControllerError> {
self.base_controller.set_max_position(max_speed)
}
/// Set the minimum acceleration
///
/// Updates the minimum acceleration constraint for the controller.
///
/// # Parameters
/// * `min_acceleration` - New minimum acceleration limit (typically negative for deceleration)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum acceleration limit)
pub fn set_min_acceleration(
&mut self,
min_acceleration: f64,
) -> Result<(), MotionControllerError> {
self.base_controller.set_min_speed(min_acceleration)
}
/// Set the maximum acceleration
///
/// Updates the maximum acceleration constraint for the controller.
///
/// # Parameters
/// * `max_acceleration` - New maximum acceleration limit (typically positive)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum acceleration limit or negative/zero value)
pub fn set_max_acceleration(
&mut self,
max_acceleration: f64,
) -> Result<(), MotionControllerError> {
self.base_controller.set_max_speed(max_acceleration)
}
/// Set the minimum jerk
///
/// Updates the minimum jerk constraint for the controller.
///
/// # Parameters
/// * `min_jerk` - New minimum jerk limit (typically negative for decreasing acceleration)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum jerk limit)
pub fn set_min_jerk(&mut self, min_jerk: f64) -> Result<(), MotionControllerError> {
self.base_controller.set_min_acceleration(min_jerk)
}
/// Set the maximum jerk
///
/// Updates the maximum jerk constraint for the controller.
///
/// # Parameters
/// * `max_jerk` - New maximum jerk limit (typically positive for increasing acceleration)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum jerk limit or negative/zero value)
pub fn set_max_jerk(&mut self, max_jerk: f64) -> Result<(), MotionControllerError> {
self.base_controller.set_max_acceleration(max_jerk)
}
/// Reset the controller to a new speed and acceleration
///
/// This resets all internal state of the controller to start fresh from a new state.
/// The controller will reset its speed, clear any accumulated motion profile state,
/// and set the target speed to the current speed.
///
/// # Parameters
/// * `speed` - The new current speed to reset to
///
/// # Returns
/// Result indicating success or failure of the reset operation
///
/// # Errors
/// Returns MotionControllerError if the speed is outside the configured limits
///
/// # Example
/// ```ignore
/// let mut controller = JerkSpeedController::new_simple(Some(100.0), 50.0, 25.0);
///
/// // Reset to zero speed with zero acceleration
/// controller.reset(0.0)?;
///
/// // Reset to specific speed
/// controller.reset(25.0)?;
/// ```
pub fn reset(&mut self, speed: f64) -> Result<(), MotionControllerError> {
// Reset to the speed position (treating speed as position in the base controller)
self.base_controller.reset(speed)?;
Ok(())
}
}
@@ -0,0 +1,389 @@
use std::time::Instant;
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
use super::acceleration_position_controller::{
AccelerationPositionController, MotionControllerError,
};
/// Linear Acceleration Position Controller with proper physical units
///
/// This controller provides linear motion control with proper SI units (meters, m/s, m/s²).
/// It wraps the core AccelerationPositionController and provides unit-typed interfaces
/// for position (Length), velocity (Velocity), and acceleration (Acceleration).
///
/// The controller manages smooth motion profiles with configurable limits and tolerances,
/// ensuring that position, speed, and acceleration constraints are respected during motion.
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(10.0);
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
/// let max_acceleration = Acceleration::new::<meter_per_second_squared>(5.0);
///
/// let controller = LinearAccelerationPositionController::new_simple(
/// Some(position_limit),
/// max_speed,
/// max_acceleration,
/// ).unwrap();
/// ```
#[derive(Debug)]
pub struct LinearAccelerationPositionController {
controller: AccelerationPositionController,
last_update: Option<Instant>,
}
impl LinearAccelerationPositionController {
/// Create a new linear position controller with acceleration limits
///
/// # Parameters
/// * `min_position` - Minimum position limit (optional)
/// * `max_position` - Maximum position limit (optional)
/// * `min_speed` - Minimum speed limit (typically negative)
/// * `max_speed` - Maximum speed limit (typically positive)
/// * `min_acceleration` - Minimum acceleration limit (typically negative for deceleration)
/// * `max_acceleration` - Maximum acceleration limit (typically positive)
///
/// # Returns
/// A new LinearAccelerationPositionController instance
///
/// # Panics
/// Panics if the underlying AccelerationPositionController cannot be created with the given parameters
pub fn new(
min_position: Option<Length>,
max_position: Option<Length>,
min_speed: Velocity,
max_speed: Velocity,
min_acceleration: Acceleration,
max_acceleration: Acceleration,
) -> Self {
Self {
controller: AccelerationPositionController::new(
min_speed.get::<meter_per_second>(),
max_speed.get::<meter_per_second>(),
min_acceleration.get::<meter_per_second_squared>(),
max_acceleration.get::<meter_per_second_squared>(),
min_position.map(|length| length.get::<meter>()),
max_position.map(|length| length.get::<meter>()),
1e-6, // position_tolerance
1e-6, // speed_tolerance
)
.expect("Failed to create AccelerationPositionController"),
last_update: None,
}
}
/// Create a new simple linear acceleration position controller with symmetric limits
///
/// This is a convenience constructor that creates symmetric limits around zero for
/// position, speed, and acceleration. This is useful for most common use cases.
///
/// # Parameters
/// * `position` - Maximum position limit (creates [-position, +position] range if Some)
/// * `speed` - Maximum speed limit (creates [-speed, +speed] range)
/// * `acceleration` - Maximum acceleration limit (creates [-acceleration, +acceleration] range)
///
/// # Returns
/// A Result containing the new controller or a MotionControllerError
///
/// # Errors
/// Returns MotionControllerError if:
/// - Speed or acceleration values are negative or zero
/// - The underlying controller cannot be created with the calculated limits
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0); // Creates [-2.0, +2.0] m/s² range
///
/// let controller = LinearAccelerationPositionController::new_simple(
/// Some(position_limit),
/// max_speed,
/// max_accel,
/// )?;
/// ```
pub fn new_simple(
position: Option<Length>,
speed: Velocity,
acceleration: Acceleration,
) -> Result<Self, MotionControllerError> {
let controller = AccelerationPositionController::new_simple(
position.map(|p| p.get::<meter>()),
speed.get::<meter_per_second>(),
acceleration.get::<meter_per_second_squared>(),
)?;
Ok(Self {
controller,
last_update: None,
})
}
/// Update the controller with a new target position
///
/// Calculates the next position in the motion profile based on the target position
/// and elapsed time since the last update. The controller automatically manages
/// acceleration and velocity to reach the target while respecting all limits.
///
/// # Parameters
/// * `target_position` - The desired target position
/// * `t` - Current timestamp for calculating time delta
///
/// # Returns
/// The current position after applying the motion profile for this time step
///
/// # Panics
/// Panics if the underlying controller update fails (should not happen in normal operation)
///
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{length::meter, f64::Length};
///
/// let mut controller = LinearAccelerationPositionController::new_simple(
/// Some(Length::new::<meter>(10.0)),
/// // ... other parameters
/// )?;
///
/// let target = Length::new::<meter>(5.0);
/// let current_position = controller.update(target, Instant::now());
/// ```
pub fn update(&mut self, target_position: Length, t: Instant) -> Length {
// Calculate dt from the last update
let dt = if let Some(last_t) = self.last_update {
t.duration_since(last_t).as_secs_f64()
} else {
0.0 // First update, no time has passed
};
self.last_update = Some(t);
// Convert target to raw value, update controller, and convert result back to Length
let target_raw = target_position.get::<meter>();
let position_raw = self
.controller
.update(target_raw, dt)
.expect("Failed to update controller");
Length::new::<meter>(position_raw)
}
/// Get the current position
///
/// Returns the controller's current position in the motion profile.
///
/// # Returns
/// Current position as a Length value
pub fn get_position(&self) -> Length {
Length::new::<meter>(self.controller.get_position())
}
/// Get the target position
///
/// Returns the current target position that the controller is trying to reach.
///
/// # Returns
/// Target position as a Length value
pub fn get_target_position(&self) -> Length {
Length::new::<meter>(self.controller.get_target_position())
}
/// Get the minimum position limit
///
/// Returns the minimum position constraint, if one is set.
///
/// # Returns
/// Minimum position limit as an Option<Length>, None if no limit is set
pub fn get_min_position(&self) -> Option<Length> {
self.controller.get_min_position().map(Length::new::<meter>)
}
/// Get the maximum position limit
///
/// Returns the maximum position constraint, if one is set.
///
/// # Returns
/// Maximum position limit as an Option<Length>, None if no limit is set
pub fn get_max_position(&self) -> Option<Length> {
self.controller.get_max_position().map(Length::new::<meter>)
}
/// Set the minimum position limit
///
/// Updates the minimum position constraint for the controller.
///
/// # Parameters
/// * `min_position` - New minimum position limit (None to remove limit)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum position limit)
pub fn set_min_position(
&mut self,
min_position: Option<Length>,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_position(min_position.map(|length| length.get::<meter>()))
}
/// Set the maximum position limit
///
/// Updates the maximum position constraint for the controller.
///
/// # Parameters
/// * `max_position` - New maximum position limit (None to remove limit)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum position limit)
pub fn set_max_position(
&mut self,
max_position: Option<Length>,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_position(max_position.map(|length| length.get::<meter>()))
}
/// Get the current speed
///
/// Returns the controller's current velocity in the motion profile.
///
/// # Returns
/// Current velocity as a Velocity value
pub fn get_speed(&self) -> Velocity {
Velocity::new::<meter_per_second>(self.controller.get_speed())
}
/// Set the minimum speed
///
/// Updates the minimum velocity constraint for the controller.
///
/// # Parameters
/// * `min_speed` - New minimum speed limit (typically negative for reverse motion)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum speed limit)
pub fn set_min_speed(&mut self, min_speed: Velocity) -> Result<(), MotionControllerError> {
self.controller
.set_min_speed(min_speed.get::<meter_per_second>())
}
/// Set the maximum speed
///
/// Updates the maximum velocity constraint for the controller.
///
/// # Parameters
/// * `max_speed` - New maximum speed limit (typically positive)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum speed limit or negative/zero value)
pub fn set_max_speed(&mut self, max_speed: Velocity) -> Result<(), MotionControllerError> {
self.controller
.set_max_speed(max_speed.get::<meter_per_second>())
}
/// Get the current acceleration
///
/// Returns the controller's current acceleration in the motion profile.
///
/// # Returns
/// Current acceleration as an Acceleration value
pub fn get_acceleration(&self) -> Acceleration {
Acceleration::new::<meter_per_second_squared>(self.controller.get_acceleration())
}
/// Set the minimum acceleration
///
/// Updates the minimum acceleration constraint for the controller.
///
/// # Parameters
/// * `min_acceleration` - New minimum acceleration limit (typically negative for deceleration)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., greater than the maximum acceleration limit)
pub fn set_min_acceleration(
&mut self,
min_acceleration: Acceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_acceleration(min_acceleration.get::<meter_per_second_squared>())
}
/// Set the maximum acceleration
///
/// Updates the maximum acceleration constraint for the controller.
///
/// # Parameters
/// * `max_acceleration` - New maximum acceleration limit (typically positive)
///
/// # Returns
/// Result indicating success or failure of the operation
///
/// # Errors
/// Returns MotionControllerError if the new limit would be invalid
/// (e.g., less than the minimum acceleration limit or negative/zero value)
pub fn set_max_acceleration(
&mut self,
max_acceleration: Acceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_acceleration(max_acceleration.get::<meter_per_second_squared>())
}
/// Reset the controller to a new position and velocity
///
/// This resets all internal state of the controller to start fresh from a new state.
/// The controller will reset its position, clear any accumulated motion profile state,
/// and set the target position to the current position.
///
/// # Parameters
/// * `position` - The new current position to reset to
///
/// # Returns
/// Result indicating success or failure of the reset operation
///
/// # Errors
/// Returns MotionControllerError if the position is outside the configured limits
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
///
/// let new_position = Length::new::<meter>(0.0);
///
/// controller.reset(new_position)?;
/// ```
pub fn reset(&mut self, position: Length) -> Result<(), MotionControllerError> {
self.controller.reset(position.get::<meter>())?;
self.last_update = None;
Ok(())
}
}
@@ -0,0 +1,281 @@
use std::time::Instant;
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Jerk, Velocity},
jerk::meter_per_second_cubed,
velocity::meter_per_second,
};
use super::acceleration_position_controller::MotionControllerError;
use super::jerk_speed_controller::JerkSpeedController;
/// Linear Jerk Speed Controller with proper physical units
///
/// This controller provides linear motion control with proper SI units for translational systems.
/// It wraps the core JerkSpeedController and provides unit-typed interfaces for velocity (Velocity),
/// acceleration (Acceleration), and jerk (Jerk).
///
/// The controller manages smooth linear speed profiles with configurable limits and tolerances,
/// ensuring that velocity, acceleration, and jerk constraints are respected during motion.
/// This is particularly useful for linear actuators, conveyor systems, CNC machines, and any
/// translational machinery requiring smooth speed transitions.
///
/// # Example
/// ```ignore
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
/// f64::{Velocity, Acceleration, Jerk}
/// };
///
/// let max_speed = Velocity::new::<meter_per_second>(5.0);
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0);
/// let max_jerk = Jerk::new::<meter_per_second_cubed>(1.0);
///
/// let controller = LinearJerkSpeedController::new_simple(
/// Some(max_speed),
/// max_accel,
/// max_jerk,
/// );
/// ```
#[derive(Debug)]
pub struct LinearJerkSpeedController {
controller: JerkSpeedController,
last_update: Option<Instant>,
}
impl LinearJerkSpeedController {
/// Create a new linear speed controller with jerk limits
///
/// This constructor allows full control over all velocity, acceleration, and jerk limits.
/// Use this when you need asymmetric limits or fine-grained control over the linear motion profile.
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `min_speed` - Optional minimum allowed velocity (None for no limit)
/// * `max_speed` - Optional maximum allowed velocity (None for no limit)
/// * `min_acceleration` - Minimum acceleration (typically negative for deceleration)
/// * `max_acceleration` - Maximum acceleration (typically positive)
/// * `min_jerk` - Minimum jerk for decreasing acceleration (typically negative)
/// * `max_jerk` - Maximum jerk for increasing acceleration (typically positive)
///
/// # Returns
/// A new LinearJerkSpeedController instance
///
/// # Example
/// ```ignore
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
/// f64::{Velocity, Acceleration, Jerk}
/// };
///
/// // Create controller with asymmetric limits
/// let controller = LinearJerkSpeedController::new(
/// Some(Velocity::new::<meter_per_second>(-2.0)), // Min: -2 m/s
/// Some(Velocity::new::<meter_per_second>(5.0)), // Max: +5 m/s
/// Acceleration::new::<meter_per_second_squared>(-3.0), // Min accel
/// Acceleration::new::<meter_per_second_squared>(4.0), // Max accel
/// Jerk::new::<meter_per_second_cubed>(-2.0), // Min jerk
/// Jerk::new::<meter_per_second_cubed>(3.0), // Max jerk
/// );
/// ```
pub fn new(
min_speed: Option<Velocity>,
max_speed: Option<Velocity>,
min_acceleration: Acceleration,
max_acceleration: Acceleration,
min_jerk: Jerk,
max_jerk: Jerk,
) -> Self {
Self {
controller: JerkSpeedController::new(
min_speed.map(|speed| speed.get::<meter_per_second>()),
max_speed.map(|speed| speed.get::<meter_per_second>()),
min_acceleration.get::<meter_per_second_squared>(),
max_acceleration.get::<meter_per_second_squared>(),
min_jerk.get::<meter_per_second_cubed>(),
max_jerk.get::<meter_per_second_cubed>(),
),
last_update: None,
}
}
/// Create a new simple linear jerk speed controller with symmetric limits
///
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
/// This is the most common use case and simplifies controller setup for typical linear applications.
///
/// The created limits are:
/// - Velocity limits: [-speed, +speed] (if speed is Some), no limits if None
/// - Acceleration limits: [-acceleration, +acceleration]
/// - Jerk limits: [-jerk, +jerk]
/// - Default tolerances: 1e-6 for both position and speed
///
/// # Parameters (ordered: speed, acceleration, jerk)
/// * `speed` - Optional maximum velocity magnitude (None for no limits, Some(x) creates [-x, +x])
/// * `acceleration` - Maximum acceleration magnitude (creates limits [-acceleration, +acceleration])
/// * `jerk` - Maximum jerk magnitude (creates limits [-jerk, +jerk])
///
/// # Returns
/// A new LinearJerkSpeedController instance
///
/// # Panics
/// Panics if acceleration or jerk values are negative or zero
///
/// # Example
/// ```ignore
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
/// f64::{Velocity, Acceleration, Jerk}
/// };
///
/// // Create controller with symmetric limits
/// let max_speed = Velocity::new::<meter_per_second>(3.0);
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0);
/// let max_jerk = Jerk::new::<meter_per_second_cubed>(1.5);
///
/// let controller = LinearJerkSpeedController::new_simple(
/// Some(max_speed), // Velocity limits: [-3, +3] m/s
/// max_accel, // Acceleration limits: [-2, +2] m/s²
/// max_jerk, // Jerk limits: [-1.5, +1.5] m/s³
/// );
///
/// // Create controller with no speed limits
/// let unlimited_controller = LinearJerkSpeedController::new_simple(
/// None, // No velocity limits
/// max_accel, // Acceleration limits: [-2, +2] m/s²
/// max_jerk, // Jerk limits: [-1.5, +1.5] m/s³
/// );
/// ```
pub fn new_simple(speed: Option<Velocity>, acceleration: Acceleration, jerk: Jerk) -> Self {
let controller = JerkSpeedController::new_simple(
speed.map(|s| s.get::<meter_per_second>()),
acceleration.get::<meter_per_second_squared>(),
jerk.get::<meter_per_second_cubed>(),
);
Self {
controller,
last_update: None,
}
}
/// Update the controller with a new target speed
pub fn update(&mut self, target_speed: Velocity, t: Instant) -> Velocity {
// Calculate dt from the last update
let dt = if let Some(last_t) = self.last_update {
t.duration_since(last_t).as_secs_f64()
} else {
0.0 // First update, no time has passed
};
self.last_update = Some(t);
// Convert target to raw value, update controller, and convert result back to Velocity
let target_raw = target_speed.get::<meter_per_second>();
let speed_raw = self.controller.update(target_raw, dt);
Velocity::new::<meter_per_second>(speed_raw)
}
/// Get the current speed
pub fn get_speed(&self) -> Velocity {
Velocity::new::<meter_per_second>(self.controller.get_speed())
}
/// Get the target speed
pub fn get_target_speed(&self) -> Velocity {
Velocity::new::<meter_per_second>(self.controller.get_target_speed())
}
/// Get the minimum velocity limit
pub fn get_min_speed(&self) -> Option<Velocity> {
self.controller
.get_min_speed()
.map(Velocity::new::<meter_per_second>)
}
/// Get the maximum velocity limit
pub fn get_max_speed(&self) -> Option<Velocity> {
self.controller
.get_max_speed()
.map(Velocity::new::<meter_per_second>)
}
/// Set the minimum velocity limit
pub fn set_min_speed(
&mut self,
min_speed: Option<Velocity>,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_speed(min_speed.map(|speed| speed.get::<meter_per_second>()))
}
/// Set the maximum velocity limit
pub fn set_max_speed(
&mut self,
max_speed: Option<Velocity>,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_speed(max_speed.map(|speed| speed.get::<meter_per_second>()))
}
/// Get the current acceleration
pub fn get_acceleration(&self) -> Acceleration {
Acceleration::new::<meter_per_second_squared>(self.controller.get_acceleration())
}
/// Set the minimum acceleration
pub fn set_min_acceleration(
&mut self,
min_acceleration: Acceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_min_acceleration(min_acceleration.get::<meter_per_second_squared>())
}
/// Set the maximum acceleration
pub fn set_max_acceleration(
&mut self,
max_acceleration: Acceleration,
) -> Result<(), MotionControllerError> {
self.controller
.set_max_acceleration(max_acceleration.get::<meter_per_second_squared>())
}
/// Get the current jerk
pub fn get_jerk(&self) -> Jerk {
Jerk::new::<meter_per_second_cubed>(self.controller.get_jerk())
}
/// Set the minimum jerk
pub fn set_min_jerk(&mut self, min_jerk: Jerk) -> Result<(), MotionControllerError> {
self.controller
.set_min_jerk(min_jerk.get::<meter_per_second_cubed>())
}
/// Set the maximum jerk
pub fn set_max_jerk(&mut self, max_jerk: Jerk) -> Result<(), MotionControllerError> {
self.controller
.set_max_jerk(max_jerk.get::<meter_per_second_cubed>())
}
/// Reset the controller to a new velocity and acceleration
///
/// This resets all internal state including:
/// - Current velocity to the provided value
/// - Current acceleration to 0
/// - Current jerk to 0
/// - Target velocity to the current velocity
///
/// # Parameters
/// - `velocity`: The new current velocity
pub fn reset(&mut self, velocity: Velocity) -> Result<(), MotionControllerError> {
self.controller.reset(velocity.get::<meter_per_second>())?;
self.last_update = None;
Ok(())
}
}
@@ -0,0 +1,6 @@
pub mod acceleration_position_controller;
pub mod angular_acceleration_position_controller;
pub mod angular_jerk_speed_controller;
pub mod jerk_speed_controller;
pub mod linear_acceleration_position_controller;
pub mod linear_jerk_speed_controller;
@@ -0,0 +1,297 @@
use uom::si::{angle::radian, f64::Angle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
pub flip_x: bool, // true to flip X-axis (mirror horizontally)
pub flip_y: bool, // true to flip Y-axis (mirror vertically)
pub is_cw: bool, // true for clockwise, false for counter-clockwise
}
impl AngleConverter {
/// Create a custom coordinate system
pub const fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
Self {
flip_x,
flip_y,
is_cw,
}
}
/// Standard mathematical coordinate system (CCW positive, 0° at positive X)
pub const fn mathematical() -> Self {
Self::new(false, false, false)
}
/// Screen/graphics coordinate system (CW positive, 0° at positive X, Y-flipped)
pub const fn screen() -> Self {
Self::new(false, true, false)
}
/// System where 0° points up, clockwise positive
pub const fn y_up_cw() -> Self {
Self::new(false, false, true)
}
/// System where 0° points down, counter-clockwise positive
pub const fn y_down_ccw() -> Self {
Self::new(false, true, true)
}
/// System where 0° points left, clockwise positive
pub const fn x_left_cw() -> Self {
Self::new(true, false, false)
}
/// System where 0° points right, counter-clockwise positive (same as mathematical)
pub const fn x_right_ccw() -> Self {
Self::mathematical()
}
/// System where 0° points up, counter-clockwise positive
pub const fn y_up_ccw() -> Self {
Self::new(false, false, false)
}
/// System where 0° points down, clockwise positive
pub const fn y_down_cw() -> Self {
Self::new(false, true, false)
}
/// System where 0° points left, counter-clockwise positive
pub const fn x_left_ccw() -> Self {
Self::new(true, false, true)
}
/// Convert angle from mathematical system to this system (f32)
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(math_angle_degrees);
let mut result = normalized_input;
// Apply axis flips
match (self.flip_x, self.flip_y) {
(false, false) => {} // No change
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
(false, true) => result = -result, // Y-flip: mirror across X-axis
(true, true) => result += 180.0, // Both flips: 180° rotation
}
// Handle rotation direction
if self.is_cw {
result = -result;
}
self.normalize_angle(result)
}
/// Convert angle from this system to mathematical system (f32)
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(system_angle_degrees);
let mut result = normalized_input;
// Handle rotation direction
if self.is_cw {
result = -result;
}
// Apply inverse axis flips
match (self.flip_x, self.flip_y) {
(false, false) => {} // No change
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
(false, true) => result = -result, // Y-flip: mirror across X-axis
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
}
self.normalize_angle(result)
}
/// Convert angle in radians from mathematical system to this system (f32)
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode(degrees);
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f32)
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode(degrees);
converted_degrees.to_radians()
}
/// Convert angle from mathematical system to this system (f64)
pub fn degrees_encode_f64(&self, math_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle_f64(math_angle_degrees);
let mut result = normalized_input;
// Apply axis flips
match (self.flip_x, self.flip_y) {
(false, false) => {} // No change
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
(false, true) => result = -result, // Y-flip: mirror across X-axis
(true, true) => result += 180.0, // Both flips: 180° rotation
}
// Handle rotation direction
if self.is_cw {
result = -result;
}
self.normalize_angle_f64(result)
}
/// Convert angle from this system to mathematical system (f64)
pub fn degrees_decode_f64(&self, system_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle_f64(system_angle_degrees);
let mut result = normalized_input;
// Handle rotation direction
if self.is_cw {
result = -result;
}
// Apply inverse axis flips
match (self.flip_x, self.flip_y) {
(false, false) => {} // No change
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
(false, true) => result = -result, // Y-flip: mirror across X-axis
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
}
self.normalize_angle_f64(result)
}
/// Convert angle in radians from mathematical system to this system (f64)
pub fn radians_encode_f64(&self, math_angle_radians: f64) -> f64 {
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode_f64(degrees);
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f64)
pub fn radians_decode_f64(&self, system_angle_radians: f64) -> f64 {
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode_f64(degrees);
converted_degrees.to_radians()
}
/// Normalize angle to [0, 360) range (f32)
fn normalize_angle(&self, angle: f64) -> f64 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
}
normalized
}
/// Normalize angle to [0, 360) range (f64)
fn normalize_angle_f64(&self, angle: f64) -> f64 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
}
normalized
}
}
#[derive(Debug, Clone)]
pub struct AngleConverterUom {
angle_converter: AngleConverter,
}
impl AngleConverterUom {
pub const fn new(angle_converter: AngleConverter) -> Self {
Self { angle_converter }
}
pub fn encode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
}
pub fn decode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
}
}
#[cfg(test)]
mod tests {
use std::f32::consts::PI;
use super::*;
#[test]
fn test_constructors() {
let math = AngleConverter::mathematical();
assert_eq!(math.flip_x, false);
assert_eq!(math.flip_y, false);
assert_eq!(math.is_cw, false);
let screen = AngleConverter::screen();
assert_eq!(screen.flip_x, false);
assert_eq!(screen.flip_y, true);
assert_eq!(screen.is_cw, false);
}
#[test]
fn test_screen_converter_bidirectional() {
let converter = AngleConverter::screen();
// Math to screen (Y-flip only)
assert_eq!(converter.degrees_encode(0.0), 0.0); // Right stays right
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
assert_eq!(converter.degrees_encode(180.0), 180.0); // Left stays left
// Screen to math (should be inverse)
assert_eq!(converter.degrees_decode(0.0), 0.0); // Right stays right
assert_eq!(converter.degrees_decode(270.0), 90.0); // Down becomes up
assert_eq!(converter.degrees_decode(90.0), 270.0); // Up becomes down
assert_eq!(converter.degrees_decode(180.0), 180.0); // Left stays left
}
#[test]
fn test_x_flip_converter() {
let converter = AngleConverter::x_left_cw();
// Math to x-flipped system
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
assert_eq!(converter.degrees_encode(90.0), 90.0); // Up stays up
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
assert_eq!(converter.degrees_encode(270.0), 270.0); // Down stays down
}
#[test]
fn test_both_flips_converter() {
let converter = AngleConverter::new(true, true, false);
// Math to both-flipped system (180° rotation)
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
}
#[test]
fn test_radians_conversion() {
let converter = AngleConverter::screen();
// Test radians_encode
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
assert!((result - expected).abs() < 1e-6);
}
#[test]
fn test_roundtrip_conversion() {
let converter = AngleConverter::new(true, false, true); // X-flip + CW
let original = 75.0_f64;
let converted = converter.degrees_encode(original);
let roundtrip = converter.degrees_decode(converted);
assert!((original - roundtrip as f64).abs() < 1e-6);
}
}
@@ -5,51 +5,78 @@ use uom::si::{
f64::{Angle, AngularAcceleration, AngularVelocity},
};
/// Converts between motor steps and angular measurements
///
/// This converter handles the transformation between motor steps and angular
/// measurements (angle, angular velocity, and angular acceleration) based on
/// the number of steps required for a full revolution.
#[derive(Debug, Clone)]
pub struct StepConverter {
steps_per_revolution: i16,
pub struct AngularStepConverter {
/// Number of motor steps required for one complete revolution
pub steps_per_revolution: i16,
}
impl StepConverter {
pub fn new(steps_per_revolution: i16) -> Self {
impl AngularStepConverter {
/// Create a new converter with the specified steps per revolution
pub const fn new(steps_per_revolution: i16) -> Self {
Self {
steps_per_revolution,
}
}
/// Convert steps to angle
///
/// Formula: angle (in revolutions) = steps / steps_per_revolution
pub fn steps_to_angle(&self, steps: f64) -> Angle {
let revolutions = steps / self.steps_per_revolution as f64;
Angle::new::<revolution>(revolutions)
}
/// Convert angle to steps
///
/// Formula: steps = angle (in revolutions) * steps_per_revolution
pub fn angle_to_steps(&self, angle: Angle) -> f64 {
let revolutions = angle.get::<revolution>();
revolutions * self.steps_per_revolution as f64
}
/// Convert steps/second to angular velocity
pub fn steps_to_angular_velocity(&self, steps: f64) -> AngularVelocity {
let revolutions_per_second = steps / self.steps_per_revolution as f64;
///
/// Formula: angular velocity (in rev/s) = steps_per_second / steps_per_revolution
pub fn steps_to_angular_velocity(&self, steps_per_second: f64) -> AngularVelocity {
let revolutions_per_second = steps_per_second / self.steps_per_revolution as f64;
AngularVelocity::new::<revolution_per_second>(revolutions_per_second)
}
/// Convert angular velocity to steps/second
///
/// Formula: steps_per_second = angular velocity (in rev/s) * steps_per_revolution
pub fn angular_velocity_to_steps(&self, angular_velocity: AngularVelocity) -> f64 {
let revolutions_per_second = angular_velocity.get::<revolution_per_second>();
revolutions_per_second * self.steps_per_revolution as f64
}
/// Convert steps/second^2 to angular acceleration
pub fn steps_to_angular_acceleration(&self, steps: f64) -> AngularAcceleration {
let revolutions_per_second_squared = steps / self.steps_per_revolution as f64;
/// Convert steps/second² to angular acceleration
///
/// Formula:
/// - revolutions_per_second² = steps_per_second² / steps_per_revolution
/// - angular acceleration (in rad/s²) = revolutions_per_second² * 2π
pub fn steps_to_angular_acceleration(
&self,
steps_per_second_squared: f64,
) -> AngularAcceleration {
let revolutions_per_second_squared =
steps_per_second_squared / self.steps_per_revolution as f64;
let radians_per_second_squared =
revolutions_per_second_squared * 2.0 * std::f64::consts::PI;
AngularAcceleration::new::<radian_per_second_squared>(radians_per_second_squared)
}
/// Convert angular acceleration to steps/second^2
/// Convert angular acceleration to steps/second²
///
/// Formula:
/// - revolutions_per_second² = angular acceleration (in rad/s²) / 2π
/// - steps_per_second² = revolutions_per_second² * steps_per_revolution
pub fn angular_acceleration_to_steps(&self, angular_acceleration: AngularAcceleration) -> f64 {
let radians_per_second_squared = angular_acceleration.get::<radian_per_second_squared>();
let revolutions_per_second_squared =
@@ -70,13 +97,13 @@ mod tests {
#[test]
fn test_new() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
assert_eq!(converter.steps_per_revolution, 200);
}
#[test]
fn test_steps_to_angle() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test full revolution
let angle = converter.steps_to_angle(200.0);
@@ -94,14 +121,14 @@ mod tests {
assert_relative_eq!(angle.get::<degree>(), 90.0, epsilon = EPSILON);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let angle = converter.steps_to_angle(100.0);
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = EPSILON);
}
#[test]
fn test_angle_to_steps() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test full revolution
let steps = converter.angle_to_steps(Angle::new::<revolution>(1.0));
@@ -116,14 +143,14 @@ mod tests {
assert_relative_eq!(steps, 50.0, epsilon = EPSILON);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let steps = converter.angle_to_steps(Angle::new::<degree>(90.0));
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
}
#[test]
fn test_steps_to_angular_velocity() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test one revolution per second
let angular_velocity = converter.steps_to_angular_velocity(200.0);
@@ -147,7 +174,7 @@ mod tests {
);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let angular_velocity = converter.steps_to_angular_velocity(200.0);
assert_relative_eq!(
angular_velocity.get::<revolution_per_second>(),
@@ -158,7 +185,7 @@ mod tests {
#[test]
fn test_angular_velocity_to_steps() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test one revolution per second
let steps =
@@ -171,7 +198,7 @@ mod tests {
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let steps =
converter.angular_velocity_to_steps(AngularVelocity::new::<revolution_per_second>(0.5));
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
@@ -179,7 +206,7 @@ mod tests {
#[test]
fn test_steps_to_angular_acceleration() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test one revolution per second squared
let angular_acceleration = converter.steps_to_angular_acceleration(200.0);
@@ -199,7 +226,7 @@ mod tests {
);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let angular_acceleration = converter.steps_to_angular_acceleration(400.0);
assert_relative_eq!(
angular_acceleration.get::<radian_per_second_squared>(),
@@ -210,7 +237,7 @@ mod tests {
#[test]
fn test_angular_acceleration_to_steps() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test one revolution per second squared (2π rad/s²)
let rps2 = 2.0 * std::f64::consts::PI;
@@ -226,7 +253,7 @@ mod tests {
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
// Test with different steps_per_revolution
let converter = StepConverter::new(400);
let converter = AngularStepConverter::new(400);
let steps = converter.angular_acceleration_to_steps(AngularAcceleration::new::<
radian_per_second_squared,
>(rps2));
@@ -235,7 +262,7 @@ mod tests {
#[test]
fn test_roundtrip_conversions() {
let converter = StepConverter::new(200);
let converter = AngularStepConverter::new(200);
// Test angle roundtrip
let original_steps = 123.0;
@@ -0,0 +1,279 @@
//! # Circular Converter
//!
//! This module provides the `CircularConverter` which handles transformations between
//! linear and angular measurements for circular motion systems.
//!
//! ## Usage
//!
//! The `CircularConverter` is designed to work alongside other converters:
//! - Use `AngularStepConverter` for angular-to-step conversions
//! - Use `LinearStepConverter` for complete linear-to-step conversions (uses `CircularConverter` internally)
//! - Use `CircularConverter` directly when you need linear-to-angular conversions without steps
//!
//! ## Examples
//!
//! ```rust
//! use control_core::converters::circular_converter::CircularConverter;
//! use uom::si::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//!
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
//! let velocity = Velocity::new::<meter_per_second>(1.0);
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
//! ```
use uom::si::{
acceleration::meter_per_second_squared,
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
/// A converter that handles transformations between linear and angular measurements
/// for circular motion. This includes conversions for position, velocity, acceleration,
/// and higher-order derivatives based on a circle's geometric properties.
#[derive(Debug, Clone)]
pub struct CircularConverter {
radius: Length,
circumference: Length,
diameter: Length,
}
impl CircularConverter {
/// Create a new converter from radius
pub fn from_radius(radius: Length) -> Self {
let diameter = radius * 2.0;
let circumference =
Length::new::<meter>(2.0 * std::f64::consts::PI * radius.get::<meter>());
Self {
radius,
diameter,
circumference,
}
}
/// Create a new converter from diameter
pub fn from_diameter(diameter: Length) -> Self {
let radius = diameter / 2.0;
Self::from_radius(radius)
}
/// Create a new converter from circumference
pub fn from_circumference(circumference: Length) -> Self {
let radius =
Length::new::<meter>(circumference.get::<meter>() / (2.0 * std::f64::consts::PI));
Self::from_radius(radius)
}
/// Get the radius
pub fn radius(&self) -> Length {
self.radius
}
/// Get the diameter
pub fn diameter(&self) -> Length {
self.diameter
}
/// Get the circumference
pub fn circumference(&self) -> Length {
self.circumference
}
// Linear to Angular Position Conversions
/// Convert linear distance to angular position
/// Formula: angle = distance / radius
pub fn linear_to_angular_position(&self, distance: Length) -> Angle {
let angle_rad = distance.get::<meter>() / self.radius.get::<meter>();
Angle::new::<radian>(angle_rad)
}
/// Convert angular position to linear distance
/// Formula: distance = angle * radius
pub fn angular_to_linear_position(&self, angle: Angle) -> Length {
let distance = angle.get::<radian>() * self.radius.get::<meter>();
Length::new::<meter>(distance)
}
// Linear to Angular Velocity Conversions
/// Convert linear velocity to angular velocity
/// Formula: angular_velocity = linear_velocity / radius
pub fn linear_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
let angular_velocity_rad_per_s =
velocity.get::<meter_per_second>() / self.radius.get::<meter>();
AngularVelocity::new::<radian_per_second>(angular_velocity_rad_per_s)
}
/// Convert angular velocity to linear velocity
/// Formula: linear_velocity = angular_velocity * radius
pub fn angular_to_linear_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
let velocity = angular_velocity.get::<radian_per_second>() * self.radius.get::<meter>();
Velocity::new::<meter_per_second>(velocity)
}
// Linear to Angular Acceleration Conversions
/// Convert linear acceleration to angular acceleration
/// Formula: angular_acceleration = linear_acceleration / radius
pub fn linear_to_angular_acceleration(
&self,
acceleration: Acceleration,
) -> AngularAcceleration {
let angular_acceleration_rad_per_s2 =
acceleration.get::<meter_per_second_squared>() / self.radius.get::<meter>();
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2)
}
/// Convert angular acceleration to linear acceleration
/// Formula: linear_acceleration = angular_acceleration * radius
pub fn angular_to_linear_acceleration(
&self,
angular_acceleration: AngularAcceleration,
) -> Acceleration {
let acceleration =
angular_acceleration.get::<radian_per_second_squared>() * self.radius.get::<meter>();
Acceleration::new::<meter_per_second_squared>(acceleration)
}
// Jerk Conversions (using f64 for now since UOM may not have jerk units)
/// Convert linear jerk to angular jerk
/// Formula: angular_jerk = linear_jerk / radius
/// Units: rad/s³ = (m/s³) / m
pub fn linear_to_angular_jerk(&self, linear_jerk: f64) -> f64 {
linear_jerk / self.radius.get::<meter>()
}
/// Convert angular jerk to linear jerk
/// Formula: linear_jerk = angular_jerk * radius
/// Units: m/s³ = (rad/s³) * m
pub fn angular_to_linear_jerk(&self, angular_jerk: f64) -> f64 {
angular_jerk * self.radius.get::<meter>()
}
// Arc Length Calculations
/// Calculate arc length from angle
/// Formula: arc_length = angle * radius
pub fn angle_to_arc_length(&self, angle: Angle) -> Length {
self.angular_to_linear_position(angle)
}
/// Calculate angle from arc length
/// Formula: angle = arc_length / radius
pub fn arc_length_to_angle(&self, arc_length: Length) -> Angle {
self.linear_to_angular_position(arc_length)
}
// Circumference-based calculations
/// Convert linear distance to number of revolutions
/// Formula: revolutions = distance / circumference
pub fn linear_distance_to_revolutions(&self, distance: Length) -> f64 {
distance.get::<meter>() / self.circumference.get::<meter>()
}
/// Convert number of revolutions to linear distance
/// Formula: distance = revolutions * circumference
pub fn revolutions_to_linear_distance(&self, revolutions: f64) -> Length {
Length::new::<meter>(revolutions * self.circumference.get::<meter>())
}
/// Convert linear velocity to revolutions per second
/// Formula: rps = velocity / circumference
pub fn linear_velocity_to_rps(&self, velocity: Velocity) -> f64 {
velocity.get::<meter_per_second>() / self.circumference.get::<meter>()
}
/// Convert revolutions per second to linear velocity
/// Formula: velocity = rps * circumference
pub fn rps_to_linear_velocity(&self, rps: f64) -> Velocity {
Velocity::new::<meter_per_second>(rps * self.circumference.get::<meter>())
}
}
#[cfg(test)]
mod tests {
use super::*;
use uom::si::length::centimeter;
#[test]
fn test_constructors() {
let radius = Length::new::<centimeter>(5.0);
let diameter = Length::new::<centimeter>(10.0);
let circumference = Length::new::<centimeter>(2.0 * std::f64::consts::PI * 5.0);
let converter1 = CircularConverter::from_radius(radius);
let converter2 = CircularConverter::from_diameter(diameter);
let converter3 = CircularConverter::from_circumference(circumference);
// All should have approximately the same values
assert!((converter1.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter2.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter3.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter1.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
assert!((converter2.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
assert!((converter3.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
}
#[test]
fn test_position_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
// Test linear to angular
let distance = Length::new::<meter>(std::f64::consts::PI);
let angle = converter.linear_to_angular_position(distance);
assert!((angle.get::<radian>() - std::f64::consts::PI).abs() < 1e-10);
// Test angular to linear
let angle = Angle::new::<radian>(std::f64::consts::PI / 2.0);
let distance = converter.angular_to_linear_position(angle);
assert!((distance.get::<meter>() - std::f64::consts::PI / 2.0).abs() < 1e-10);
}
#[test]
fn test_velocity_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(0.5));
// Test linear to angular
let velocity = Velocity::new::<meter_per_second>(1.0);
let angular_velocity = converter.linear_to_angular_velocity(velocity);
assert!((angular_velocity.get::<radian_per_second>() - 2.0).abs() < 1e-10);
// Test angular to linear
let angular_velocity = AngularVelocity::new::<radian_per_second>(4.0);
let velocity = converter.angular_to_linear_velocity(angular_velocity);
assert!((velocity.get::<meter_per_second>() - 2.0).abs() < 1e-10);
}
#[test]
fn test_revolution_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
// One full revolution should equal circumference
let revolutions = 1.0;
let distance = converter.revolutions_to_linear_distance(revolutions);
assert!((distance.get::<meter>() - converter.circumference().get::<meter>()).abs() < 1e-10);
// Converting back should give 1 revolution
let calculated_revolutions = converter.linear_distance_to_revolutions(distance);
assert!((calculated_revolutions - 1.0).abs() < 1e-10);
}
#[test]
fn test_jerk_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(2.0));
let linear_jerk = 4.0; // m/s³
let angular_jerk = converter.linear_to_angular_jerk(linear_jerk);
assert!((angular_jerk - 2.0).abs() < 1e-10); // 4.0 / 2.0 = 2.0 rad/s³
let calculated_linear_jerk = converter.angular_to_linear_jerk(angular_jerk);
assert!((calculated_linear_jerk - linear_jerk).abs() < 1e-10);
}
}
@@ -0,0 +1,573 @@
use uom::si::{
angle::revolution,
angular_velocity::revolution_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
};
use super::angular_step_converter::AngularStepConverter;
use super::circular_converter::CircularConverter;
/// Converts between linear measurements and motor steps
///
/// This converter handles the transformation between linear motion (distance, velocity,
/// acceleration) and motor steps, using the relationship between linear and rotary motion
/// through a wheel or pulley of a specific radius.
#[derive(Debug, Clone)]
pub struct LinearStepConverter {
angular_step_converter: AngularStepConverter,
circular_converter: CircularConverter,
}
// Constructor and basic getters
impl LinearStepConverter {
/// Create a new converter from radius and steps per revolution
pub fn from_radius(steps_per_revolution: i16, radius: Length) -> Self {
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_radius(radius),
}
}
/// Create a new converter from diameter and steps per revolution
pub fn from_diameter(steps_per_revolution: i16, diameter: Length) -> Self {
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_diameter(diameter),
}
}
/// Create a new converter from circumference and steps per revolution
pub fn from_circumference(steps_per_revolution: i16, circumference: Length) -> Self {
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_circumference(circumference),
}
}
/// Get the radius used by the converter
pub fn radius(&self) -> Length {
self.circular_converter.radius()
}
/// Get the diameter of the system
pub fn diameter(&self) -> Length {
self.circular_converter.diameter()
}
/// Get the circumference of the system
pub fn circumference(&self) -> Length {
self.circular_converter.circumference()
}
/// Get the steps per revolution
pub const fn steps_per_revolution(&self) -> i16 {
self.angular_step_converter.steps_per_revolution
}
}
// Linear to/from steps conversions
impl LinearStepConverter {
/// Convert linear distance to steps
///
/// Formula: steps = (distance / circumference) * steps_per_revolution
pub fn distance_to_steps(&self, distance: Length) -> f64 {
// Convert distance to revolutions using CircularConverter
let revolutions = self
.circular_converter
.linear_distance_to_revolutions(distance);
let angle = Angle::new::<revolution>(revolutions);
// Convert angle to steps
self.angular_step_converter.angle_to_steps(angle)
}
/// Convert steps to linear distance
///
/// Formula: distance = (steps / steps_per_revolution) * circumference
pub fn steps_to_distance(&self, steps: f64) -> Length {
// Convert steps to angle
let angle = self.angular_step_converter.steps_to_angle(steps);
// Convert angle to distance using CircularConverter
let revolutions = angle.get::<revolution>();
self.circular_converter
.revolutions_to_linear_distance(revolutions)
}
/// Convert linear velocity to steps/second
///
/// Formula: steps/second = (velocity / circumference) * steps_per_revolution
pub fn velocity_to_steps(&self, velocity: Velocity) -> f64 {
// Convert linear velocity to revolutions per second using CircularConverter
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
let angular_velocity = AngularVelocity::new::<revolution_per_second>(rps);
// Convert angular velocity to steps/second
self.angular_step_converter
.angular_velocity_to_steps(angular_velocity)
}
/// Convert steps/second to linear velocity
///
/// Formula: velocity = (steps_per_second / steps_per_revolution) * circumference
pub fn steps_to_velocity(&self, steps_per_second: f64) -> Velocity {
// Convert steps/second to angular velocity
let angular_velocity = self
.angular_step_converter
.steps_to_angular_velocity(steps_per_second);
// Convert angular velocity to linear velocity using CircularConverter
let rps = angular_velocity.get::<revolution_per_second>();
self.circular_converter.rps_to_linear_velocity(rps)
}
/// Convert linear acceleration to steps/second²
///
/// Formula: steps/second² = (acceleration / radius) * (steps_per_revolution / (2π))
pub fn acceleration_to_steps(&self, acceleration: Acceleration) -> f64 {
// Convert linear acceleration to angular acceleration using CircularConverter
let angular_acceleration = self
.circular_converter
.linear_to_angular_acceleration(acceleration);
// Convert angular acceleration to steps/second²
self.angular_step_converter
.angular_acceleration_to_steps(angular_acceleration)
}
/// Convert steps/second² to linear acceleration
///
/// Formula: acceleration = (steps_per_second² / steps_per_revolution) * (2π) * radius
pub fn steps_to_acceleration(&self, steps_per_second_squared: f64) -> Acceleration {
// Convert steps/second² to angular acceleration
let angular_acceleration = self
.angular_step_converter
.steps_to_angular_acceleration(steps_per_second_squared);
// Convert angular acceleration to linear acceleration using CircularConverter
self.circular_converter
.angular_to_linear_acceleration(angular_acceleration)
}
}
// Linear to/from angular conversions
impl LinearStepConverter {
/// Convert linear distance to angle
///
/// Formula: angle (in revolutions) = distance / circumference
pub fn distance_to_angle(&self, distance: Length) -> Angle {
let revolutions = self
.circular_converter
.linear_distance_to_revolutions(distance);
Angle::new::<revolution>(revolutions)
}
/// Convert angle to linear distance
///
/// Formula: distance = angle (in revolutions) * circumference
pub fn angle_to_distance(&self, angle: Angle) -> Length {
let revolutions = angle.get::<revolution>();
self.circular_converter
.revolutions_to_linear_distance(revolutions)
}
/// Convert linear velocity to angular velocity
///
/// Formula: angular velocity (in rev/s) = velocity / circumference
pub fn velocity_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
AngularVelocity::new::<revolution_per_second>(rps)
}
/// Convert angular velocity to linear velocity
///
/// Formula: velocity = angular velocity (in rev/s) * circumference
pub fn angular_velocity_to_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
let rps = angular_velocity.get::<revolution_per_second>();
self.circular_converter.rps_to_linear_velocity(rps)
}
/// Convert linear acceleration to angular acceleration
///
/// Formula: angular acceleration (in rad/s²) = acceleration / radius
pub fn acceleration_to_angular_acceleration(
&self,
acceleration: Acceleration,
) -> AngularAcceleration {
self.circular_converter
.linear_to_angular_acceleration(acceleration)
}
/// Convert angular acceleration to linear acceleration
///
/// Formula: acceleration = angular acceleration (in rad/s²) * radius
pub fn angular_acceleration_to_acceleration(
&self,
angular_acceleration: AngularAcceleration,
) -> Acceleration {
self.circular_converter
.angular_to_linear_acceleration(angular_acceleration)
}
}
// Forward angular to/from steps conversions from StepConverter
impl LinearStepConverter {
/// Convert steps to angle
pub fn steps_to_angle(&self, steps: f64) -> Angle {
self.angular_step_converter.steps_to_angle(steps)
}
/// Convert angle to steps
pub fn angle_to_steps(&self, angle: Angle) -> f64 {
self.angular_step_converter.angle_to_steps(angle)
}
/// Convert steps/second to angular velocity
pub fn steps_to_angular_velocity(&self, steps: f64) -> AngularVelocity {
self.angular_step_converter.steps_to_angular_velocity(steps)
}
/// Convert angular velocity to steps/second
pub fn angular_velocity_to_steps(&self, angular_velocity: AngularVelocity) -> f64 {
self.angular_step_converter
.angular_velocity_to_steps(angular_velocity)
}
/// Convert steps/second² to angular acceleration
pub fn steps_to_angular_acceleration(&self, steps: f64) -> AngularAcceleration {
self.angular_step_converter
.steps_to_angular_acceleration(steps)
}
/// Convert angular acceleration to steps/second²
pub fn angular_acceleration_to_steps(&self, angular_acceleration: AngularAcceleration) -> f64 {
self.angular_step_converter
.angular_acceleration_to_steps(angular_acceleration)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
#[test]
fn test_new() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
assert_eq!(converter.steps_per_revolution(), 200);
assert_eq!(converter.radius().get::<meter>(), 0.1);
}
#[test]
fn test_diameter_and_circumference() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Test diameter
let diameter = converter.diameter();
assert_relative_eq!(diameter.get::<meter>(), 0.2, epsilon = EPSILON);
// Test circumference
let circumference = converter.circumference();
let expected_circumference = 2.0 * std::f64::consts::PI * 0.1;
assert_relative_eq!(
circumference.get::<meter>(),
expected_circumference,
epsilon = EPSILON
);
}
// Tests for linear <-> steps conversions
#[test]
fn test_distance_to_steps() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// One full revolution
let steps = converter.distance_to_steps(Length::new::<meter>(circumference));
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
// Half revolution
let steps = converter.distance_to_steps(Length::new::<meter>(circumference / 2.0));
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
}
#[test]
fn test_steps_to_distance() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// One full revolution
let distance = converter.steps_to_distance(200.0);
assert_relative_eq!(distance.get::<meter>(), circumference, epsilon = EPSILON);
// Half revolution
let distance = converter.steps_to_distance(100.0);
assert_relative_eq!(
distance.get::<meter>(),
circumference / 2.0,
epsilon = EPSILON
);
}
#[test]
fn test_velocity_to_steps() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// Linear velocity equivalent to one revolution per second
let velocity = Velocity::new::<meter_per_second>(circumference);
let steps_per_second = converter.velocity_to_steps(velocity);
assert_relative_eq!(steps_per_second, 200.0, epsilon = EPSILON);
// Half revolution per second
let velocity = Velocity::new::<meter_per_second>(circumference / 2.0);
let steps_per_second = converter.velocity_to_steps(velocity);
assert_relative_eq!(steps_per_second, 100.0, epsilon = EPSILON);
}
#[test]
fn test_steps_to_velocity() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// Steps for one revolution per second
let velocity = converter.steps_to_velocity(200.0);
assert_relative_eq!(
velocity.get::<meter_per_second>(),
circumference,
epsilon = EPSILON
);
// Half revolution per second
let velocity = converter.steps_to_velocity(100.0);
assert_relative_eq!(
velocity.get::<meter_per_second>(),
circumference / 2.0,
epsilon = EPSILON
);
}
#[test]
fn test_acceleration_to_steps() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Create a linear acceleration of 1 m/s²
// This will result in angular acceleration of a/r = 1/0.1 = 10 rad/s²
let acceleration = Acceleration::new::<meter_per_second_squared>(1.0);
let steps_per_second_squared = converter.acceleration_to_steps(acceleration);
// Expected steps for 10 rad/s² angular acceleration:
// 10 rad/s² * (1 rev/2π rad) * 200 steps/rev = 318.31 steps/s²
let expected_steps = 200.0 * 10.0 / (2.0 * std::f64::consts::PI);
assert_relative_eq!(
steps_per_second_squared,
expected_steps,
epsilon = EPSILON * 10.0
);
}
#[test]
fn test_steps_to_acceleration() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Steps for one revolution per second squared
let acceleration = converter.steps_to_acceleration(200.0);
let expected = 2.0 * std::f64::consts::PI * 0.1; // rad/s² * radius
assert_relative_eq!(
acceleration.get::<meter_per_second_squared>(),
expected,
epsilon = EPSILON
);
}
// Tests for linear <-> angular conversions
#[test]
fn test_distance_to_angle() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// Full circumference = 1 revolution
let angle = converter.distance_to_angle(Length::new::<meter>(circumference));
assert_relative_eq!(angle.get::<revolution>(), 1.0, epsilon = EPSILON);
// Half circumference = 0.5 revolution
let angle = converter.distance_to_angle(Length::new::<meter>(circumference / 2.0));
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = EPSILON);
}
#[test]
fn test_angle_to_distance() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// 1 revolution = full circumference
let distance = converter.angle_to_distance(Angle::new::<revolution>(1.0));
assert_relative_eq!(distance.get::<meter>(), circumference, epsilon = EPSILON);
// 0.5 revolution = half circumference
let distance = converter.angle_to_distance(Angle::new::<revolution>(0.5));
assert_relative_eq!(
distance.get::<meter>(),
circumference / 2.0,
epsilon = EPSILON
);
}
#[test]
fn test_velocity_to_angular_velocity() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// Linear velocity of full circumference per second = 1 revolution per second
let angular_velocity = converter
.velocity_to_angular_velocity(Velocity::new::<meter_per_second>(circumference));
assert_relative_eq!(
angular_velocity.get::<revolution_per_second>(),
1.0,
epsilon = EPSILON
);
// Half circumference per second = 0.5 revolution per second
let angular_velocity =
converter.velocity_to_angular_velocity(Velocity::new::<meter_per_second>(
circumference / 2.0,
));
assert_relative_eq!(
angular_velocity.get::<revolution_per_second>(),
0.5,
epsilon = EPSILON
);
}
#[test]
fn test_angular_velocity_to_velocity() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
// 1 revolution per second = linear velocity of full circumference per second
let velocity = converter
.angular_velocity_to_velocity(AngularVelocity::new::<revolution_per_second>(1.0));
assert_relative_eq!(
velocity.get::<meter_per_second>(),
circumference,
epsilon = EPSILON
);
// 0.5 revolution per second = half circumference per second
let velocity = converter
.angular_velocity_to_velocity(AngularVelocity::new::<revolution_per_second>(0.5));
assert_relative_eq!(
velocity.get::<meter_per_second>(),
circumference / 2.0,
epsilon = EPSILON
);
}
#[test]
fn test_acceleration_to_angular_acceleration() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Linear acceleration of 1 m/s² = angular acceleration of 10 rad/s²
let angular_accel = converter.acceleration_to_angular_acceleration(Acceleration::new::<
meter_per_second_squared,
>(1.0));
assert_relative_eq!(
angular_accel.get::<radian_per_second_squared>(),
10.0, // a/r = 1/0.1 = 10
epsilon = EPSILON
);
}
#[test]
fn test_angular_acceleration_to_acceleration() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Angular acceleration of 10 rad/s² = linear acceleration of 1 m/s²
let acceleration =
converter.angular_acceleration_to_acceleration(AngularAcceleration::new::<
radian_per_second_squared,
>(10.0));
assert_relative_eq!(
acceleration.get::<meter_per_second_squared>(),
1.0, // α*r = 10*0.1 = 1
epsilon = EPSILON
);
}
// Tests for forwarded angular <-> steps methods
#[test]
fn test_forwarded_steps_to_angle() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// 200 steps = 1 revolution
let angle = converter.steps_to_angle(200.0);
assert_relative_eq!(angle.get::<revolution>(), 1.0, epsilon = EPSILON);
// 100 steps = 0.5 revolution
let angle = converter.steps_to_angle(100.0);
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = EPSILON);
}
#[test]
fn test_forwarded_angle_to_steps() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// 1 revolution = 200 steps
let steps = converter.angle_to_steps(Angle::new::<revolution>(1.0));
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
// 0.5 revolution = 100 steps
let steps = converter.angle_to_steps(Angle::new::<revolution>(0.5));
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
}
#[test]
fn test_roundtrip_conversions() {
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
// Test linear distance to steps roundtrip
let original_distance = Length::new::<meter>(0.456);
let steps = converter.distance_to_steps(original_distance);
let roundtrip_distance = converter.steps_to_distance(steps);
assert_relative_eq!(
original_distance.get::<meter>(),
roundtrip_distance.get::<meter>(),
epsilon = EPSILON
);
// Test linear velocity to steps roundtrip
let original_velocity = Velocity::new::<meter_per_second>(0.789);
let steps_per_second = converter.velocity_to_steps(original_velocity);
let roundtrip_velocity = converter.steps_to_velocity(steps_per_second);
assert_relative_eq!(
original_velocity.get::<meter_per_second>(),
roundtrip_velocity.get::<meter_per_second>(),
epsilon = EPSILON
);
// Test linear distance to angle roundtrip
let original_distance = Length::new::<meter>(0.123);
let angle = converter.distance_to_angle(original_distance);
let roundtrip_distance = converter.angle_to_distance(angle);
assert_relative_eq!(
original_distance.get::<meter>(),
roundtrip_distance.get::<meter>(),
epsilon = EPSILON
);
// Test angle to steps roundtrip
let original_angle = Angle::new::<revolution>(0.75);
let steps = converter.angle_to_steps(original_angle);
let roundtrip_angle = converter.steps_to_angle(steps);
assert_relative_eq!(
original_angle.get::<revolution>(),
roundtrip_angle.get::<revolution>(),
epsilon = EPSILON
);
}
}
+4 -1
View File
@@ -1,2 +1,5 @@
pub mod angle_converter;
pub mod angular_step_converter;
pub mod circular_converter;
pub mod linear_step_converter;
pub mod motor_converter;
pub mod step_converter;
@@ -1,9 +1 @@
pub struct MotorConverter {}
impl MotorConverter {
pub fn rpm_to_hz(rpm: f32) -> f32 {
rpm / 60.0
}
pub fn hz_to_rpm(hz: f32) -> f32 {
hz * 60.0
}
}
+36
View File
@@ -0,0 +1,36 @@
use anyhow::anyhow;
use smol::lock::Mutex;
use std::any::Any;
use std::sync::Arc;
pub trait Downcast<T> {
fn downcast(&self) -> Result<T, anyhow::Error>;
}
pub trait DowncastRef<T> {
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
}
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let lock = self.lock_blocking();
// Check if the inner type can be downcasted to T
let any: &dyn Any = &*lock;
if any.is::<T>() {
// Clone the Arc and return it as the desired type
let cloned: Arc<Mutex<dyn Any>> = self.clone();
// Transmute the Arc to the desired type
unsafe {
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
Ok(arc)
}
} else {
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
}
}
}
@@ -4,8 +4,10 @@ use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
use ethercat_hal::devices::el1002::EL1002_IDENTITY_A;
use ethercat_hal::devices::el1008::EL1008_IDENTITY_A;
use ethercat_hal::devices::el2002::EL2002_IDENTITY_A;
use ethercat_hal::devices::el2002::EL2002_IDENTITY_B;
use ethercat_hal::devices::el2004::EL2004_IDENTITY_A;
use ethercat_hal::devices::el2008::EL2008_IDENTITY_A;
use ethercat_hal::devices::el2008::EL2008_IDENTITY_B;
use ethercat_hal::devices::el2521::{
EL2521_IDENTITY_0000_A, EL2521_IDENTITY_0000_B, EL2521_IDENTITY_0024_A,
};
@@ -13,20 +15,20 @@ use ethercat_hal::devices::el2522::EL2522_IDENTITY_A;
use ethercat_hal::devices::el3001::EL3001_IDENTITY_A;
use ethercat_hal::devices::el3021::EL3021_IDENTITY_A;
use ethercat_hal::devices::el3024::EL3024_IDENTITY_A;
use ethercat_hal::devices::el3062_0030::EL3062_0030_IDENTITY_A;
use ethercat_hal::devices::el3204::EL3204_IDENTITY_A;
use ethercat_hal::devices::el6021::EL6021_IDENTITY_A;
use ethercat_hal::devices::el6021::EL6021_IDENTITY_B;
use ethercat_hal::devices::el3204::EL3204_IDENTITY_B;
use ethercat_hal::devices::el6021::EL6021_IDENTITY_C;
use ethercat_hal::devices::el7031::EL7031_IDENTITY_A;
use ethercat_hal::devices::el7031::EL7031_IDENTITY_B;
use ethercat_hal::devices::el4002::EL4002_IDENTITY_A;
use ethercat_hal::devices::el5152::EL5152_IDENTITY_A;
use ethercat_hal::devices::el6021::{
EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D,
};
use ethercat_hal::devices::el7031::{EL7031_IDENTITY_A, EL7031_IDENTITY_B};
use ethercat_hal::devices::el7031_0030::EL7031_0030_IDENTITY_A;
use ethercat_hal::devices::el7041_0052::EL7041_0052_IDENTITY_A;
use ethercat_hal::devices::subdevice_identity_to_tuple;
use ethercat_hal::types::{
EthercrabSubDeviceOperational,
EthercrabSubDevicePreoperational,
use ethercat_hal::helpers::ethercrab_types::{
EthercrabSubDeviceOperational, EthercrabSubDevicePreoperational,
};
use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
@@ -44,7 +46,12 @@ pub struct MachineIdentificationAddresses {
}
impl MachineIdentificationAddresses {
pub fn new(vendor_word: u16, serial_word: u16, machine_word: u16, device_word: u16) -> Self {
pub const fn new(
vendor_word: u16,
serial_word: u16,
machine_word: u16,
device_word: u16,
) -> Self {
Self {
vendor_word,
serial_word,
@@ -58,8 +65,8 @@ impl Default for MachineIdentificationAddresses {
fn default() -> Self {
Self {
vendor_word: 0x0028,
serial_word: 0x0029,
machine_word: 0x002a,
machine_word: 0x0029,
serial_word: 0x002a,
role_word: 0x002b,
}
}
@@ -68,15 +75,13 @@ impl Default for MachineIdentificationAddresses {
/// Reads the EEPROM of all subdevices to get their machine device identifications
///
/// Returns a vector of MachineDeviceIdentification for all subdevices
pub async fn read_device_identifications<
'maindevice,
>(
pub async fn read_device_identifications<'maindevice>(
subdevices: &Vec<EthercrabSubDevicePreoperational<'maindevice>>,
maindevice: &MainDevice<'maindevice>,
) -> Vec<Result<DeviceMachineIdentification, Error>> {
let mut result = Vec::new();
for subdevice in subdevices.iter() {
let identification = machine_device_identification(&subdevice, maindevice).await;
let identification = machine_device_identification(subdevice, maindevice).await;
result.push(identification);
}
result
@@ -123,7 +128,6 @@ pub async fn machine_device_identification<'maindevice>(
module_path!(),
subdevice.name()
)))?,
},
role: subdevice
.eeprom_read::<u16>(maindevice, addresses.role_word)
@@ -135,17 +139,20 @@ pub async fn machine_device_identification<'maindevice>(
)))?,
};
log::debug!(
"[{}::machine_device_identification] Read MDI from EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
module_path!(),
tracing::debug!(
"Read MDI from EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
subdevice.name(),
mdi.machine_identification_unique.machine_identification.vendor,
mdi.machine_identification_unique
.machine_identification
.vendor,
addresses.vendor_word,
addresses.vendor_word + 1,
mdi.machine_identification_unique.serial,
addresses.serial_word,
addresses.serial_word + 1,
mdi.machine_identification_unique.machine_identification.machine,
mdi.machine_identification_unique
.machine_identification
.machine,
addresses.machine_word,
addresses.machine_word + 1,
mdi.role,
@@ -163,17 +170,22 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
device_identification: &DeviceMachineIdentification,
) -> Result<(), Error> {
let addresses = get_identification_addresses(&subdevice.identity(), subdevice.name())?;
log::debug!(
"[{}::write_machine_device_identification] Writing MDI to EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
module_path!(),
tracing::debug!(
"Writing MDI to EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
subdevice.name(),
device_identification.machine_identification_unique.machine_identification.vendor,
device_identification
.machine_identification_unique
.machine_identification
.vendor,
addresses.vendor_word,
addresses.vendor_word + 1,
device_identification.machine_identification_unique.serial,
addresses.serial_word,
addresses.serial_word + 1,
device_identification.machine_identification_unique.machine_identification.machine,
device_identification
.machine_identification_unique
.machine_identification
.machine,
addresses.machine_word,
addresses.machine_word + 1,
device_identification.role,
@@ -185,7 +197,10 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
.eeprom_write_dangerously(
maindevice,
addresses.vendor_word,
device_identification.machine_identification_unique.machine_identification.vendor,
device_identification
.machine_identification_unique
.machine_identification
.vendor,
)
.await?;
subdevice
@@ -199,7 +214,10 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
.eeprom_write_dangerously(
maindevice,
addresses.machine_word,
device_identification.machine_identification_unique.machine_identification.machine,
device_identification
.machine_identification_unique
.machine_identification
.machine,
)
.await?;
subdevice
@@ -210,30 +228,37 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
/// Returns the EEPROM addresses for the machine device identification
/// based on the subdevice's identity
pub fn get_identification_addresses<'maindevice>(
pub fn get_identification_addresses(
subdevice_identity: &SubDeviceIdentity,
subdevice_name: &str,
) -> Result<MachineIdentificationAddresses, Error> {
let identity_tuple = subdevice_identity_to_tuple(&subdevice_identity);
let identity_tuple = subdevice_identity_to_tuple(subdevice_identity);
Ok(match identity_tuple {
EK1100_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2002_IDENTITY_A | EL2002_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2004_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2008_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2008_IDENTITY_A | EL2008_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
MachineIdentificationAddresses::default()
}
EL2522_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3062_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
EL4002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL5152_IDENTITY_A => MachineIdentificationAddresses::default(),
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C | EL6021_IDENTITY_D => {
MachineIdentificationAddresses::default()
}
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C => MachineIdentificationAddresses::default(),
_ => {
// block_on(u16dump(&subdevice, maindevice, 0x00, 0xff))?;
Err(anyhow!(
@@ -259,8 +284,8 @@ async fn u16dump<'maindevice>(
words.push(subdevice.eeprom_read(maindevice, word).await?);
}
print!(
"EEPROM dump for {} from 0x{:04x} to 0x{:04x}\n",
println!(
"EEPROM dump for {} from 0x{:04x} to 0x{:04x}",
subdevice.name(),
start_byte / 2,
end_byte / 2
@@ -275,7 +300,7 @@ fn u16print(start_byte: u16, end_byte: u16, data: Vec<u16>) {
let table_start_word = start_byte & 0xfff0;
let table_end_word = (end_byte & 0xfff0_u16) + 0x10_u16;
let rows = table_end_word - table_start_word >> 4;
let rows = (table_end_word - table_start_word) >> 4;
for row in 0..rows {
print!("0x{:04x} | ", (table_start_word + row * 0x10) / 2);
@@ -292,7 +317,7 @@ fn u16print(start_byte: u16, end_byte: u16, data: Vec<u16>) {
}
}
}
print!("\n");
println!();
}
}
@@ -1,4 +1,4 @@
use std::{sync::Arc, time::Duration};
use std::{process::Command, sync::Arc, time::Duration};
use ethercrab::{
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
@@ -12,94 +12,118 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
/// Finds an ethernet interface that is suitable for EtherCAT communication.
///
/// ```ignore
/// match discover_ethercat_interface().await {
/// Ok(interface) => println!("Found working interface: {}", interface),
/// Err(_) => println!("No working interface found"),
/// }
/// ```
/// Sets a network interface to unmanaged by NetworkManager.
/// Returns true if the command succeeded.
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
let managed_str = match managed {
true => "yes",
false => "no",
};
tracing::info!(
"set_interface_managed for {} managed was set to: {}",
interface,
managed_str
);
let status = Command::new("nmcli")
.args(["dev", "set", interface, "managed", managed_str])
.status();
matches!(status, Ok(s) if s.success())
}
/// Returns true if the given network interface is Ethernet.
/// Prevents testing wlan,loopback and other non ethernet devices
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
#[cfg(target_os = "linux")]
{
let base_path = format!("/sys/class/net/{}", interface);
let type_path = std::path::Path::new(&base_path).join("type");
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
// 1 means Ethernet
let reports_as_ethernet = iface_type == "1";
// Double-check that it's not a wireless interface
let uevent_path = std::path::Path::new(&base_path).join("uevent");
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
// If "DEVTYPE=wlan" appears, it's lying
let actually_wifi = uevent.contains("DEVTYPE=wlan");
return Ok(reports_as_ethernet && !actually_wifi);
}
#[cfg(not(target_os = "linux"))]
return Ok(true);
}
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
log::info!("Discovering EtherCAT interface...");
tracing::info!("Discovering EtherCAT interface...");
// Set up a custom panic hook that suppresses panic output
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
// Optional: log the panic in a controlled way
if let Some(location) = panic_info.location() {
log::debug!(
tracing::debug!(
"Suppressed panic in interface test: {} (at {}:{})",
panic_info,
location.file(),
location.line()
);
} else {
log::debug!("Suppressed panic in interface test: {}", panic_info);
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
}
// Don't call the default hook, which would print the backtrace
}));
// Get eligible interfaces
let mut interfaces = Interface::get_all()
.or_else(|e| Err(anyhow::anyhow!("Failed to get network interfaces: {}", e)))?
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
.into_iter()
.filter(|iface| {
iface.is_up()
&& iface.is_running()
&& !iface.is_loopback()
&& is_ethernet(&iface.name).unwrap_or(false)
&& !iface.name.starts_with("bridge")
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
})
.collect::<Vec<_>>();
// Sort interfaces by name
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
let mut interface: Option<&str> = None;
// Try all interfaces concurrently
let tasks = interfaces
.iter()
.map(|iface| {
let name = iface.name.clone();
std::thread::spawn(move || {
std::panic::catch_unwind(|| {
smol::block_on(async {
match test_interface(&name) {
Ok(_) => {
log::debug!("Found working EtherCAT interface: {}", name);
Some(name.clone())
}
Err(e) => {
log::debug!("Interface {} failed: {}", name, e);
None
}
}
})
})
.unwrap_or(None)
})
})
.collect::<Vec<_>>();
for i in 0..interfaces.len() {
// Avoid NetworkManager hangups
set_interface_managed(&interfaces[i].name, false);
// Return first successful interface
let result =
tasks
.into_iter()
.find_map(|task| match task.join().expect("Should join thread") {
Some(name) => Some(name),
None => None,
});
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
interface = Some(&interfaces[i].name);
break;
}
Err(_) => (),
}
}
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
continue;
} else {
set_interface_managed(&interfaces[i].name, true);
}
}
// Restore the default panic hook
std::panic::set_hook(default_hook);
match result {
Some(name) => Ok(name),
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
match interface {
Some(interface) => return Ok(interface.to_string()),
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
}
}
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
log::debug!("Testing interface: {}", interface);
tracing::info!("Testing interface: {}", interface);
let pdu_storage = Box::leak(Box::new(PduStorage::<
IFACE_DISCOVERY_MAX_FRAMES,
@@ -133,7 +157,12 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
// Default 1000ms
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig::default(),
MainDeviceConfig {
// Default 10000
dc_static_sync_iterations: 10000,
// Default None
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
},
));
let result = maindevice
+42
View File
@@ -0,0 +1,42 @@
pub struct ListChange<'t, T>
where
T: PartialEq,
{
pub added: Vec<&'t T>,
pub removed: Vec<&'t T>,
pub same: Vec<&'t T>,
}
pub fn compare_lists<'oldlist, 'newlist, 't, T: PartialEq>(
old_list: &'oldlist Vec<T>,
new_list: &'newlist Vec<T>,
) -> ListChange<'t, T>
where
T: PartialEq,
'oldlist: 't,
'newlist: 't,
{
let mut added = Vec::new();
let mut removed = Vec::new();
let mut same = Vec::new();
for item in new_list.iter() {
if !old_list.iter().any(|x| x == item) {
added.push(item);
} else {
same.push(item);
}
}
for item in old_list.iter() {
if !new_list.iter().any(|x| x == item) {
removed.push(item);
}
}
ListChange {
added,
removed,
same,
}
}
@@ -0,0 +1,337 @@
use serde::Serialize;
use serde::ser::{
SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
SerializeTupleStruct, SerializeTupleVariant, Serializer,
};
use std::error::Error;
use std::fmt::Display;
use std::hash::{DefaultHasher, Hasher};
#[derive(Debug)]
pub struct HashSerializerError {}
impl std::fmt::Display for HashSerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{:?}", self))
// f.debug_struct("HashSerialiyerError")
// .finish()
}
}
impl Error for HashSerializerError {}
impl serde::ser::Error for HashSerializerError {
fn custom<T>(_msg: T) -> Self
where
T: Display,
{
Self {}
}
}
pub struct HashSerializer<'a, H: Hasher>(&'a mut H);
impl<H: Hasher> SerializeSeq for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeTuple for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeTupleStruct for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeTupleVariant for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeMap for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
key.serialize(HashSerializer(self.0))
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeStruct for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
// Optional: hash the field name to avoid collisions
self.0.write(key.as_bytes());
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> SerializeStructVariant for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
self.0.write(key.as_bytes());
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<H: Hasher> Serializer for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Self;
type SerializeTupleVariant = Self;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Self;
fn serialize_bool(self, v: bool) -> Result<(), Self::Error> {
self.0.write_u8(v as u8);
Ok(())
}
fn serialize_i8(self, v: i8) -> Result<(), Self::Error> {
self.0.write_i8(v);
Ok(())
}
fn serialize_i16(self, v: i16) -> Result<(), Self::Error> {
self.0.write_i16(v);
Ok(())
}
fn serialize_i32(self, v: i32) -> Result<(), Self::Error> {
self.0.write_i32(v);
Ok(())
}
fn serialize_i64(self, v: i64) -> Result<(), Self::Error> {
self.0.write_i64(v);
Ok(())
}
fn serialize_u8(self, v: u8) -> Result<(), Self::Error> {
self.0.write_u8(v);
Ok(())
}
fn serialize_u16(self, v: u16) -> Result<(), Self::Error> {
self.0.write_u16(v);
Ok(())
}
fn serialize_u32(self, v: u32) -> Result<(), Self::Error> {
self.0.write_u32(v);
Ok(())
}
fn serialize_u64(self, v: u64) -> Result<(), Self::Error> {
self.0.write_u64(v);
Ok(())
}
fn serialize_f32(self, v: f32) -> Result<(), Self::Error> {
self.0.write_u32(v.to_bits());
Ok(())
}
fn serialize_f64(self, v: f64) -> Result<(), Self::Error> {
self.0.write_u64(v.to_bits());
Ok(())
}
fn serialize_char(self, v: char) -> Result<(), Self::Error> {
let mut buf = [0; 4];
self.0.write(v.encode_utf8(&mut buf).as_bytes());
Ok(())
}
fn serialize_str(self, v: &str) -> Result<(), Self::Error> {
self.0.write(v.as_bytes());
Ok(())
}
fn serialize_bytes(self, v: &[u8]) -> Result<(), Self::Error> {
self.0.write(v);
Ok(())
}
fn serialize_none(self) -> Result<(), Self::Error> {
self.0.write_u8(0); // type tag for None
Ok(())
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
self.0.write_u8(1); // type tag for Some
value.serialize(self)
}
fn serialize_unit(self) -> Result<(), Self::Error> {
self.0.write_u8(0);
Ok(())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> {
self.serialize_unit()
}
fn serialize_unit_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
) -> Result<(), Self::Error> {
self.0.write_u32(variant_index);
Ok(())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
value: &T,
) -> Result<(), Self::Error> {
self.0.write_u32(variant_index);
value.serialize(self)
}
// For sequences, tuples, maps, structs, just return self for now
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Ok(self)
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Ok(self)
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Ok(self)
}
fn serialize_tuple_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
self.0.write_u32(variant_index);
Ok(self)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Ok(self)
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(self)
}
fn serialize_struct_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
self.0.write_u32(variant_index);
Ok(self)
}
}
// A helper that uses Serialize to feed into a Hasher without allocating a buffer:
pub fn hash_with_serde_model<T: Serialize>(value: T) -> u64 {
let mut h = DefaultHasher::new();
let ser = HashSerializer(&mut h);
value.serialize(ser).expect("serialize to hasher failed");
h.finish()
}
+108
View File
@@ -0,0 +1,108 @@
/// Folds bytes using XOR to produce a u128 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u128.
/// If there are fewer than 16 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u128(bytes: &[u8]) -> u128 {
let mut result = 0u128;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 16) * 8;
result ^= (byte as u128) << shift;
}
result
}
/// Folds bytes using XOR to produce a u64 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u64.
/// If there are fewer than 8 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u64(bytes: &[u8]) -> u64 {
let mut result = 0u64;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 8) * 8;
result ^= (byte as u64) << shift;
}
result
}
/// Folds bytes using XOR to produce a u32 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u32.
/// If there are fewer than 4 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u32(bytes: &[u8]) -> u32 {
let mut result = 0u32;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 4) * 8;
result ^= (byte as u32) << shift;
}
result
}
/// Folds bytes using XOR to produce a u16 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u16.
/// If there are fewer than 2 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u16(bytes: &[u8]) -> u16 {
let mut result = 0u16;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 2) * 8;
result ^= (byte as u16) << shift;
}
result
}
/// Folds bytes using XOR to produce a u8 value.
///
/// Takes a slice of bytes and XORs them all together to produce a u8.
pub fn byte_folding_u8(bytes: &[u8]) -> u8 {
bytes.iter().fold(0u8, |acc, &byte| acc ^ byte)
}
/// Computes a djb2 hash of the input bytes.
///
/// The djb2 algorithm is a simple non-cryptographic hash function created by
/// Daniel J. Bernstein. It uses the formula: `hash = hash * 33 + byte` for each byte.
///
/// This implementation uses the traditional 32-bit hash value and uses wrapping
/// arithmetic to handle potential overflows gracefully.
///
/// # Arguments
///
/// * `bytes` - A slice of bytes to hash
///
/// # Returns
///
/// A 32-bit hash value as a `u32`
///
/// # Examples
///
/// ```
/// use control_core::helpers::hashing::hash_djb2;
///
/// let hash = hash_djb2(b"hello world");
/// assert_ne!(hash, 0);
/// ```
///
/// # Algorithm Details
///
/// - Initial hash value: 5381 (djb2 magic number)
/// - For each byte: `hash = hash * 33 + byte`
/// - Uses wrapping arithmetic to prevent overflow panics
pub fn hash_djb2(bytes: &[u8]) -> u32 {
let mut hash: u32 = 5381;
for byte in bytes {
hash = hash.wrapping_mul(33).wrapping_add(*byte as u32);
}
hash
}
#[test]
fn test_hash() {
let test_case = "/dev/ttyUSB1";
let hash_u32 = hash_djb2(test_case.as_bytes());
let hash_u16 = byte_folding_u16(&hash_u32.to_le_bytes());
tracing::info!("{}", test_case);
assert_eq!(test_case, "/dev/ttyUSB1");
assert_eq!(hash_u32, 266121182);
assert_eq!(hash_u16, 40962);
}
+880
View File
@@ -0,0 +1,880 @@
/// Normalizes a value from a given range [min, max] to the range [0, 1].
///
/// This function maps a value from an arbitrary range to the standard
/// normalized range [0, 1]. Values o/// Plot for Steepness = -1:ge are clamped
/// to the boundaries (0.0 or 1.0).
///
/// # Arguments
/// * `value` - The input value to normalize
/// * `min` - The minimum value of the input range
/// * `max` - The maximum value of the input range
///
/// # Returns
/// * Normalized value in range [0.0, 1.0]
///
/// # Panics
/// * Panics if `min >= max`
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::normalize;
/// let normalized = normalize(5.0, 0.0, 10.0); // Returns 0.5
/// let clamped = normalize(-2.0, 0.0, 10.0); // Returns 0.0 (clamped)
/// ```
pub fn normalize<T>(value: T, min: T, max: T) -> f64
where
T: Into<f64>,
{
let value: f64 = value.into();
let min: f64 = min.into();
let max: f64 = max.into();
if min >= max {
panic!("min must be less than max");
}
if value < min {
return 0.0;
} else if value > max {
return 1.0;
}
(value - min) / (max - min)
}
/// Scales a normalized value from the range [0, 1] to a new range [new_min, new_max].
///
/// This function takes a normalized value (typically from 0 to 1) and maps it
/// to a new target range. It's the inverse operation of normalization.
///
/// # Arguments
/// * `normalized_value` - Input value in range [0.0, 1.0]
/// * `new_min` - The minimum value of the target range
/// * `new_max` - The maximum value of the target range
///
/// # Returns
/// * Scaled value in the range [new_min, new_max]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::scale;
/// let scaled = scale(0.5, 0.0, 100.0); // Returns 50.0
/// let negative_range = scale(0.25, -10.0, 10.0); // Returns -5.0
/// ```
pub fn scale<T>(normalized_value: f64, new_min: T, new_max: T) -> T
where
T: Into<f64> + From<f64>,
{
let normalized_value: f64 = normalized_value;
let new_min: f64 = new_min.into();
let new_max: f64 = new_max.into();
let result = normalized_value.mul_add(new_max - new_min, new_min);
T::from(result)
}
/// Performs hinge interpolation creating a piecewise linear function.
///
/// This function creates a piecewise linear curve that:
/// - For values < x: linearly interpolates from (0, 0) to (x, y)
/// - For values > x: linearly interpolates from (x, y) to (1, 1)
/// - For value == x: returns y
///
/// The function assumes the input value is normalized to the range [0, 1],
/// and creates a "hinge" or "elbow" shape at the point (x, y).
///
/// # Arguments
/// * `value` - The input value (typically in range [0, 1])
/// * `x` - The x-coordinate of the hinge point (typically in range [0, 1])
/// * `y` - The y-coordinate of the hinge point (typically in range [0, 1])
///
/// # Returns
/// * Interpolated value based on the hinge function
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_hinge;
/// // Create a hinge at (0.3, 0.7)
/// let result1 = interpolate_hinge(0.15, 0.3, 0.7); // Returns 0.35 (halfway to hinge)
/// let result2 = interpolate_hinge(0.3, 0.3, 0.7); // Returns 0.7 (at hinge)
/// let result3 = interpolate_hinge(0.65, 0.3, 0.7); // Returns 0.85 (halfway from hinge to 1)
/// ```
/// ```ignore
/// Hinge at (0.8, 0.2)
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠤⠒⠒⠊⠁⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠒⠒⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⣀⣀⠤⠤⠔⠒⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠉⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_hinge<T>(value: T, x: T, y: T) -> f64
where
T: Into<f64>,
{
let value_f64: f64 = value.into();
let x_f64: f64 = x.into();
let y_f64: f64 = y.into();
// If we are under X we scale linearly from 0 to y
// If we are over X we scale linearly from y to 1
if value_f64 < x_f64 {
normalize(value_f64, 0.0, x_f64) * y_f64
} else if value_f64 > x_f64 {
normalize(value_f64, x_f64, 1.0).mul_add(1.0 - y_f64, y_f64)
} else {
y_f64
}
}
/// Transforms a normalized value from the range [0, 1] to a [0, 1] range using a normalized exponential function.
///
/// This function creates smooth curves that are either convex (fast start, slow end)
/// or concave (slow start, fast end) depending on the steepness parameter sign.
/// The curve always passes through (0,0) and (1,1).
///
/// # Arguments
/// * `normalized_value` - Input value in range [0.0, 1.0]
/// * `steepness` - Controls the curve shape:
/// - `steepness = 0.0`: Linear interpolation (y = x)
/// - `steepness > 0.0`: Convex curve (slow start → fast end)
/// - `steepness < 0.0`: Concave curve (fast start → slow end)
/// - Typical range: -5.0 to 5.0 for most applications
///
/// # Returns
/// * Transformed value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_exponential;
/// let convex = interpolate_exponential(0.5, 2.0); // Slow start, fast end
/// let concave = interpolate_exponential(0.5, -2.0); // Fast start, slow end
/// let linear = interpolate_exponential(0.5, 0.0); // Linear (returns 0.5)
/// ```
///
/// # Plots
/// Steepness = -3
/// Steepness = -3
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠔⠒⠒⠉⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
/// 0.0 1.0
/// ```
///
/// Steepness = -1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡲⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 0
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢂⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⢀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢂⣀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 3
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⣀⣀⠤⠤⠒⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠉⠉⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
if steepness.abs() < 1e-10 {
// When a is very close to zero, the function becomes linear
return normalized_value;
}
let numerator = steepness.exp().powf(normalized_value) - 1.0;
let denominator = steepness.exp_m1();
clip(numerator / denominator)
}
/// A piecewise exponential interpolation function that creates smooth S-curves
/// through the points (0,0), (0.5,0.5), and (1,1).
///
/// The function combines two normalized exponential segments:
/// - First half (0 to 0.5): inverted exponential (concave, fast start → slow end)
/// - Second half (0.5 to 1): standard exponential (convex, slow start → fast end)
///
/// This creates an S-like curve that's concave then convex, similar to
/// a smoothed step function or sigmoid-like transition.
///
/// # Arguments
/// * `x` - Input value, should be in range [0.0, 1.0]
/// * `steepness` - Controls how steep the S-curve is
/// - `steepness = 0.0`: Linear interpolation (y = x)
/// - `steepness > 0.0`: More pronounced S-curve
/// - Higher values create sharper transitions at the boundaries
/// - Typical range: 0.5 to 5.0 for most applications
///
/// # Returns
/// * Interpolated y value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_inflected_exponential;
/// let y = interpolate_inflected_exponential(0.25, 2.0); // Gentle S-curve
/// let y = interpolate_inflected_exponential(0.75, 5.0); // Sharp S-curve
/// ```
///
/// # Plots
/// Steepness = 0.5
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢀⠤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡢⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡲⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 2
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠒⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 5
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠉⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠤⠤⠤⠤⠤⠤⠒⠒⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⢀⠔⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⣁⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
debug_assert!((0.0..=1.0).contains(&x), "x must be in range [0.0, 1.0]");
debug_assert!(steepness >= 0.0, "steepness must be non-negative");
if steepness == 0.0 {
// Linear case
return x;
}
if x <= 0.5 {
// First segment: inverted exponential (concave)
// Maps [0, 0.5] → [0, 0.5] with concave shape
let x_norm = 2.0 * x; // Scale to [0, 1]
// Use inverted exponential interpolation
let result = 1.0 - interpolate_exponential(1.0 - x_norm, steepness);
0.5 * result
} else {
// Second segment: standard exponential (convex)
// Maps [0.5, 1] → [0.5, 1] with convex shape
let x_norm = 2.0 * (x - 0.5); // Scale to [0, 1]
// Use standard exponential interpolation
let result = interpolate_exponential(x_norm, steepness);
0.5f64.mul_add(result, 0.5)
}
}
/// Inverts a value from the range [0, 1] to [1, 0].
///
/// This function flips a normalized value, where 0 becomes 1 and 1 becomes 0.
/// Values outside [0, 1] are first clamped to this range before inversion.
///
/// # Arguments
/// * `value` - Input value (will be clamped to [0.0, 1.0])
///
/// # Returns
/// * Inverted value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::invert;
/// let inverted = invert(0.3); // Returns 0.7
/// let boundary = invert(0.0); // Returns 1.0
/// let clamped = invert(1.5); // Returns 0.0 (input clamped to 1.0 first)
/// ```
pub fn invert(value: f64) -> f64 {
let value = clip(value);
1.0 - value
}
/// Clips a value to the range [0, 1].
///
/// This function ensures that any input value is constrained to the
/// normalized range [0, 1] by clamping values below 0 to 0 and
/// values above 1 to 1.
///
/// # Arguments
/// * `value` - Input value to clip
///
/// # Returns
/// * Clipped value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::clip;
/// let normal = clip(0.5); // Returns 0.5
/// let low = clip(-0.3); // Returns 0.0
/// let high = clip(1.7); // Returns 1.0
/// ```
pub fn clip(value: f64) -> f64 {
if value < 0.0 {
return 0.0;
} else if value > 1.0 {
return 1.0;
}
value
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const EPSILON: f64 = 1e-10;
#[test]
fn test_scale() {
// Basic normalization with same types
assert_relative_eq!(normalize(5.0f64, 0.0f64, 10.0f64), 0.5, epsilon = EPSILON);
assert_relative_eq!(normalize(5.0f32, 0.0f32, 10.0f32), 0.5, epsilon = EPSILON);
assert_relative_eq!(normalize(5i32, 0i32, 10i32), 0.5, epsilon = EPSILON);
assert_relative_eq!(normalize(5u8, 0u8, 10u8), 0.5, epsilon = EPSILON);
// Edge cases
assert_relative_eq!(normalize(0.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(normalize(10.0, 0.0, 10.0), 1.0, epsilon = EPSILON);
// Out of range (should clamp)
assert_relative_eq!(normalize(-5.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(normalize(15.0, 0.0, 10.0), 1.0, epsilon = EPSILON);
// Different ranges
assert_relative_eq!(normalize(0.0, -10.0, 10.0), 0.5, epsilon = EPSILON);
assert_relative_eq!(normalize(5.0, -5.0, 5.0), 1.0, epsilon = EPSILON);
}
#[test]
fn test_interpolate_linear() {
// Basic interpolation - f64
let result_f64: f64 = scale(0.5, 0.0, 10.0);
assert_relative_eq!(result_f64, 5.0, epsilon = EPSILON);
// For non-f64 return types, we need to use a separate implementation
// that uses `as` for the conversion
// For testing f32 output with f32 input - use direct calculation
let normalized_value = 0.5;
let new_min_f32 = 0.0f32;
let new_max_f32 = 10.0f32;
let expected_f32 = (normalized_value * (new_max_f32 as f64 - new_min_f32 as f64)
+ new_min_f32 as f64) as f32;
assert_relative_eq!(expected_f32, 5.0f32, epsilon = EPSILON as f32);
// Testing i32 return type with direct calculation
let new_min_i32 = 0i32;
let new_max_i32 = 10i32;
let expected_i32 = (normalized_value * (new_max_i32 as f64 - new_min_i32 as f64)
+ new_min_i32 as f64) as i32;
assert_eq!(expected_i32, 5i32);
// Edge cases
assert_relative_eq!(scale(0.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(scale(1.0, 0.0, 10.0), 10.0, epsilon = EPSILON);
// Negative ranges
assert_relative_eq!(scale(0.5, -10.0, 10.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(scale(0.5, -20.0, -10.0), -15.0, epsilon = EPSILON);
}
#[test]
fn test_interpolate_exponential() {
// Test boundary points (0,0) and (1,1) for various values of a
for a in [-10.0, -5.0, -1.0, -0.1, 0.1, 1.0, 5.0, 10.0].iter() {
assert_relative_eq!(interpolate_exponential(0.0, *a), 0.0, epsilon = EPSILON);
assert_relative_eq!(interpolate_exponential(1.0, *a), 1.0, epsilon = EPSILON);
}
// Test behavior for x outside [0,1]
let a = 2.0;
assert_relative_eq!(interpolate_exponential(-0.1, a), 0.0, epsilon = EPSILON);
assert_relative_eq!(interpolate_exponential(1.1, a), 1.0, epsilon = EPSILON);
// Test monotonicity - function should be strictly increasing for any a
for a in [-10.0, -1.0, 0.0, 1.0, 10.0].iter() {
let mut prev = interpolate_exponential(0.0, *a);
for i in 1..=10 {
let x = i as f64 / 10.0;
let current = interpolate_exponential(x, *a);
assert!(
current > prev,
"Failed monotonicity test at x={}, a={}",
x,
a
);
prev = current;
}
}
// Test symmetry property: f(x, a) + f(1-x, -a) ≈ 1
for x in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].iter() {
for a in [0.5, 1.0, 2.0, 5.0].iter() {
let sum = interpolate_exponential(*x, *a) + interpolate_exponential(1.0 - *x, -*a);
assert_relative_eq!(sum, 1.0, epsilon = 1e-8);
}
}
}
#[test]
fn test_invert() {
assert_relative_eq!(invert(0.0), 1.0, epsilon = EPSILON);
assert_relative_eq!(invert(1.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(invert(0.3), 0.7, epsilon = EPSILON);
assert_relative_eq!(invert(0.5), 0.5, epsilon = EPSILON);
// Test clipping instead of panicking
assert_relative_eq!(invert(-0.1), 1.0, epsilon = EPSILON);
assert_relative_eq!(invert(1.1), 0.0, epsilon = EPSILON);
}
#[test]
fn test_clip() {
assert_relative_eq!(clip(0.5), 0.5, epsilon = EPSILON);
assert_relative_eq!(clip(0.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(clip(1.0), 1.0, epsilon = EPSILON);
assert_relative_eq!(clip(-0.5), 0.0, epsilon = EPSILON);
assert_relative_eq!(clip(1.5), 1.0, epsilon = EPSILON);
}
#[test]
fn test_interpolate_inflected_exponential() {
// Test boundary points (0,0), (0.5,0.5), and (1,1) for various steepness values
for steepness in [0.0, 0.5, 1.0, 2.0, 5.0].iter() {
assert_relative_eq!(
interpolate_inflected_exponential(0.0, *steepness),
0.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_inflected_exponential(0.5, *steepness),
0.5,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_inflected_exponential(1.0, *steepness),
1.0,
epsilon = EPSILON
);
}
// Test linear case (steepness = 0)
for x in [0.0, 0.25, 0.5, 0.75, 1.0].iter() {
assert_relative_eq!(
interpolate_inflected_exponential(*x, 0.0),
*x,
epsilon = EPSILON
);
}
// Test monotonicity - function should be strictly increasing
for steepness in [0.5, 1.0, 2.0, 5.0].iter() {
let mut prev = interpolate_inflected_exponential(0.0, *steepness);
for i in 1..=20 {
let x = i as f64 / 20.0;
let current = interpolate_inflected_exponential(x, *steepness);
assert!(
current >= prev,
"Failed monotonicity test at x={}, steepness={}, prev={}, current={}",
x,
steepness,
prev,
current
);
prev = current;
}
}
// Test S-curve property: first half should be concave, second half convex
let steepness = 2.0;
// First half: check concave property (derivative decreasing)
let x1 = 0.1;
let x2 = 0.2;
let x3 = 0.3;
let y1 = interpolate_inflected_exponential(x1, steepness);
let y2 = interpolate_inflected_exponential(x2, steepness);
let y3 = interpolate_inflected_exponential(x3, steepness);
let slope1 = (y2 - y1) / (x2 - x1);
let slope2 = (y3 - y2) / (x3 - x2);
assert!(
slope1 > slope2,
"First half should be concave (decreasing slope)"
);
// Second half: check convex property (derivative increasing)
let x1 = 0.7;
let x2 = 0.8;
let x3 = 0.9;
let y1 = interpolate_inflected_exponential(x1, steepness);
let y2 = interpolate_inflected_exponential(x2, steepness);
let y3 = interpolate_inflected_exponential(x3, steepness);
let slope1 = (y2 - y1) / (x2 - x1);
let slope2 = (y3 - y2) / (x3 - x2);
assert!(
slope1 < slope2,
"Second half should be convex (increasing slope)"
);
// Test that output is always in [0, 1] range
for steepness in [0.0, 1.0, 5.0, 10.0].iter() {
for i in 0..=100 {
let x = i as f64 / 100.0;
let y = interpolate_inflected_exponential(x, *steepness);
assert!(
y >= 0.0 && y <= 1.0,
"Output {} out of range [0,1] for x={}, steepness={}",
y,
x,
steepness
);
}
}
}
#[test]
fn test_interpolate_hinge() {
// Test basic functionality with hinge at (0.3, 0.7)
let x_hinge = 0.3;
let y_hinge = 0.7;
// Test exact hinge point
assert_relative_eq!(
interpolate_hinge(x_hinge, x_hinge, y_hinge),
y_hinge,
epsilon = EPSILON
);
// Test left segment (value < x): should interpolate from (0,0) to (x,y)
assert_relative_eq!(
interpolate_hinge(0.0, x_hinge, y_hinge),
0.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.15, x_hinge, y_hinge),
0.35, // 0.15/0.3 * 0.7 = 0.5 * 0.7 = 0.35
epsilon = EPSILON
);
// Test right segment (value > x): should interpolate from (x,y) to (1,1)
assert_relative_eq!(
interpolate_hinge(1.0, x_hinge, y_hinge),
1.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.65, x_hinge, y_hinge),
0.85, // y + ((0.65-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + 0.5 * 0.3 = 0.85
epsilon = EPSILON
);
// Test with different hinge points
// Hinge at (0.5, 0.5) - should be linear
assert_relative_eq!(interpolate_hinge(0.25, 0.5, 0.5), 0.25, epsilon = EPSILON);
assert_relative_eq!(interpolate_hinge(0.75, 0.5, 0.5), 0.75, epsilon = EPSILON);
// Hinge at (0.2, 0.9) - steep left, shallow right
assert_relative_eq!(
interpolate_hinge(0.1, 0.2, 0.9),
0.45, // 0.1/0.2 * 0.9 = 0.5 * 0.9 = 0.45
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.6, 0.2, 0.9),
0.95, // 0.9 + ((0.6-0.2)/(1.0-0.2)) * (1.0-0.9) = 0.9 + 0.5 * 0.1 = 0.95
epsilon = EPSILON
);
// Test edge cases
// Hinge at (0, 0)
assert_relative_eq!(interpolate_hinge(0.0, 0.0, 0.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(
interpolate_hinge(0.5, 0.0, 0.0),
0.5, // Should interpolate from (0,0) to (1,1)
epsilon = EPSILON
);
// Hinge at (1, 1)
assert_relative_eq!(
interpolate_hinge(0.5, 1.0, 1.0),
0.5, // Should interpolate from (0,0) to (1,1)
epsilon = EPSILON
);
assert_relative_eq!(interpolate_hinge(1.0, 1.0, 1.0), 1.0, epsilon = EPSILON);
// Test with different numeric types
assert_relative_eq!(
interpolate_hinge(0.5f32, 0.3f32, 0.7f32),
0.7857142857142857, // 0.7 + ((0.5-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + (2/7) * 0.3
epsilon = 1e-7 // Relaxed epsilon for f32 precision
);
assert_relative_eq!(
interpolate_hinge(1u8, 0u8, 1u8), // 1.0, 0.0, 1.0 - should return 1.0
1.0,
epsilon = EPSILON
);
// Test monotonicity - function should be non-decreasing
let test_x = 0.4;
let test_y = 0.6;
let mut prev = interpolate_hinge(0.0, test_x, test_y);
for i in 1..=20 {
let value = i as f64 / 20.0;
let current = interpolate_hinge(value, test_x, test_y);
assert!(
current >= prev,
"Failed monotonicity test at value={}, x={}, y={}, prev={}, current={}",
value,
test_x,
test_y,
prev,
current
);
prev = current;
}
}
#[test]
fn test_interpolate_exponential_graph() {
use textplots::{Chart, Plot, Shape};
println!("\n=== Exponential Interpolation Examples ===");
// Test different steepness values (both positive and negative)
let steepness_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
for steepness in steepness_values.iter() {
println!("\nSteepness = {}", steepness);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_exponential(x, *steepness);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
fn test_interpolate_hinge_graph() {
use textplots::{Chart, Plot, Shape};
println!("\n=== hinge Interpolation Examples ===");
// Test different hinge points
let hinges = [(0.3, 0.7), (0.5, 0.5), (0.2, 0.9), (0.8, 0.2)];
for &(x_hinge, y_hinge) in hinges.iter() {
println!("\nHinge at ({}, {})", x_hinge, y_hinge);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_hinge(x, x_hinge, y_hinge);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
fn test_interpolate_inflected_exponential_graph() {
use textplots::{Chart, Plot, Shape};
// Test different steepness values
let steepness_values = [0.5, 1.0, 2.0, 5.0];
for steepness in steepness_values.iter() {
println!("\nSteepness = {}", steepness);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_inflected_exponential(x, *steepness);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
#[should_panic]
fn test_normalize_invalid_range() {
normalize(5.0, 10.0, 5.0); // min >= max should panic
}
}
+6
View File
@@ -0,0 +1,6 @@
pub mod compare_lists;
pub mod hasher_serializer;
pub mod hashing;
pub mod interpolation;
pub mod moving_time_window;
pub mod retry;
@@ -0,0 +1,243 @@
use std::{
collections::VecDeque,
iter::Sum,
time::{Duration, Instant},
};
/// A helper trait to handle division for averaging
pub trait DivideByCount {
fn divide_by_count(self, count: usize) -> Self;
}
impl DivideByCount for f64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
impl DivideByCount for f32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
impl DivideByCount for i32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
impl DivideByCount for i64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
impl DivideByCount for u32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
impl DivideByCount for u64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as Self)
}
}
// Type aliases for common use cases
pub type F64Window = MovingTimeWindow<f64>;
pub type F32Window = MovingTimeWindow<f32>;
pub type I32Window = MovingTimeWindow<i32>;
pub type I64Window = MovingTimeWindow<i64>;
#[derive(Debug)]
pub struct MovingTimeWindow<T>
where
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
{
max_samples: usize,
samples: VecDeque<(T, Instant)>,
last_sample: Option<(T, Instant)>,
sample_time: Duration,
cached_average: Option<T>,
cached_max: Option<T>,
cached_min: Option<T>,
}
/// A time-based moving window that maintains samples within a specified duration and count limit.
///
/// This structure efficiently tracks values over time, automatically removing samples that fall
/// outside the configured time window or exceed the maximum sample count. It provides cached
/// average calculation to avoid redundant computations.
impl<T> MovingTimeWindow<T>
where
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
{
/// Creates a new moving time window with the specified duration and maximum sample count.
///
/// # Arguments
///
/// * `duration` - The time window duration; samples older than this will be automatically removed
/// * `samples` - Maximum number of samples to retain, regardless of their age
///
/// # Returns
///
/// A new `MovingTimeWindow` instance with empty sample collection and initialized cache
pub fn new(duration: Duration, max_samples: usize) -> Self {
Self {
max_samples,
samples: VecDeque::with_capacity(max_samples),
last_sample: None,
sample_time: duration / (max_samples as u32),
cached_average: None,
cached_max: None,
cached_min: None,
}
}
/// Updates the window with a new sample value at the given timestamp.
///
/// This method adds the new sample to the window and automatically removes samples that:
/// - Are older than the configured duration from the current timestamp
/// - Exceed the maximum sample count limit
///
/// The cached average is invalidated after this operation.
///
/// # Arguments
///
/// * `value` - The numeric value to add to the window
/// * `timestamp` - The time at which this sample was taken
pub fn update(&mut self, value: T, timestamp: Instant) {
// Chekc if we should add a new sample based on the sample time and the last sample
if let Some((_, last_timestamp)) = self.last_sample {
if timestamp.duration_since(last_timestamp) < self.sample_time {
// If the new sample is too close to the last one, we skip adding it
return;
}
}
// If we have reached the maximum sample count, remove the oldest sample
if self.samples.len() >= self.max_samples {
self.samples.pop_back();
}
// Add the new sample
self.samples.push_front((value, timestamp));
self.last_sample = Some((value, timestamp));
// Invalidate all cached values since we added a new sample
self.cached_average = None;
self.cached_max = None;
self.cached_min = None;
}
/// Calculates and returns the average of all samples currently in the window.
///
/// This method uses caching to avoid redundant calculations. The average is only
/// recalculated when the window contents have changed since the last call.
///
/// # Returns
///
/// The arithmetic mean of all current samples, or T::default() if the window is empty
pub fn average(&mut self) -> T {
// Return cached value if available
if let Some(cached) = self.cached_average {
return cached;
}
let sum: T = self.samples.iter().map(|(v, _)| *v).sum();
let count = self.samples.len();
let avg = if count > 0 {
sum.divide_by_count(count)
} else {
T::default()
};
// Cache the calculated value
self.cached_average = Some(avg);
avg
}
/// Calculates and returns the maximum value of all samples currently in the window.
///
/// This method uses caching to avoid redundant calculations. The maximum is only
/// recalculated when the window contents have changed since the last call.
///
/// # Returns
///
/// The maximum value of all current samples, or T::default() if the window is empty
pub fn max(&mut self) -> T {
// Return cached value if available
if let Some(cached) = self.cached_max {
return cached;
}
let result = self
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_max| {
if val > current_max {
Some(val)
} else {
Some(current_max)
}
})
})
.unwrap_or_default();
// Cache the calculated value
self.cached_max = Some(result);
result
}
/// Calculates and returns the minimum value of all samples currently in the window.
///
/// This method uses caching to avoid redundant calculations. The minimum is only
/// recalculated when the window contents have changed since the last call.
///
/// # Returns
///
/// The minimum value of all current samples, or T::default() if the window is empty
pub fn min(&mut self) -> T {
// Return cached value if available
if let Some(cached) = self.cached_min {
return cached;
}
let result = self
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_min| {
if val < current_min {
Some(val)
} else {
Some(current_min)
}
})
})
.unwrap_or_default();
// Cache the calculated value
self.cached_min = Some(result);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic() {
// Test that the struct compiles with basic numeric types
let _: MovingTimeWindow<f64> = MovingTimeWindow::new(Duration::from_secs(1), 1);
let _: MovingTimeWindow<f32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
let _: MovingTimeWindow<i32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
}
}
+427
View File
@@ -0,0 +1,427 @@
use std::future::Future;
/// Retries a fallible operation up to `n` times synchronously.
///
/// This function executes the provided closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - All retry attempts are exhausted
///
/// # Arguments
///
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
/// * `f` - A mutable closure that returns a `Result<T, E>`
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt if all retries are exhausted.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_n_times;
///
/// let mut attempt = 0;
/// let result = retry_n_times(3, || {
/// attempt += 1;
/// if attempt < 3 {
/// Err("failed")
/// } else {
/// Ok("success")
/// }
/// });
/// assert_eq!(result, Ok("success"));
/// ```
pub fn retry_n_times<T, E, F>(n: usize, mut f: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let mut last_err: Option<E> = None;
for _ in 0..=n {
match f() {
Ok(value) => return Ok(value),
Err(err) => last_err = Some(err),
}
}
Err(last_err.unwrap())
}
/// Retries a fallible async operation up to `n` times.
///
/// This function executes the provided async closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - All retry attempts are exhausted
///
/// Each attempt is awaited before proceeding to the next retry or returning.
///
/// # Arguments
///
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt if all retries are exhausted.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_n_times_async;
/// use std::sync::Arc;
/// use std::sync::atomic::{AtomicUsize, Ordering};
///
/// async fn example() {
/// let attempt = Arc::new(AtomicUsize::new(0));
/// let attempt_clone = attempt.clone();
/// let result = retry_n_times_async(3, move || {
/// let attempt = attempt_clone.clone();
/// async move {
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
/// if current < 3 {
/// Err("failed")
/// } else {
/// Ok("success")
/// }
/// }
/// }).await;
/// assert_eq!(result, Ok("success"));
/// }
/// ```
pub async fn retry_n_times_async<T, E, F, Fut>(n: usize, mut f: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut last_err: Option<E> = None;
for _ in 0..=n {
match f().await {
Ok(value) => return Ok(value),
Err(err) => last_err = Some(err),
}
}
Err(last_err.unwrap())
}
/// Retries a fallible operation until a closure decides to stop retrying.
///
/// This function executes the provided closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - The retry condition returns false
///
/// # Arguments
///
/// * `f` - A mutable closure that returns a `Result<T, E>`
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt when retries stop.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_conditionally;
///
/// let mut attempt = 0;
/// let result = retry_conditionally(
/// || {
/// attempt += 1;
/// if attempt < 3 {
/// Err(format!("failed attempt {}", attempt))
/// } else {
/// Ok("success")
/// }
/// },
/// |err| {
/// // Retry up to 3 times
/// !err.contains("attempt 3")
/// }
/// );
/// assert_eq!(result, Ok("success"));
/// ```
pub fn retry_conditionally<T, E, F, R>(mut f: F, mut should_retry: R) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
R: FnMut(&E) -> bool,
{
loop {
match f() {
Ok(value) => return Ok(value),
Err(err) => {
if !should_retry(&err) {
return Err(err);
}
}
}
}
}
/// Retries a fallible async operation until a closure decides to stop retrying.
///
/// This function executes the provided async closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - The retry condition returns false
///
/// # Arguments
///
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt when retries stop.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_conditionally_async;
/// use std::sync::Arc;
/// use std::sync::atomic::{AtomicUsize, Ordering};
///
/// async fn example() {
/// let attempt = Arc::new(AtomicUsize::new(0));
/// let attempt_clone = attempt.clone();
/// let result = retry_conditionally_async(
/// move || {
/// let attempt = attempt_clone.clone();
/// async move {
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
/// if current < 3 {
/// Err(format!("failed attempt {}", current))
/// } else {
/// Ok("success")
/// }
/// }
/// },
/// |err| {
/// // Retry up to 3 times
/// !err.contains("attempt 3")
/// }
/// ).await;
/// assert_eq!(result, Ok("success"));
/// }
/// ```
pub async fn retry_conditionally_async<T, E, F, Fut, R>(
mut f: F,
mut should_retry: R,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
R: FnMut(&E) -> bool,
{
loop {
match f().await {
Ok(value) => return Ok(value),
Err(err) => {
if !should_retry(&err) {
return Err(err);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
// Simple blocking executor for async functions in tests
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use std::thread;
struct DummyWaker;
impl std::task::Wake for DummyWaker {
fn wake(self: Arc<Self>) {}
}
let waker = Waker::from(Arc::new(DummyWaker));
let mut context = Context::from_waker(&waker);
let mut future = Box::pin(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(result) => return result,
Poll::Pending => {
thread::yield_now();
}
}
}
}
#[test]
fn test_retry_success_first_attempt() {
let result = retry_n_times(3, || Ok::<i32, &str>(42));
assert_eq!(result, Ok(42));
}
#[test]
fn test_retry_success_after_failures() {
let mut attempt = 0;
let result = retry_n_times(3, || {
attempt += 1;
if attempt < 3 { Err("failed") } else { Ok(42) }
});
assert_eq!(result, Ok(42));
assert_eq!(attempt, 3);
}
#[test]
fn test_retry_all_attempts_fail() {
let mut attempt = 0;
let result: Result<i32, &str> = retry_n_times(2, || {
attempt += 1;
Err("always fails")
});
assert_eq!(result, Err("always fails"));
assert_eq!(attempt, 3); // 0..=2 means 3 attempts
}
#[test]
fn test_retry_zero_retries() {
let mut attempt = 0;
let result: Result<i32, &str> = retry_n_times(0, || {
attempt += 1;
Err("failed")
});
assert_eq!(result, Err("failed"));
assert_eq!(attempt, 1); // Only one attempt
}
#[test]
fn test_retry_async_success_first_attempt() {
let result = block_on(retry_n_times_async(3, || async { Ok::<i32, &str>(42) }));
assert_eq!(result, Ok(42));
}
#[test]
fn test_retry_async_success_after_failures() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result = block_on(retry_n_times_async(3, move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
if *a < 3 { Err("failed") } else { Ok(42) }
}
}));
assert_eq!(result, Ok(42));
assert_eq!(*attempt.borrow(), 3);
}
#[test]
fn test_retry_async_all_attempts_fail() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, &str> = block_on(retry_n_times_async(2, move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
Err("always fails")
}
}));
assert_eq!(result, Err("always fails"));
assert_eq!(*attempt.borrow(), 3); // 0..=2 means 3 attempts
}
#[test]
fn test_retry_and_success_with_condition() {
let mut attempt = 0;
let result: Result<i32, String> = retry_conditionally(
|| {
attempt += 1;
if attempt < 3 {
Err(format!("failed attempt {}", attempt))
} else {
Ok(42)
}
},
|err| {
// Retry as long as it's not the 3rd attempt
!err.contains("attempt 3")
},
);
assert_eq!(result, Ok(42));
assert_eq!(attempt, 3);
}
#[test]
fn test_retry_and_stop_early() {
let mut attempt = 0;
let result: Result<i32, String> = retry_conditionally(
|| {
attempt += 1;
Err(format!("failed attempt {}", attempt))
},
|_err| {
// Stop after first failure
false
},
);
assert!(result.is_err());
assert_eq!(attempt, 1);
assert_eq!(result.unwrap_err(), "failed attempt 1");
}
#[test]
fn test_retry_async_success_with_condition() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, String> = block_on(retry_conditionally_async(
move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
if *a < 3 {
Err(format!("failed attempt {}", *a))
} else {
Ok(42)
}
}
},
|err| {
// Retry as long as it's not the 3rd attempt
!err.contains("attempt 3")
},
));
assert_eq!(result, Ok(42));
assert_eq!(*attempt.borrow(), 3);
}
#[test]
fn test_retry_async_stop_early() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, String> = block_on(retry_conditionally_async(
move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
Err(format!("failed attempt {}", *a))
}
},
|_err| {
// Stop after first failure
false
},
));
assert!(result.is_err());
assert_eq!(*attempt.borrow(), 1);
assert_eq!(result.unwrap_err(), "failed attempt 1");
}
}
+113
View File
@@ -0,0 +1,113 @@
#[cfg(target_os = "linux")]
use libc;
use std::ffi::CString;
use std::io;
use std::str;
#[cfg(target_os = "linux")]
fn read_proc_interrupts() -> Result<String, io::Error> {
let path = CString::new("/proc/interrupts").unwrap();
// O_RDONLY
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
if fd < 0 {
return Err(io::Error::from_raw_os_error(-fd));
}
let mut buf: Vec<u8> = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let n = unsafe { libc::read(fd, chunk.as_mut_ptr() as *mut _, chunk.len()) };
if n < 0 {
let e = io::Error::from_raw_os_error(-n as i32);
unsafe { libc::close(fd) };
return Err(e);
} else if n == 0 {
break;
} else {
buf.extend_from_slice(&chunk[..n as usize]);
}
}
unsafe { libc::close(fd) };
Ok(String::from_utf8_lossy(&buf).into_owned())
}
#[cfg(target_os = "linux")]
fn get_interface_irq(proc_content: &str, interface_name: &str) -> Option<u32> {
for line in proc_content.lines() {
if line.contains(interface_name) {
if let Some(colon_pos) = line.find(':') {
let head = &line[..colon_pos].trim();
// try parse head as integer IRQ number
if let Ok(irq) = head.parse::<u32>() {
return Some(irq);
}
}
}
}
None
}
/// Since kernel 3.0 it’s possible to use the /proc/irq/<IRQ-NUMBER>/smp_affinity_list
/// With comma seperated values
/// This function takes the irq identifier and writes the cpu string
/// into /proc/irq/irq_number/smp_affinity_list
#[cfg(target_os = "linux")]
fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
let path: String = format!("/proc/irq/{}/smp_affinity_list", irq);
let cpath = CString::new(path.clone()).unwrap();
// We want to completely overwrite the smp affinities, so that we guarentee execution on the specified cores
let file_descriptor = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) };
if file_descriptor < 0 {
tracing::info!("Permission denied to write: {}", path);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("failed to open {}: errno {}", path, -file_descriptor),
));
}
// convert to "raw bytes"
// cpu needs to be a string because we need the raw ascii code 48 = '0' and so on as bytes
let bytes = cpu.as_bytes();
let mut bytes_written_total = 0usize;
while bytes_written_total < bytes.len() {
let to_write = &bytes[bytes_written_total..];
let bytes_written = unsafe {
libc::write(
file_descriptor,
to_write.as_ptr() as *const _,
to_write.len(),
)
};
if bytes_written < 0 {
let e = io::Error::from_raw_os_error(-bytes_written as i32);
unsafe { libc::close(file_descriptor) };
return Err(e);
}
bytes_written_total += bytes_written as usize;
}
unsafe { libc::close(file_descriptor) };
Ok(())
}
/// This is used to make sure that a given irq handler runs on the expected cpu
/// On Realtime systems this is the make or break fix for certain usecases like ethercrab
/// On our machines the 99.99th percentile of cycle times was very bad ~5-11 ms while ethercat is supposed to be in the microseconds
/// After pinning the ethernet irq to the core that the ethercat code ran on 99.99th percentile went down to ~200us (microseconds)
/// Example input: irq_name: "eno1" , cpu: 2
/// Remember that cpu cores are counted from 0
#[cfg(target_os = "linux")]
pub fn set_irq_affinity(irq_name: &str, cpu: u32) -> Result<(), anyhow::Error> {
let proc_contents = read_proc_interrupts()?;
let irq = get_interface_irq(&proc_contents, irq_name);
if irq.is_none() {
return Err(anyhow::anyhow!("Couldnt find irq number!"));
}
let res = set_irq_affinity_raw(irq.unwrap(), &cpu.to_string());
if res.is_err() {
return Err(anyhow::anyhow!("Couldnt write affinity list!"));
} else {
return Ok(());
}
}
+13 -1
View File
@@ -1,8 +1,20 @@
pub mod actors;
pub mod controllers;
pub mod converters;
pub mod downcast;
pub mod ethercat;
pub mod helpers;
pub mod irq_handling;
pub mod machines;
pub mod modbus;
pub mod realtime;
pub mod rest;
pub mod serial;
pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
+22 -2
View File
@@ -1,7 +1,27 @@
use crate::socketio::namespace::NamespaceInterface;
use serde_json::Value;
use smol::lock::Mutex;
use std::sync::Arc;
use crate::socketio::namespace::Namespace;
pub trait MachineApi {
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> &mut dyn NamespaceInterface;
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>>;
/// Returns a list of available video stream identifiers for this machine
#[cfg(feature = "video-streaming")]
fn api_video_streams(&self) -> Vec<String> {
Vec::new()
}
/// Get a video stream receiver for the specified stream ID
/// Returns None if the stream doesn't exist
#[cfg(feature = "video-streaming")]
fn api_get_video_stream(
&mut self,
stream_id: &str,
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
let _ = stream_id;
None
}
}
+208
View File
@@ -0,0 +1,208 @@
use crate::downcast::Downcast;
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::manager::MachineManager;
use crate::socketio::{event::GenericEvent, namespace::Namespace};
use serde::Serialize;
use smol::block_on;
use smol::lock::RwLock;
use std::sync::Arc;
use std::sync::Weak;
use super::Machine;
use anyhow::anyhow;
use smol::{channel::Sender, lock::Mutex};
use socketioxide::extract::SocketRef;
#[derive(Debug)]
pub enum MachineConnection<M: Machine + ?Sized>
where
Self: Sized,
{
Error(anyhow::Error),
Disconnected,
Connected(Arc<Mutex<M>>),
}
#[derive(Debug)]
pub struct MachineSlot<M: Machine + ?Sized>
where
Self: Sized,
{
pub machine_connection: MachineConnection<M>,
pub namespace: Arc<Mutex<Namespace>>,
}
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
impl<M: Machine> Clone for MachineConnection<M> {
fn clone(&self) -> Self {
match self {
Self::Connected(m) => Self::Connected(m.clone()),
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
Self::Disconnected => Self::Disconnected,
}
}
}
impl<M: Machine + ?Sized> MachineConnection<M> {
pub fn to_error(&self) -> Option<anyhow::Error> {
match self {
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
_ => None,
}
}
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
match self {
Self::Connected(machine) => Some(machine.clone()),
_ => None,
}
}
}
impl<M: Machine + ?Sized> MachineSlot<M> {
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
let namespace = Namespace::new(socket_queue_tx);
Self {
machine_connection: MachineConnection::Disconnected,
namespace: Arc::new(Mutex::new(namespace)),
}
}
pub const fn is_connected(&self) -> bool {
matches!(self.machine_connection, MachineConnection::Connected(_))
}
}
pub trait CrossConnectableMachine<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
>: Machine
{
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
}
#[derive(Debug)]
pub struct MachineCrossConnection<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
> {
machine_manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: MachineIdentificationUnique,
pub connected_machine: Weak<Mutex<MachineSlot<T>>>,
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
// need F in our type arguemnts. We are ok with
// it being used only in a fake field. Without
// F being fixed, we would not be anble to implement
// reverse connecting and disconnecting properly,
// as the type on the other side would decay
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct MachineCrossConnectionState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
impl<F: CrossConnectableMachine<F, T>, T: CrossConnectableMachine<T, F>>
MachineCrossConnection<F, T>
{
pub fn new(
manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: &MachineIdentificationUnique,
) -> Self {
Self {
machine_manager: manager,
connected_machine: Weak::new(),
machine_identification_unique: machine_identification_unique.clone(),
_make_compiler_happy: std::marker::PhantomData,
}
}
pub fn set_connected_machine(
&mut self,
machine_identification_unique: &MachineIdentificationUnique,
) {
if let Some(machine_manager) = self.machine_manager.upgrade() {
let machine_manager = block_on(machine_manager.read());
let slot: Arc<Mutex<MachineSlotGeneric>> =
match machine_manager.get(&machine_identification_unique) {
Some(c) => c,
None => return,
};
let slot: Arc<Mutex<MachineSlot<T>>> = match Downcast::downcast(&slot) {
Ok(s) => s,
Err(_) => return,
};
self.connected_machine = Arc::downgrade(&slot);
} else {
panic!("The machine manager has died, cannot do anything at this point");
}
}
pub fn reverse_connect(&self) {
if let Some(slot) = self.connected_machine.upgrade() {
let slot = &mut slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut machine = machine.lock_blocking();
let cross_connection = machine.get_cross_connection();
if !cross_connection.is_connected() {
cross_connection.set_connected_machine(&self.machine_identification_unique);
}
};
}
}
pub fn disconnect(&mut self) {
self.connected_machine = Weak::new();
}
pub fn reverse_disconnect(&mut self) {
if let Some(slot) = self.connected_machine.upgrade() {
let slot = slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut machine = machine.lock_blocking();
let cross_connection = machine.get_cross_connection();
cross_connection.disconnect();
};
}
}
pub fn is_connected(&self) -> bool {
let arc = self.connected_machine.upgrade();
arc.is_some()
}
pub fn to_state(&self) -> MachineCrossConnectionState {
if let Some(slot) = self.connected_machine.upgrade() {
let connection = &slot.lock_blocking().machine_connection;
if let MachineConnection::Connected(machine) = connection {
let machine = machine.lock_blocking();
return MachineCrossConnectionState {
machine_identification_unique: Some(
machine.get_machine_identification_unique(),
),
is_available: true,
};
}
}
MachineCrossConnectionState {
machine_identification_unique: None,
is_available: false,
}
}
}
+27 -11
View File
@@ -1,3 +1,5 @@
use std::fmt::Display;
use serde::Deserialize;
use serde::Serialize;
@@ -10,11 +12,21 @@ pub struct MachineIdentificationUnique {
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub fn is_valid(&self) -> bool {
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
@@ -24,12 +36,12 @@ pub struct MachineIdentification {
impl MachineIdentification {
/// Check if values are non-zero
pub fn is_valid(&self) -> bool {
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
@@ -37,7 +49,7 @@ pub struct DeviceMachineIdentification {
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub fn is_valid(&self) -> bool {
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
@@ -47,7 +59,7 @@ impl DeviceMachineIdentification {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
@@ -55,14 +67,14 @@ pub struct DeviceIdentification {
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
DeviceIdentification {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
@@ -78,20 +90,24 @@ impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
module_path!()
))?;
Ok(DeviceIdentificationIdentified {
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
// UsbDevice...,
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
+165 -28
View File
@@ -1,22 +1,41 @@
use smol::lock::Mutex;
use crate::machines::{
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
};
use std::collections::HashMap;
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use crate::{
machines::{
connection::{
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
},
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
},
serial::SerialDevice,
socketio::event::GenericEvent,
};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines:
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
pub serial_machines:
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
pub ethercat_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
pub serial_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
}
impl Default for MachineManager {
fn default() -> Self {
Self::new()
}
}
impl MachineManager {
@@ -32,16 +51,14 @@ impl MachineManager {
device_identifications: &Vec<DeviceIdentification>,
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
// empty ethercat machines
self.ethercat_machines.clear();
// group devices by machine device identification
let device_grouping_result = group_devices_by_identification(device_identifications);
log::info!(
"[{}::set_ethercat_devices] Device Groups {:?}",
module_path!(),
tracing::info!(
"Device Groups {}",
device_grouping_result.device_groups.len()
);
@@ -50,32 +67,152 @@ impl MachineManager {
// iterate over all identified device groups but ignore unaffected machines
for device_group in device_grouping_result.device_groups.iter() {
// get the machine identification
let machine_identification = match device_group.first() {
Some(device_identification) => &device_identification.device_machine_identification,
None => continue, // Skip this group if empty
};
let machine_identification_unique: MachineIdentificationUnique =
match device_group.first() {
Some(device_identification) => device_identification
.device_machine_identification
.machine_identification_unique
.clone(),
None => continue, // Skip this group if empty
};
let slot =
self.get_or_create_slot(socket_queue_tx.clone(), machine_identification_unique);
let mut slot = slot.lock_blocking();
// create the machine
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group,
hardware: &machine_new_hardware,
socket_queue_tx: socket_queue_tx.clone(),
machine_manager: machine_manager.clone(),
namespace: slot.namespace.clone(),
});
// insert the machine into the ethercat machines map
self.ethercat_machines.insert(
machine_identification.machine_identification_unique.clone(),
new_machine,
);
slot.machine_connection = match new_machine {
Err(err) => MachineConnectionGeneric::Error(err),
Ok(machine) => MachineConnectionGeneric::Connected(machine),
};
}
}
fn get_or_create_slot(
&mut self,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_identification: MachineIdentificationUnique,
) -> Arc<Mutex<MachineSlotGeneric>> {
if let Some(slot) = self.get(&machine_identification) {
return slot;
}
let slot = Arc::new(Mutex::new(MachineSlot::new(socket_queue_tx)));
self.ethercat_machines
.insert(machine_identification, slot.clone());
return slot;
}
pub fn get(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<&Result<Box<Mutex<dyn Machine>>, anyhow::Error>> {
) -> Option<Arc<Mutex<MachineSlotGeneric>>> {
self.ethercat_machines
.get(machine_identification)
.or_else(|| self.serial_machines.get(machine_identification))
.map(|x| x.clone())
}
pub fn add_serial_device(
&mut self,
device_identification: &DeviceIdentification,
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
let hardware = MachineNewHardwareSerial { device };
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
let machine_identification: MachineIdentificationUnique = device_identification_identified
.device_machine_identification
.machine_identification_unique
.clone();
let slot = self.get_or_create_slot(socket_queue_tx.clone(), machine_identification);
let mut slot = slot.lock_blocking();
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group: &vec![device_identification_identified.clone()],
hardware: &MachineNewHardware::Serial(&hardware),
socket_queue_tx,
machine_manager: machine_manager.clone(),
namespace: slot.namespace.clone(),
});
slot.machine_connection = match new_machine {
Err(err) => MachineConnectionGeneric::Error(err),
Ok(machine) => MachineConnectionGeneric::Connected(machine),
};
tracing::info!("Adding serial machine {:?}", slot);
}
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
tracing::info!(
"Removing serial machine {:?}",
device_identification_identified
);
let machine_identification_unique = &device_identification_identified
.device_machine_identification
.machine_identification_unique;
if let Some(slot) = self.serial_machines.get(machine_identification_unique) {
let mut slot = slot.lock_blocking();
slot.machine_connection = MachineConnection::Disconnected;
}
}
pub fn get_ethercat_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
self.get_weak(&self.ethercat_machines, machine_identification)
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
self.get_weak(&self.serial_machines, machine_identification)
}
fn get_weak(
&self,
machines: &HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let slot = machines.get(machine_identification).clone();
return slot.and_then(|slot| {
let connection = &slot.lock_blocking().machine_connection;
match connection {
MachineConnectionGeneric::Error(_) => None,
MachineConnectionGeneric::Disconnected => None,
MachineConnectionGeneric::Connected(machine) => Some(Arc::downgrade(&machine)),
}
});
}
}
+9 -7
View File
@@ -1,22 +1,25 @@
use std::collections::HashMap;
use std::sync::Arc;
use smol::lock::Mutex;
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
use crate::machines::connection::MachineSlotGeneric;
use super::{identification::MachineIdentificationUnique, manager::MachineManager};
pub enum MachineManagerIter<'a> {
EthercatMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
Arc<Mutex<MachineSlotGeneric>>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
Arc<Mutex<MachineSlotGeneric>>,
>,
},
Done,
@@ -24,14 +27,13 @@ pub enum MachineManagerIter<'a> {
pub struct MachineManagerIterator<'a> {
iter: MachineManagerIter<'a>,
serial_machines:
&'a HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
serial_machines: &'a HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
}
impl<'a> Iterator for MachineManagerIterator<'a> {
type Item = (
&'a MachineIdentificationUnique,
&'a Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
&'a Arc<Mutex<MachineSlotGeneric>>,
);
fn next(&mut self) -> Option<Self::Item> {
@@ -71,7 +73,7 @@ impl<'a> Iterator for MachineManagerIterator<'a> {
impl MachineManager {
// Returns an iterator over all machines (both ethercat and serial)
pub fn iter(&self) -> MachineManagerIterator {
pub fn iter(&self) -> MachineManagerIterator<'_> {
MachineManagerIterator {
iter: MachineManagerIter::EthercatMachines {
iter: self.ethercat_machines.iter(),
+12 -3
View File
@@ -1,14 +1,23 @@
use crate::actors::Actor;
use api::MachineApi;
use new::MachineNewTrait;
use std::any::Any;
use std::fmt::Debug;
use crate::machines::{
api::MachineApi, identification::MachineIdentificationUnique, new::MachineAct,
};
pub mod api;
pub mod connection;
pub mod identification;
pub mod manager;
pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
+48 -6
View File
@@ -1,24 +1,44 @@
use crate::{
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
serial::SerialDevice,
socketio::namespace::Namespace,
};
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
use ethercat_hal::{devices::EthercatDevice, types::EthercrabSubDevicePreoperational};
use ethercat_hal::{
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use ethercrab::{SubDevice, SubDeviceRef};
use smol::lock::RwLock;
use std::sync::Arc;
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use std::{
sync::{Arc, Weak},
time::Instant,
};
use crate::socketio::event::GenericEvent;
pub trait MachineNewTrait {
fn new<'maindevice, 'subdevices>(
params: &MachineNewParams<'maindevice, 'subdevices, '_, '_, '_, '_>,
) -> Result<Self, Error>
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant);
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
> where
'maindevice: 'machine_new_hardware,
@@ -32,7 +52,23 @@ pub struct MachineNewParams<
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub machine_manager: Weak<RwLock<MachineManager>>,
pub namespace: Arc<Mutex<Namespace>>,
}
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone()
}
}
pub enum MachineNewHardware<
@@ -40,6 +76,7 @@ pub enum MachineNewHardware<
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
> where
'maindevice: 'machine_new_hardware_etehrcat,
'subdevices: 'machine_new_hardware_etehrcat,
@@ -52,6 +89,7 @@ pub enum MachineNewHardware<
'ethercat_devices,
>,
),
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
}
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
@@ -60,6 +98,10 @@ pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_device
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
}
pub struct MachineNewHardwareSerial {
pub device: Arc<RwLock<dyn SerialDevice>>,
}
// validates that all devices in the group have the same machine identification
pub fn validate_same_machine_identification_unique(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
+12 -44
View File
@@ -1,19 +1,21 @@
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
use anyhow::Error;
use smol::lock::{Mutex, RwLock};
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::Arc,
};
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Box<Mutex<dyn Machine>>, Error> + Send + Sync>;
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
}
impl Default for MachineRegistry {
fn default() -> Self {
Self::new()
}
}
impl MachineRegistry {
pub fn new() -> Self {
Self {
@@ -28,10 +30,10 @@ impl MachineRegistry {
self.type_map.insert(
TypeId::of::<T>(),
(
machine_identficiation,
machine_identficiation.clone(),
// create a machine construction closure
Box::new(|machine_new_params| {
Ok(Box::new(Mutex::new(T::new(machine_new_params)?)))
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
}),
),
);
@@ -40,7 +42,7 @@ impl MachineRegistry {
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Box<Mutex<dyn Machine>>, anyhow::Error> {
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
@@ -69,38 +71,4 @@ impl MachineRegistry {
// call machine new function by reference
(machine_new_closure)(machine_new_params)
}
pub fn downcast_arc_rwlock<T: Machine + 'static>(
&self,
machine: Arc<RwLock<dyn Machine>>,
) -> Result<Arc<RwLock<T>>, Error> {
if TypeId::of::<T>() == machine.type_id() {
// transmute Arc
let arc = unsafe { Arc::from_raw(Arc::into_raw(machine) as *const RwLock<T>) };
Ok(arc)
} else {
Err(anyhow::anyhow!(
"[{}::MachineConstructor::downcast] Machine is not of type {}",
module_path!(),
std::any::type_name::<T>()
))
}
}
pub fn downcast_box_rwlock<T: Machine + 'static>(
&self,
machine: Box<RwLock<dyn Machine>>,
) -> Result<RwLock<T>, Error> {
if TypeId::of::<T>() == machine.type_id() {
// transmute Box
let box_machine = unsafe { Box::from_raw(Box::into_raw(machine) as *mut RwLock<T>) };
Ok(*box_machine)
} else {
Err(anyhow::anyhow!(
"[{}::MachineConstructor::downcast] Machine is not of type {}",
module_path!(),
std::any::type_name::<T>()
))
}
}
}
+117 -50
View File
@@ -1,6 +1,9 @@
use std::time::Duration;
pub mod modbus_serial_interface;
use anyhow::Error;
use crc::{CRC_16_MODBUS, Crc};
use serialport::SerialPort;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParityType {
@@ -14,12 +17,66 @@ pub enum ParityType {
pub enum ModbusFunctionCode {
/// Read one or more Registers
ReadHoldingRegister,
/// Read Input register
ReadInputRegister,
/// write one Register Value
PresetHoldingRegister,
/// The response should echo back your request
DiagnoseFunction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ModbusExceptionCode {
None = 0,
IllegalFunction = 1,
IllegalDataAddress = 2,
IllegalDataValue = 3,
SlaveDeviceFailure = 4,
Acknowledge = 5,
SlaveDeviceBusy = 6,
MemoryParityError = 8,
GatewayPathUnavailable = 10,
GatewayTargetDeviceFailedToRespond = 11,
Unknown(u8), // For any unknown exception codes
}
impl From<ModbusExceptionCode> for u8 {
fn from(code: ModbusExceptionCode) -> Self {
match code {
ModbusExceptionCode::None => 0,
ModbusExceptionCode::IllegalFunction => 1,
ModbusExceptionCode::IllegalDataAddress => 2,
ModbusExceptionCode::IllegalDataValue => 3,
ModbusExceptionCode::SlaveDeviceFailure => 4,
ModbusExceptionCode::Acknowledge => 5,
ModbusExceptionCode::SlaveDeviceBusy => 6,
ModbusExceptionCode::MemoryParityError => 8,
ModbusExceptionCode::GatewayPathUnavailable => 10,
ModbusExceptionCode::GatewayTargetDeviceFailedToRespond => 11,
ModbusExceptionCode::Unknown(code) => code,
}
}
}
impl From<u8> for ModbusExceptionCode {
fn from(value: u8) -> Self {
match value {
0 => Self::None,
1 => Self::IllegalFunction,
2 => Self::IllegalDataAddress,
3 => Self::IllegalDataValue,
4 => Self::SlaveDeviceFailure,
5 => Self::Acknowledge,
6 => Self::SlaveDeviceBusy,
8 => Self::MemoryParityError,
10 => Self::GatewayPathUnavailable,
11 => Self::GatewayTargetDeviceFailedToRespond,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModbusRequest {
pub slave_id: u8,
@@ -35,32 +92,15 @@ pub struct ModbusResponse {
pub crc: u16,
}
impl ModbusResponse {
fn is_exception(self) -> bool {
if self.data.len() > 2 {
let function_code = self.data[1];
return (function_code & 0b10000000) > 0; // first bit is set when an exception happens in Modbus
}
return false;
}
fn get_exception_code(self) -> u8 {
if self.data.len() > 3 {
let exception = self.data[2];
return exception;
}
return 0;
}
}
impl TryFrom<u8> for ModbusFunctionCode {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x03 => Ok(ModbusFunctionCode::ReadHoldingRegister),
0x06 => Ok(ModbusFunctionCode::PresetHoldingRegister),
0x08 => Ok(ModbusFunctionCode::DiagnoseFunction),
0x03 => Ok(Self::ReadHoldingRegister),
0x04 => Ok(Self::ReadInputRegister),
0x06 => Ok(Self::PresetHoldingRegister),
0x08 => Ok(Self::DiagnoseFunction),
_ => Err(anyhow::anyhow!("Error: Modbus Function Code doesnt exist!")),
}
}
@@ -70,6 +110,7 @@ impl From<ModbusFunctionCode> for u8 {
fn from(value: ModbusFunctionCode) -> Self {
match value {
ModbusFunctionCode::ReadHoldingRegister => 0x03,
ModbusFunctionCode::ReadInputRegister => 0x04,
ModbusFunctionCode::PresetHoldingRegister => 0x06,
ModbusFunctionCode::DiagnoseFunction => 0x08,
}
@@ -78,7 +119,7 @@ impl From<ModbusFunctionCode> for u8 {
impl From<ModbusRequest> for Vec<u8> {
fn from(request: ModbusRequest) -> Self {
let mut buffer = Vec::new();
let mut buffer = Self::new();
buffer.push(request.slave_id);
buffer.push(request.function_code.into()); // convert functioncode into u8 and push it
@@ -91,63 +132,89 @@ impl From<ModbusRequest> for Vec<u8> {
// convert u16 value to le_bytes and add it to the end of the frame
buffer.extend_from_slice(&result_crc.to_le_bytes());
return buffer;
buffer
}
}
/// Reads data from a Modbus device via a serial port.
///
/// # Parameters
/// - `port`: A mutable reference to a serial port.
///
/// # Returns
/// A `Result` containing an `Option` with the raw Modbus response as a `Vec<u8>` if successful,
/// or `None` if the response is invalid or an error occurs.
pub fn receive_data_modbus(port: &mut dyn SerialPort) -> Result<Option<Vec<u8>>, anyhow::Error> {
let mut buf: [u8; 256] = [0; 256];
let data_length = port.read(&mut buf)?;
if data_length == 0 {
return Ok(None);
}
validate_modbus_response(buf[..data_length].to_vec()).map(Some)
}
fn validate_modbus_response(raw_data: Vec<u8>) -> Result<(), Error> {
if raw_data.len() == 0 {
/// Computes Modbus CRC-16 using `crc` crate.
pub const fn modbus_crc16(data: &[u8]) -> u16 {
let modbus: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
modbus.checksum(data)
}
/// Checks if the message's CRC is correct (uses Modbus CRC-16).
fn check_crc(raw_data: &Vec<u8>) -> Result<bool, Error> {
let expected_crc = extract_crc(raw_data)?;
let actual_crc = modbus_crc16(&raw_data[..raw_data.len() - 2]);
Ok(expected_crc == actual_crc)
}
fn validate_modbus_response(raw_data: Vec<u8>) -> Result<Vec<u8>, Error> {
if raw_data.is_empty() {
return Err(anyhow::anyhow!("Error: Response is Empty!"));
}
// 5 is the smallest possible Response Size
if raw_data.len() < 5 {
println!("raw_data.len() < 5");
if raw_data.len() <= 5 {
return Err(anyhow::anyhow!(
"Error: Response is invalid, its less than 5 bytes"
));
}
// check if the slave id is valid
if raw_data[0] < 1 || raw_data[0] > 247 {
return Err(anyhow::anyhow!(
"Error: Response is invalid, slave_id is outside of the valid range 1-247"
));
}
return Ok(());
if check_crc(&raw_data)? {
Ok(raw_data)
} else {
Err(anyhow::anyhow!("Error: CRC does not match"))
}
}
// expects to be given the entire raw message
fn extract_crc(raw_data: &Vec<u8>) -> Result<u16, Error> {
let raw_data1 = raw_data.clone();
let result = validate_modbus_response(raw_data1);
if let Err(error) = result {
return Err(error);
if raw_data.len() < 5 {
return Err(anyhow::anyhow!("Not enough data to extract CRC"));
}
let low_byte = raw_data[raw_data.len() - 2];
let high_byte = raw_data[raw_data.len() - 1];
let crc = u16::from_le_bytes([low_byte, high_byte]);
Ok(crc)
}
impl TryFrom<Vec<u8>> for ModbusResponse {
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> Result<ModbusResponse, Error> {
let crc = match extract_crc(&value) {
Ok(crc_value) => crc_value,
Err(err) => return Err(err),
};
fn try_from(value: Vec<u8>) -> Result<Self, Error> {
let crc = extract_crc(&value)?;
let function_code_res = ModbusFunctionCode::try_from(value[1]);
let function_code = match function_code_res {
Ok(code) => code,
Err(err) => return Err(err),
};
let function_code = function_code_res?;
Ok(ModbusResponse {
Ok(Self {
slave_id: value[0],
function_code: function_code,
function_code,
data: value[2..value.len() - 2].to_vec(), // get data without the crc
crc: crc,
crc,
})
}
}
@@ -159,23 +226,23 @@ impl TryFrom<Vec<u8>> for ModbusResponse {
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
/// baudrate: bits per second
/// message_size: size of original message in bytes
pub fn calculate_modbus_rtu_timeout(
pub const fn calculate_modbus_rtu_timeout(
bits: u8,
machine_operation_delay_nano: Duration,
baudrate: u32,
message_size: usize,
) -> Duration {
let nanoseconds_per_bit: u64 = (1000000 / baudrate) as u64;
let nanoseconds_per_byte: u64 = bits as u64 * nanoseconds_per_bit as u64;
let nanoseconds_per_byte: u64 = bits as u64 * nanoseconds_per_bit;
let transmission_timeout: u64 = nanoseconds_per_byte * message_size as u64;
let silent_time: u64 = (nanoseconds_per_byte * (35)) / 10 as u64; // silent_time is 3.5x of character length,which is 11 bit for 8E1
let silent_time: u64 = (nanoseconds_per_byte * (35)) / 10_u64; // silent_time is 3.5x of character length,which is 11 bit for 8E1
let mut full_timeout: u64 = transmission_timeout;
full_timeout += machine_operation_delay_nano.as_nanos() as u64;
full_timeout += silent_time;
return Duration::from_nanos(full_timeout);
Duration::from_nanos(full_timeout)
}
#[cfg(test)]
@@ -0,0 +1,268 @@
use crate::modbus::{ModbusRequest, ModbusResponse};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
struct RequestMetaData {
priority: u32,
ignored_times: u32,
/// This is used when a machine for example takes 4ms to process the request
/// and is added ontop of the standard waiting time for a serial transfer
extra_delay: Option<u32>,
no_response_expected: bool,
}
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Request we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub struct ModbusSerialInterface {
serial_interface: SerialInterface,
baudrate: Option<u32>,
encoding: Option<SerialEncoding>,
response: Option<ModbusResponse>,
state: State,
last_message_size: usize,
last_message_delay: u32,
pub last_message_id: u32,
no_response_expected: bool,
request_map: HashMap<u32, ModbusRequest>,
request_metadata_map: HashMap<u32, RequestMetaData>,
last_ts: Instant,
}
impl ModbusSerialInterface {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
baudrate: None,
encoding: None,
request_map: HashMap::new(),
request_metadata_map: HashMap::new(),
response: None,
last_message_size: 0,
last_message_delay: 0,
state: State::Uninitialized,
last_ts: Instant::now(),
last_message_id: 0,
no_response_expected: false,
}
}
pub const fn is_initialized(&self) -> bool {
!matches!(self.state, State::Uninitialized)
}
pub fn add_request(
&mut self,
request_id: u32,
priority: u32,
request: ModbusRequest,
no_response_expected: bool,
delay: Option<u32>,
) {
self.request_map.insert(request_id, request);
self.request_metadata_map
.entry(request_id)
.or_insert_with(|| RequestMetaData {
priority,
ignored_times: 0,
extra_delay: delay,
no_response_expected,
});
}
/// This is used internally to read the receive buffer of the el6021
async fn read_modbus_response(&mut self) -> Result<ModbusResponse, anyhow::Error> {
let raw_response = (self.serial_interface.read_message)()
.await
.unwrap_or_default();
let response = ModbusResponse::try_from(raw_response)?;
self.last_message_size = response.data.len() + 4;
self.state = State::WaitingForReceiveAccept;
Ok(response)
}
/// This is used internally to fill the write buffer of the el6021 with the modbus request
/// Decides what requests to send first by finding the one with the highest priority
/// For example Highest Priority requests: ResetInverter StopMotor
async fn send_modbus_request(&mut self) {
if self.request_map.is_empty() {
return;
}
let (highest_id, delay, no_response_expected) = self
.request_metadata_map
.iter_mut()
.map(|(key, value)| {
let effective_priority = value.priority + value.ignored_times;
(
*key,
effective_priority,
value.extra_delay.unwrap_or(0),
value.no_response_expected,
)
})
.max_by_key(|(_, priority, _, _)| *priority)
.map(|(key, _, delay, no_response)| (key, delay, no_response))
.unwrap(); // Safe because we checked is_empty() above
let request = &self.request_map[&highest_id];
let modbus_request: Vec<u8> = request.clone().into();
if let Err(_) = (self.serial_interface.write_message)(modbus_request.clone()).await {
tracing::error!("ERROR: serial_interface.write_message has failed");
} else {
self.last_message_delay = delay;
self.last_message_id = highest_id;
self.no_response_expected = no_response_expected;
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
}
pub async fn initialize_communication_settings(&mut self) -> bool {
let baudrate = (self.serial_interface.get_baudrate)().await;
let encoding = (self.serial_interface.get_serial_encoding)().await;
match (baudrate, encoding) {
(Some(b), Some(e)) => {
self.baudrate = Some(b);
self.encoding = Some(e);
true
}
_ => false,
}
}
pub async fn initialize(&mut self) -> bool {
if matches!(self.state, State::Uninitialized) {
let success = (self.serial_interface.initialize)().await;
if success {
self.state = State::ReadyToSend;
self.initialize_communication_settings().await;
}
success
} else {
false
}
}
fn increment_ignored_times(&mut self) {
for metadata in self.request_metadata_map.values_mut() {
metadata.ignored_times += 1;
}
}
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
/// This silent time is needed to identify the start and end of messages
/// This function also takes into account the time that the slave we are talking to needs to process our request
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
/// baudrate: bits per second
/// message_size: size of original message in bytes
pub fn calculate_modbus_rtu_timeout(
&self,
machine_operation_delay: Duration,
message_size: usize,
) -> Option<Duration> {
let baudrate = self.baudrate?;
let encoding = self.encoding?;
let nanoseconds_per_bit = 1_000_000_000 / baudrate as u64;
let nanoseconds_per_byte = encoding.total_bits() as u64 * nanoseconds_per_bit;
let transmission_timeout = nanoseconds_per_byte * message_size as u64;
let silent_time = (nanoseconds_per_byte * 35) / 10; // silent_time is 3.5x of character length
let total_timeout =
transmission_timeout + machine_operation_delay.as_nanos() as u64 + silent_time;
Some(Duration::from_nanos(total_timeout))
}
pub const fn get_response(&self) -> Option<&ModbusResponse> {
self.response.as_ref()
}
pub async fn act(&mut self, now_ts: Instant) {
let elapsed = now_ts.duration_since(self.last_ts);
let timeout = self.calculate_modbus_rtu_timeout(
Duration::from_nanos(self.last_message_delay as u64),
self.last_message_size,
);
let Some(timeout) = timeout else { return };
// Check if we need to wait for timeout (except for specific states)
if !matches!(
self.state,
State::Uninitialized | State::WaitingForReceiveAccept | State::WaitingForRequestAccept
) && elapsed < timeout
{
return;
}
self.last_ts = now_ts;
self.response = None;
match self.state {
State::WaitingForResponse => match self.read_modbus_response().await {
Ok(response) => {
self.response = Some(response);
}
Err(_) => {
self.response = None;
if self.no_response_expected {
self.state = State::ReadyToSend;
}
}
},
State::ReadyToSend => {
self.send_modbus_request().await;
// Remove the sent request
self.request_map.remove(&self.last_message_id);
self.request_metadata_map.remove(&self.last_message_id);
self.increment_ignored_times();
}
State::WaitingForReceiveAccept => {
// Waste at least one Ethercat Cycle here to ensure that request/response stay in sync
self.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
// An empty vec is used to check if we are finished with writing the message
// This is to keep the Serialinterface more simple
match (self.serial_interface.write_message)(vec![]).await {
Ok(finished) if finished => {
self.state = State::WaitingForResponse;
}
Err(_) => {
self.state = State::ReadyToSend;
}
_ => {} // Still waiting
}
}
State::Uninitialized => {}
}
}
}
+178
View File
@@ -0,0 +1,178 @@
use tracing::debug;
use tracing::error;
#[cfg(target_os = "linux")]
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
///
/// This function configures the current thread with real-time scheduling properties by:
///
/// 1. Setting the scheduling policy to SCHED_FIFO (First-In-First-Out), which is a real-time
/// scheduling policy that allows the thread to run until it voluntarily yields, blocks on I/O,
/// or is preempted by a higher-priority real-time thread.
///
/// 2. Setting the real-time priority to 80 (on a scale of 1-99, where higher values indicate
/// higher priority). This priority level is chosen to be higher than most IRQ handlers
/// (typically priority 50) but lower than critical kernel tasks (priority 99).
///
/// When running on a PREEMPT_RT patched Linux kernel, this combination provides deterministic
/// scheduling with minimal latency, which is essential for real-time applications.
///
/// # Requirements
///
/// - The program must run with appropriate permissions to set real-time priorities.
/// This typically means either:
/// - Running as root
/// - Configuring `/etc/security/limits.conf` to allow the user to set real-time priorities
///
/// - The system should be running a Linux kernel with PREEMPT_RT patch applied for
/// optimal real-time performance.
///
/// # Example
///
/// ```ignore
/// fn main() {
/// // Lock memory to prevent page faults
/// lock_memory();
///
/// // Create a thread and make it real-time
/// std::thread::spawn(|| {
/// set_realtime_priority();
///
/// // Real-time code here
/// });
/// }
/// ```
///
/// # Returns
///
/// - `Ok(())` if the real-time priority was successfully set
/// - `Err` with the error message if setting the priority failed
#[cfg(target_os = "linux")]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
use anyhow::anyhow;
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
use std::mem;
unsafe {
// Get the pthread_t of the current thread using pthread_self()
let pthread_self = libc::pthread_self();
// Set up the scheduling parameters
let mut param: sched_param = mem::zeroed();
param.sched_priority = 95;
// Set the thread to use SCHED_FIFO scheduling policy with our priority
let result = pthread_setschedparam(pthread_self, SCHED_FIFO, &param);
if result != 0 {
error!(
"Failed to set real-time scheduling policy for thread \"{:?}\": {}",
std::thread::current().name().unwrap_or("unknown"),
std::io::Error::last_os_error()
);
return Err(anyhow!(
"Failed to set real-time priority: {}",
std::io::Error::last_os_error()
));
} else {
debug!(
"Successfully set real-time priority for the thread \"{:?}\"",
std::thread::current().name().unwrap_or("unknown")
);
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
tracing::error!("This platform does not support realtime threads");
Ok(())
}
/// Locks all current and future memory pages of the process into RAM to prevent page faults.
///
/// # When to call this function
///
/// This function should be called from the main thread, as early as possible in your program's
/// execution, ideally before creating any threads and before allocating large amounts of memory.
///
/// Important: You only need to call this function ONCE per process, not in each thread.
/// The memory locking applies to the entire process, including:
/// - The main thread
/// - All threads created after the call
///
/// This happens because all threads in a process share the same virtual memory space, and
/// `mlockall()` operates on the entire process's memory space, not just the calling thread.
///
/// # Example: Correct usage
///
/// ```ignore
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Call early in main, before creating threads
/// lock_memory()?;
///
/// // Now create real-time threads...
/// std::thread::spawn(|| {
/// set_realtime_priority();
/// // Real-time code here
/// });
///
/// Ok(())
/// }
/// ```
///
/// # Example: Incorrect usage
///
/// ```ignore
/// fn main() {
/// // Don't do this - calling in each thread is unnecessary
/// std::thread::spawn(|| {
/// lock_memory().unwrap(); // Redundant if already called in main
/// set_realtime_priority();
/// });
/// }
/// ```
///
/// # How it works
///
/// [Rest of documentation as before...]
#[cfg(target_os = "linux")]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
use libc::{MCL_FUTURE, mlockall};
unsafe {
if mlockall(MCL_FUTURE) != 0 {
error!("Failed to lock memory: {}", std::io::Error::last_os_error());
return Err(
format!("Failed to lock memory: {}", std::io::Error::last_os_error()).into(),
);
} else {
debug!("Successfully locked memory for the process");
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
tracing::error!("This platform does not support memory locking");
Ok(())
}
pub fn set_core_affinity(core_id: usize) -> Result<(), anyhow::Error> {
let cores = core_affinity::get_core_ids().unwrap_or_default();
let core = cores.get(core_id);
if let Some(core) = core {
core_affinity::set_for_current(*core);
debug!(
"Set core affinity of thread \"{:?}\" to core {:?}",
std::thread::current().name().unwrap_or("unknown"),
core
);
Ok(())
} else {
error!("No cores available to set affinity");
Err(anyhow::anyhow!("No cores available to set affinity"))
}
}
+18 -2
View File
@@ -11,13 +11,13 @@ pub struct MutationResponse {
}
impl MutationResponse {
pub fn success() -> Self {
pub const fn success() -> Self {
Self {
success: true,
error: None,
}
}
pub fn error(error: String) -> Self {
pub const fn error(error: String) -> Self {
Self {
success: false,
error: Some(error),
@@ -33,3 +33,19 @@ where
pub machine_identification_unique: MachineIdentificationUnique,
pub data: T,
}
#[derive(Debug, serde::Deserialize)]
pub struct MachineVideoStreamListBody {
pub machine_identification_unique: MachineIdentificationUnique,
}
#[derive(Debug, serde::Deserialize)]
pub struct MachineVideoStreamBody {
pub machine_identification_unique: MachineIdentificationUnique,
pub stream_id: String,
}
#[derive(Debug, Serialize)]
pub struct VideoStreamListResponse {
pub streams: Vec<String>,
}
+33
View File
@@ -0,0 +1,33 @@
use std::{any::Any, sync::Arc};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::machines::identification::DeviceIdentification;
pub mod registry;
pub mod serial_detection;
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
pub trait SerialDeviceNew {
fn new_serial(
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
where
Self: Sized;
}
pub trait SerialDeviceThread {
fn start_thread() -> Result<(), anyhow::Error>;
}
pub struct SerialDeviceNewParams {
pub path: String,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
+96
View File
@@ -0,0 +1,96 @@
use super::SerialDeviceNewParams;
use crate::{
machines::identification::DeviceIdentification,
serial::{SerialDevice, SerialDeviceIdentification},
};
use anyhow::{Error, Result};
use smol::lock::RwLock;
use std::{any::TypeId, collections::HashMap, sync::Arc};
pub type SerialDeviceNewClosure = Arc<
dyn Fn(
&SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>), Error>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct SerialDeviceRegistry {
pub type_map: HashMap<TypeId, (SerialDeviceIdentification, SerialDeviceNewClosure)>,
}
impl Default for SerialDeviceRegistry {
fn default() -> Self {
Self::new()
}
}
impl SerialDeviceRegistry {
pub fn new() -> Self {
Self {
type_map: HashMap::new(),
}
}
pub fn register<T: SerialDevice + 'static>(
&mut self,
serial_device_identification: SerialDeviceIdentification,
) {
self.type_map.insert(
TypeId::of::<T>(),
(
serial_device_identification,
Arc::new(move |params| {
let (identification, device) = T::new_serial(params)?;
Ok((identification, device))
}),
),
);
}
pub fn new_serial_device(
&self,
serial_device_new_params: &SerialDeviceNewParams,
serial_device_identification: &SerialDeviceIdentification,
) -> Result<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>), anyhow::Error> {
// find serial new function by comparing ProdutConfig
let (_, serial_new_fn) = self
.type_map
.values()
.find(|(sdi, _)| sdi == serial_device_identification)
.ok_or(anyhow::anyhow!(
"[{}::MachineConstructor::new_machine] Machine not found",
module_path!()
))?;
// call machine new function by reference
(serial_new_fn)(serial_device_new_params)
}
pub async fn downcast_arc_rwlock<T: SerialDevice + 'static>(
&self,
serial_device: Arc<RwLock<dyn SerialDevice>>,
) -> Result<Arc<RwLock<T>>, Error> {
// Use the Any trait for type checking
let type_id = {
let type_id_fn = Arc::new(|device: &dyn SerialDevice| device.type_id());
let guard = serial_device.read().await;
let id = type_id_fn(&*guard);
drop(guard);
id
};
if TypeId::of::<T>() == type_id {
// transmute Arc
let arc = unsafe { Arc::from_raw(Arc::into_raw(serial_device) as *const RwLock<T>) };
Ok(arc)
} else {
Err(anyhow::anyhow!(
"[{}::MachineConstructor::downcast] Machine is not of type {}",
module_path!(),
std::any::type_name::<T>()
))
}
}
}
@@ -0,0 +1,76 @@
/*
*@author: Alisher Darmenov
*@company: QiTech
*@created: 20.04.2025
*
*@description: This module is responsible for usb detection and validation, specially made with serialport to avoid complexity and size of tokio_serial
*/
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
use std::collections::HashMap;
use super::SerialDeviceIdentification;
/// Handles scanning and caching of connected USB serial devices.
pub struct SerialDetection {}
impl SerialDetection {
/// Returns all serial ports on the system (minus macOS "cu" noise).
fn get_ports() -> Vec<SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports
.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect(),
Err(_) => Vec::new(),
}
}
/// Extracts only USB serial devices from the port list.
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
let mut map = HashMap::new();
for port in port_list {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
map.insert(port.port_name, usb_info);
}
}
map
}
/// Scans for connected USB serial devices and updates the internal map.
/// Returns a clone of the map for quick access.
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
let ports = Self::get_ports();
let usb_devices = Self::extract_usb_serial_devices(ports);
usb_devices
}
pub fn device_port_exists(device_path: &str) -> bool {
let ports = Self::get_ports();
return ports.iter().any(|p| p.port_name == device_path);
}
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
let ports = Self::get_ports();
for port in ports {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
return true;
}
}
}
return false;
}
pub fn get_path_by_id(
sdevid: SerialDeviceIdentification,
map: HashMap<String, UsbPortInfo>,
) -> Option<String> {
for (port, usbport) in map.iter() {
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
return Some(port.clone());
}
}
return None;
}
}
+31 -21
View File
@@ -1,20 +1,34 @@
use std::time::{SystemTime, UNIX_EPOCH};
use erased_serde::Serialize as ErasedSerialize;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
#[derive(Serialize)]
pub struct GenericEvent {
pub name: String,
pub data: Value,
pub data: Box<dyn ErasedSerialize + Send + Sync>,
/// Timestamp in milliseconds
pub ts: u64,
}
impl std::fmt::Debug for GenericEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GenericEvent")
.field("name", &self.name)
.field("data", &"[erased]")
.field("ts", &self.ts)
.finish()
}
}
pub trait BuildEvent: Serialize + Send + Sync + Clone {
fn build(&self) -> Event<Self>;
}
#[derive(Debug, Clone, Serialize)]
pub struct Event<T>
where
T: Serialize,
T: Serialize + Send + Sync + 'static,
{
pub name: String,
pub data: T,
@@ -22,39 +36,35 @@ where
pub ts: u64,
}
impl<T> TryFrom<Event<T>> for GenericEvent
impl<T> From<Event<T>> for GenericEvent
where
T: Serialize,
T: Serialize + Send + Sync + 'static,
{
type Error = serde_json::Error;
fn try_from(event: Event<T>) -> Result<Self, Self::Error> {
Ok(Self {
fn from(event: Event<T>) -> Self {
Self {
name: event.name,
data: serde_json::to_value(event.data)?,
data: Box::new(event.data),
ts: event.ts,
})
}
}
}
impl<T> TryFrom<&Event<T>> for GenericEvent
impl<T> From<&Event<T>> for GenericEvent
where
T: Serialize,
T: Serialize + Clone + Send + Sync + 'static,
{
type Error = serde_json::Error;
fn try_from(event: &Event<T>) -> Result<Self, Self::Error> {
Ok(Self {
fn from(event: &Event<T>) -> Self {
Self {
name: event.name.clone(),
data: serde_json::to_value(&event.data)?,
data: Box::new(event.data.clone()),
ts: event.ts,
})
}
}
}
impl<T> Event<T>
where
T: Serialize + Clone,
T: Serialize + Clone + Send + Sync + 'static,
{
pub fn new(event: &str, data: T) -> Self {
Self {
+176 -115
View File
@@ -1,25 +1,64 @@
use crate::socketio::event::GenericEvent;
use smol::channel::Sender;
use socketioxide::extract::SocketRef;
use std::{
alloc::System,
collections::HashMap,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tracing::instrument;
pub trait NamespaceInterface {
#[derive(Debug)]
pub struct Namespace {
pub sockets: Vec<SocketRef>,
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
}
impl Namespace {
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
Self {
sockets: vec![],
events: HashMap::new(),
socket_queue_tx,
}
}
}
impl Namespace {
/// Adds a socket to the namespace.
///
/// # Arguments
///
/// * `socket` - A reference to the socket to be added
fn subscribe(&mut self, socket: SocketRef);
#[instrument(skip_all)]
pub fn subscribe(&mut self, socket: SocketRef) {
// add the socket to the list
self.sockets.push(socket);
}
/// Removes a socket from the namespace.
///
/// # Arguments
///
/// * `socket` - A reference to the socket to be removed
fn unsubscribe(&mut self, socket: SocketRef);
#[instrument(skip_all)]
pub fn unsubscribe(&mut self, socket: SocketRef) {
// remove the socket from the list
self.sockets.retain(|s| s.id != socket.id);
}
/// Disconnects all clients in the namespace.
///
/// This will forcefully disconnect all sockets and clear the socket list.
#[instrument(skip_all)]
pub fn disconnect_all(&mut self) {
tracing::info!("Disconnecting {} sockets in namespace", self.sockets.len());
// Disconnect each socket
for socket in &self.sockets {
let _ = socket.clone().disconnect(); // Ignore errors if socket is already disconnected
}
// Clear the socket list
self.sockets.clear();
}
/// Re-emits cached events to a specific socket.
///
@@ -29,14 +68,23 @@ pub trait NamespaceInterface {
/// # Arguments
///
/// * `socket` - A reference to the socket that will receive the cached events
fn reemit(&mut self, socket: SocketRef);
#[instrument(skip_all)]
pub fn reemit(&mut self, socket: SocketRef) {
// Collect events grouped by name/kind with their counts for sorting
let mut event_groups: Vec<(&String, &Vec<Arc<GenericEvent>>)> =
self.events.iter().collect();
/// Emits an event to all sockets in the namespace.
///
/// # Arguments
///
/// * `event` - The event to be emitted
fn emit(&mut self, event: &GenericEvent);
// Sort by event count (ascending - lowest count first)
event_groups.sort_by(|a, b| a.1.len().cmp(&b.1.len()));
// Emit events in order of lowest count first
for (_event_name, events) in event_groups {
for event in events {
// Send to global queue instead of per-socket queue
self.send_to_queue(&socket, event, "reemit");
}
}
}
/// Caches an event with a specific key for later retrieval.
///
@@ -45,97 +93,81 @@ pub trait NamespaceInterface {
/// * `cache_key` - A string key to identify the cache
/// * `event` - The event to be cached
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
#[instrument(skip_all)]
fn cache(
&mut self,
event: &GenericEvent,
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
);
event: Arc<GenericEvent>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
) {
let cached_events_for_key = self.events.entry(event.name.clone()).or_default();
buffer_fn(cached_events_for_key, &event);
}
/// Emits an event to all sockets in the namespace and caches it.
///
/// This is a convenience method that combines the functionality of
/// [`Namespace::emit`] and [`Namespace::cache`].
///
/// # Arguments
///
/// * `event` - The event to be emitted and cached
/// * `cache_key` - A string key to identify the cache
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
fn emit_cached(
#[instrument(skip_all)]
pub fn emit(
&mut self,
event: &GenericEvent,
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
);
}
#[derive(Debug)]
pub struct Namespace {
sockets: Vec<SocketRef>,
events: HashMap<String, Vec<GenericEvent>>,
}
impl Namespace {
pub fn new() -> Self {
Self {
sockets: vec![],
events: HashMap::new(),
event: Arc<GenericEvent>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
) {
// cache the event
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
for socket in self.sockets.clone() {
self.send_to_queue(&socket, &event, "emit");
}
}
}
impl NamespaceInterface for Namespace {
fn subscribe(&mut self, socket: SocketRef) {
// add the socket to the list
self.sockets.push(socket.clone());
}
fn unsubscribe(&mut self, socket: SocketRef) {
// remove the socket from the list
self.sockets.retain(|s| s.id != socket.id);
}
fn reemit(&mut self, socket: SocketRef) {
for (_, events) in self.events.iter() {
log::debug!(
"Re-emitting {} {} events to socket {}",
events.len(),
events[0].name,
socket.id
);
for event in events.iter() {
let _ = socket.emit("event", &event);
/// Sends an event to the global queue for a specific socket.
///
/// # Arguments
///
/// * `socket` - The socket to send the event to
/// * `event` - The event to be sent
/// * `operation` - A description of the operation for logging purposes (e.g., "reemit", "emit")
#[instrument(skip_all)]
fn send_to_queue(&self, socket: &SocketRef, event: &Arc<GenericEvent>, operation: &str) {
tracing::trace!(
socket_id = ?socket.id,
event = %event.name,
operation = %operation,
"Sending event to global queue"
);
match self
.socket_queue_tx
.try_send((socket.clone(), event.clone()))
{
Ok(_) => {
tracing::trace!(
socket_id = ?socket.id,
event = %event.name,
operation = %operation,
"Successfully sent event to global queue"
);
}
Err(e) => {
tracing::error!(
socket_id = ?socket.id,
event = %event.name,
operation = %operation,
error = %e,
"Failed to send event to global queue"
);
}
}
}
}
fn emit(&mut self, event: &GenericEvent) {
for socket in self.sockets.iter() {
let _ = socket.emit("event", &event);
}
}
fn cache(
&mut self,
event: &GenericEvent,
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
) {
let mut cached_events_for_key = self
.events
.entry(event.name.clone())
.or_insert_with(Vec::new);
buffer_fn(&mut cached_events_for_key, event);
}
fn emit_cached(
&mut self,
event: &GenericEvent,
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
) {
// cache the event
self.cache(event, buffer_fn);
// emit the event
self.emit(event);
impl Drop for Namespace {
fn drop(&mut self) {
self.disconnect_all();
}
}
@@ -147,17 +179,17 @@ pub trait NamespaceCacheingLogic<V>
where
V: CacheableEvents<V>,
{
fn emit_cached(&mut self, event: V);
fn emit(&mut self, event: V);
}
pub trait CacheableEvents<Events> {
fn event_value(&self) -> Result<GenericEvent, serde_json::Error>;
fn event_value(&self) -> GenericEvent;
fn event_cache_fn(&self) -> CacheFn;
}
pub type CacheFn = Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>;
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>;
/// [BufferFn] that stores the last n events
/// [`BufferFn`] that stores the last n events
pub fn cache_n_events(n: usize) -> CacheFn {
Box::new(move |events, event| {
if events.len() >= n {
@@ -167,21 +199,41 @@ pub fn cache_n_events(n: usize) -> CacheFn {
})
}
/// [BufferFn] that stores only one event
/// [`BufferFn`] that stores only one event
pub fn cache_one_event() -> CacheFn {
cache_n_events(1)
}
/// [BufferFn] that stores events for a certain duration
/// [`BufferFn`] that stores first and last event
///
/// The primary use case of this function is to cache both the default state of a machine, which should be emitted first,
/// and the last event, which is the most recent state of the machine.
pub fn cache_first_and_last_event() -> CacheFn {
Box::new(move |events, event| {
// if the events length 0 or 1, we just push the event
if events.is_empty() || events.len() == 1 {
events.push(event.clone());
return;
}
// if the event length is 2 we remove the last event and append a new one
if events.len() == 2 {
events.remove(1);
events.push(event.clone());
}
})
}
/// [`BufferFn`] that stores events for a certain duration
pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
Box::new(move |events, event| {
// Use event.ts instead of system time
let current_time = event.ts as u128;
// calculate current bucket & last bucket
let bucket = current_time / bucket_size.as_millis();
let bucket_size_ms = bucket_size.as_millis().max(1);
let bucket = current_time / bucket_size_ms; // Avoid division by zero
let last_bucket = match events.last() {
Some(last_event) => last_event.ts as u128 / bucket_size.as_millis(),
Some(last_event) => last_event.ts as u128 / bucket_size_ms,
None => 0,
};
@@ -211,24 +263,32 @@ pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
mod tests {
use std::cmp::min;
use serde::Serialize;
use super::*;
#[derive(Debug, Clone, Serialize)]
struct TestEventData {
value: usize,
}
#[test]
fn test_cache_n_events() {
// Create a cache function that stores the last 2 events
let cache_fn = cache_n_events(2);
// Create namespace
let mut namespace = Namespace::new();
// Create namespace with dummy queue_tx
let (queue_tx, _queue_rx) = smol::channel::unbounded();
let mut namespace = Namespace::new(queue_tx);
assert!(namespace.events.is_empty());
// Add event
let event1 = GenericEvent {
let event1 = Arc::new(GenericEvent {
name: "test_event".to_string(),
data: serde_json::json!({"value": 1}),
data: Box::new(TestEventData { value: 1 }),
ts: 0,
};
namespace.cache(&event1, &cache_fn);
});
namespace.cache(event1, &cache_fn);
// Check that we have one event name in the map
assert_eq!(namespace.events.len(), 1);
@@ -237,12 +297,12 @@ mod tests {
assert_eq!(namespace.events.get("test_event").unwrap()[0].ts, 0);
// Add another event
let event2 = GenericEvent {
let event2 = Arc::new(GenericEvent {
name: "test_event".to_string(),
data: serde_json::json!({"value": 2}),
data: Box::new(TestEventData { value: 2 }),
ts: 1,
};
namespace.cache(&event2, &cache_fn);
});
namespace.cache(event2, &cache_fn);
// Still one event name
assert_eq!(namespace.events.len(), 1);
@@ -252,12 +312,12 @@ mod tests {
assert_eq!(namespace.events.get("test_event").unwrap()[1].ts, 1);
// Add a third event, which should remove the first one
let event3 = GenericEvent {
let event3 = Arc::new(GenericEvent {
name: "test_event".to_string(),
data: serde_json::json!({"value": 3}),
data: Box::new(TestEventData { value: 3 }),
ts: 2,
};
namespace.cache(&event3, &cache_fn);
});
namespace.cache(event3, &cache_fn);
// Still one event name
assert_eq!(namespace.events.len(), 1);
@@ -276,19 +336,20 @@ mod tests {
let duration = Duration::new(10, 0);
let bucket_size = Duration::new(1, 0);
let cache_fn = cache_duration(duration, bucket_size);
let (queue_tx, _queue_rx) = smol::channel::unbounded();
// Create namespace
let mut namespace = Namespace::new();
let mut namespace = Namespace::new(queue_tx);
assert!(namespace.events.is_empty());
// Add events every 100ms for 20 seconds
for i in 0..200 {
let event = GenericEvent {
let event = Arc::new(GenericEvent {
name: "test_event".to_string(),
data: serde_json::json!({"value": i}),
data: Box::new(TestEventData { value: i }),
ts: (i * 100) as u64,
};
namespace.cache(&event, &cache_fn);
});
namespace.cache(event, &cache_fn);
let should_have_events = min(i / 10, 11);
assert_eq!(
+7 -7
View File
@@ -17,8 +17,8 @@ impl Serialize for NamespaceId {
S: Serializer,
{
match self {
NamespaceId::Main => serializer.serialize_str("/main"),
NamespaceId::Machine(id) => {
Self::Main => serializer.serialize_str("/main"),
Self::Machine(id) => {
let path = format!(
"/machine/{}/{}/{}",
id.machine_identification.vendor, id.machine_identification.machine, id.serial
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for NamespaceId {
{
struct NamespaceIdVisitor;
impl<'de> Visitor<'de> for NamespaceIdVisitor {
impl Visitor<'_> for NamespaceIdVisitor {
type Value = NamespaceId;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
@@ -85,7 +85,7 @@ impl FromStr for NamespaceId {
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "/main" {
return Ok(NamespaceId::Main);
return Ok(Self::Main);
}
if let Some(machine_path) = s.strip_prefix("/machine/") {
@@ -101,7 +101,7 @@ impl FromStr for NamespaceId {
.parse::<u16>()
.map_err(|_| "Invalid serial id".to_string())?;
return Ok(NamespaceId::Machine(MachineIdentificationUnique {
return Ok(Self::Machine(MachineIdentificationUnique {
machine_identification: MachineIdentification { vendor, machine },
serial,
}));
@@ -116,8 +116,8 @@ impl FromStr for NamespaceId {
impl fmt::Display for NamespaceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NamespaceId::Main => write!(f, "/main"),
NamespaceId::Machine(id) => {
Self::Main => write!(f, "/main"),
Self::Machine(id) => {
write!(
f,
"/machine/{}/{}/{}",
+27
View File
@@ -0,0 +1,27 @@
use crate::transmission::Transmission;
#[derive(Debug, Clone)]
pub struct FixedTransmission {
ratio: f64,
}
impl Transmission for FixedTransmission {
fn get_ratio(&self) -> f64 {
self.ratio
}
}
impl FixedTransmission {
/// Creates a new FixedTransmission with the specified ratio.
///
/// # Arguments
///
/// * `ratio` - The transmission ratio (output over input).
///
/// # Returns
///
/// A new instance of FixedTransmission.
pub const fn new(ratio: f64) -> Self {
Self { ratio }
}
}
+95
View File
@@ -0,0 +1,95 @@
use uom::si::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
pub mod fixed;
/// A trait representing a mechanical transmission system that converts input to output
/// with a specific gear ratio.
///
/// This trait provides methods for working with transmission ratios and converting
/// between input and output values based on the transmission's gear ratio.
pub trait Transmission {
/// Returns the transmission ratio of this transmission system.
///
/// The ratio represents the relationship between input and output speeds/torques.
/// A ratio greater than 1.0 indicates speed reduction (torque multiplication),
/// while a ratio less than 1.0 indicates speed increase (torque reduction).
///
/// # Returns
///
/// The transmission ratio as a floating-point number.
fn get_ratio(&self) -> f64;
/// Converts an input value to the corresponding output value using the transmission ratio.
///
/// This method multiplies the input by the transmission ratio to calculate the output.
/// Typically used for converting input speed to output speed in a transmission system.
///
/// # Arguments
///
/// * `input` - The input value to be converted
///
/// # Returns
///
/// The output value after applying the transmission ratio.
fn calculate_output(&self, input: f64) -> f64 {
input * self.get_ratio()
}
/// Converts an output value to the corresponding input value using the transmission ratio.
///
/// This method divides the output by the transmission ratio to calculate the required input.
/// Typically used for determining what input is needed to achieve a desired output.
///
/// # Arguments
///
/// * `output` - The output value to be converted back to input
///
/// # Returns
///
/// The input value required to produce the given output.
fn calculate_input(&self, output: f64) -> f64 {
output / self.get_ratio()
}
fn calculate_angular_output(&self, input: Angle) -> Angle {
Angle::new::<radian>(self.calculate_output(input.get::<radian>()))
}
fn calculate_angular_input(&self, output: Angle) -> Angle {
Angle::new::<radian>(self.calculate_input(output.get::<radian>()))
}
fn calculate_angular_velocity_output(&self, input: AngularVelocity) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
self.calculate_output(input.get::<radian_per_second>()),
)
}
fn calculate_angular_velocity_input(&self, output: AngularVelocity) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
self.calculate_input(output.get::<radian_per_second>()),
)
}
fn calculate_linear_output(&self, input: Length) -> Length {
Length::new::<meter>(self.calculate_output(input.get::<meter>()))
}
fn calculate_linear_input(&self, output: Length) -> Length {
Length::new::<meter>(self.calculate_input(output.get::<meter>()))
}
fn calculate_linear_velocity_output(&self, input: Velocity) -> Velocity {
Velocity::new::<meter_per_second>(self.calculate_output(input.get::<meter_per_second>()))
}
fn calculate_linear_velocity_input(&self, output: Velocity) -> Velocity {
Velocity::new::<meter_per_second>(self.calculate_input(output.get::<meter_per_second>()))
}
}
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::acceleration;
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
"meters per minute per second";
}
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::angular_acceleration;
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
"revolutions per minute per second";
}
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::angular_jerk;
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
"revolution per minute per second squared";
}
+7
View File
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::jerk;
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
"meters per minute per second squared";
}
+5
View File
@@ -0,0 +1,5 @@
pub mod acceleration;
pub mod angular_acceleration;
pub mod angular_jerk;
pub mod jerk;
pub mod velocity;
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::velocity;
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
"meters per minute";
}
+208
View File
@@ -0,0 +1,208 @@
use anyhow::Error;
use std::collections::HashMap;
use tokio::sync::broadcast;
/// Video stream manager that handles multiple video streams per machine with true broadcasting
/// Uses tokio broadcast channels to ensure all subscribers receive all messages without stealing
#[derive(Debug)]
pub struct VideoStreamManager {
/// Each stream has its own broadcast transmitter and template receiver
streams: HashMap<String, (broadcast::Sender<Vec<u8>>, broadcast::Receiver<Vec<u8>>)>,
}
impl VideoStreamManager {
pub fn new() -> Self {
Self {
streams: HashMap::new(),
}
}
/// Send a frame to all subscribers of a stream
/// Returns Ok if the frame was sent, Err if the stream doesn't exist
pub fn send_frame(&self, stream_id: &str, frame: Vec<u8>) -> Result<(), Error> {
if let Some((tx, _rx)) = self.streams.get(stream_id) {
match tx.send(frame) {
Ok(_) => Ok(()),
Err(_) => Ok(()), // No receivers, but that's not an error
}
} else {
Err(anyhow::anyhow!("Stream '{}' not found", stream_id))
}
}
/// Register a new video stream with the given ID
pub fn add_stream(&mut self, stream_id: String) {
let (tx, rx) = broadcast::channel::<Vec<u8>>(256); // 256 frame buffer capacity
self.streams.insert(stream_id, (tx, rx));
}
/// Get list of available stream IDs
pub fn get_streams(&self) -> Vec<String> {
self.streams.keys().cloned().collect()
}
/// Check if a stream exists
pub fn has_stream(&self, stream_id: &str) -> bool {
self.streams.contains_key(stream_id)
}
/// Remove a stream and all its subscribers
pub fn remove_stream(&mut self, stream_id: &str) -> bool {
self.streams.remove(stream_id).is_some()
}
/// Subscribe to a video stream and get a receiver
/// Returns the receiver for the new subscription
pub fn subscribe_to_stream(&self, stream_id: &str) -> Option<broadcast::Receiver<Vec<u8>>> {
self.streams
.get(stream_id)
.map(|(_tx, rx)| rx.resubscribe()) // Resubscribe to create a new independent receiver
}
/// Get subscriber count for a stream
pub fn get_subscriber_count(&self, stream_id: &str) -> usize {
self.streams
.get(stream_id)
.map_or(0, |(tx, _rx)| tx.receiver_count())
}
/// Get a stream transmitter for sending frames (used by machines to send video)
/// Returns None if the stream doesn't exist
pub fn get_sender(&self, stream_id: &str) -> Option<broadcast::Sender<Vec<u8>>> {
self.streams.get(stream_id).map(|(tx, _rx)| tx.clone())
}
}
impl Default for VideoStreamManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_video_stream_manager() {
let mut manager = VideoStreamManager::new();
// Register streams
manager.register_stream("camera1".to_string());
manager.register_stream("camera2".to_string());
assert_eq!(manager.get_stream_ids().len(), 2);
assert!(manager.has_stream("camera1"));
assert!(manager.has_stream("camera2"));
assert!(!manager.has_stream("camera3"));
// Subscribe to streams
let mut rx1 = manager.subscribe_to_stream("camera1").unwrap();
let mut rx2 = manager.subscribe_to_stream("camera1").unwrap();
let mut rx3 = manager.subscribe_to_stream("camera2").unwrap();
assert_eq!(manager.get_subscriber_count("camera1"), 3); // Original + 2 clones
assert_eq!(manager.get_subscriber_count("camera2"), 2); // Original + 1 clone
assert_eq!(manager.get_subscriber_count("nonexistent"), 0);
// Send frames
let frame1 = vec![1, 2, 3, 4];
let frame2 = vec![5, 6, 7, 8];
assert!(manager.send_single_frame("camera1", frame1.clone()));
assert!(manager.send_single_frame("camera2", frame2.clone()));
assert!(!manager.send_single_frame("nonexistent", vec![]));
// Both camera1 subscribers should receive the frame
assert_eq!(rx1.recv().await.unwrap(), frame1);
assert_eq!(rx2.recv().await.unwrap(), frame1);
// Camera2 subscriber should receive its frame
assert_eq!(rx3.recv().await.unwrap(), frame2);
}
#[tokio::test]
async fn test_video_frame_broadcasting() {
let mut manager = VideoStreamManager::new();
manager.register_stream("main_camera".to_string());
// Multiple clients subscribe to the same camera
let mut rx1 = manager.subscribe_to_stream("main_camera").unwrap();
let mut rx2 = manager.subscribe_to_stream("main_camera").unwrap();
let mut rx3 = manager.subscribe_to_stream("main_camera").unwrap();
// Simulate video frames
let frames = vec![
vec![0xFF, 0xD8, 0xFF, 0xE0], // JPEG header
vec![0x00, 0x01, 0x02, 0x03], // Frame 1
vec![0x04, 0x05, 0x06, 0x07], // Frame 2
];
for frame in &frames {
assert!(manager.send_single_frame("main_camera", frame.clone()));
}
// All clients should receive all frames in order
for frame in &frames {
assert_eq!(rx1.recv().await.unwrap(), *frame);
assert_eq!(rx2.recv().await.unwrap(), *frame);
assert_eq!(rx3.recv().await.unwrap(), *frame);
}
}
#[test]
fn test_stream_management() {
let mut manager = VideoStreamManager::new();
// Initially no streams
assert_eq!(manager.get_stream_ids().len(), 0);
// Add streams
manager.register_stream("front_camera".to_string());
manager.register_stream("rear_camera".to_string());
manager.register_stream("side_camera".to_string());
let mut stream_ids = manager.get_stream_ids();
stream_ids.sort(); // Sort for consistent testing
assert_eq!(
stream_ids,
vec!["front_camera", "rear_camera", "side_camera"]
);
// Remove a stream
assert!(manager.remove_stream("rear_camera"));
assert!(!manager.remove_stream("rear_camera")); // Already removed
assert!(!manager.has_stream("rear_camera"));
assert_eq!(manager.get_stream_ids().len(), 2);
}
#[tokio::test]
async fn test_send_frame_results() {
let mut manager = VideoStreamManager::new();
manager.register_stream("test_camera".to_string());
// One initial receiver (the original one created in register_stream)
let result = manager.send_frame("test_camera", vec![1, 2, 3]).unwrap();
assert_eq!(result, 1); // Number of subscribers
// Add subscribers by cloning the receiver
let _rx1 = manager.subscribe_to_stream("test_camera").unwrap();
let _rx2 = manager.subscribe_to_stream("test_camera").unwrap();
// Send frame to multiple subscribers (original + 2 clones = 3 total)
let result = manager.send_frame("test_camera", vec![4, 5, 6]).unwrap();
assert_eq!(result, 3); // Number of subscribers
// Try sending to non-existent stream
let result = manager.send_frame("nonexistent", vec![7, 8, 9]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Stream 'nonexistent' not found");
}
#[test]
fn test_default_video_stream_manager() {
let manager = VideoStreamManager::default();
assert_eq!(manager.get_stream_ids().len(), 0);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+145
View File
@@ -0,0 +1,145 @@
# How can I add a new Machine?
Adding a new machine follows a common structure in `server/machines/`.
# TLDR
Summary
When adding a new machine:
- Create Folder server/machines/YOUR_MACHINE_NAME/.
- Add the 4 files (mod.rs, new.rs, act.rs, api.rs).
- Register it in machines/mod.rs with a unique constant.
- mod.rs: define the struct + emit functions.
- new.rs: assemble devices/controllers into the machine.
- act.rs: implement the machine loop (MachineAct).
- api.rs: define request (mutations) and response (events) types.
Tip: Start simple (copy MockMachine) and grow complexity as needed (add EtherCAT devices, controllers, etc.).
---
## 1. Create the folder
Create a new directory:
server/machines/YOUR_MACHINE_NAME
At a minimum, this folder needs the following files:
- `mod.rs`
- `new.rs`
- `act.rs`
- `api.rs`
---
## 2. Register the module
Add your machine to `server/machines/mod.rs`:
```rust
// mod.rs
pub mod buffer1;
pub mod extruder1;
pub mod laser;
pub mod mock;
pub mod registry;
pub mod winder2;
pub mod YOUR_MACHINE;
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const YOUR_MACHINE_V1: u16 = 0xffff; // pick a unique ID to avoid collisions also DO NOT touch any already defined ids!!
```
## 3. mod.rs
This file defines your machine struct and the required event emitters.
Look at MockMachine’s mod.rs for the simplest possible version.
At a minimum, you need:
- emit_state
- emit_live_values
- A machine struct for state(e.g. MockMachine, Extruder, Laser, Winder, …)
More complex machines (e.g. Extruder, Winder) encapsulate controllers that in turn manage devices like EtherCAT terminals.
## 4 new.rs
This file implements MachineNewTrait for your machine.
Here you assemble your machine from its parts — devices, controllers, and any state.
Minimal example: see MockMachine → basically no devices, just returns a struct.
Medium complexity: see Laser → wraps a single serial USB device.
High complexity: see Extruder → configures multiple EtherCAT devices inside controllers.
Think of new.rs as the factory:
This is the place to declare which devices make up your machine and how they’re configured.
## 5. act.rs
This file implements the machine control loop using MachineAct.
The act() method is called repeatedly by the control runtime.
```rs
impl MachineAct for YourMachine {
fn act(&mut self, _now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let now = Instant::now();
// Update state, e.g. step a controller
self.some_controller.update();
// Emit live values ~30 Hz if machine is in Running mode
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
self.emit_live_values();
self.last_measurement_emit = now;
}
})
}
}
```
## 6. api.rs
Defines the frontend-facing API for your machine:
Mutations → requests that the machine accepts
(e.g. start/stop commands, setpoints, configuration updates).
Events → responses emitted to the frontend
(e.g. live sensor data, alarms, machine state changes).
Look at MockMachine’s api.rs for a minimal version.
```rs
/// Mutations are API requests to control the machine
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum YourMachineMutation {
Start,
Stop,
SetSpeed { rpm: f64 },
}
/// Events are responses sent to the frontend
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum YourMachineEvent {
StateChanged { state: String },
LiveValues { rpm: f64, torque: f64 },
}
```
+94
View File
@@ -0,0 +1,94 @@
# Code Style
# General Code styling
Try to adhere to the rust style guide:
https://doc.rust-lang.org/nightly/style-guide/
# Beckhoff terminals and Machines
For Beckhoff terminals you should first implement the basic functionality, then an Interpreter of the device/data is built on top.
A good example of how to do this is by looking at the following code:
el3021.rs , AnalogInputDevice
El3021 implements a so called AnalogInputDevice, which acts like an interface.
This allows us to use AnalogInputDevice instead of having to always specify which device should be used.
In Essence any Device that implements AnalogInputDevice can be used.
AnalogInputDevice is then used for creating an AnalogInput, which is plug and play for machines.
Then a further layer of Abstraction is used on top,
where the data is interpreted and incorporated into a sort of control loop for certain functions, for example:
```rust
// ScrewSpeedController controlls the speed (frequency) and direction of the motor
// AnalogInput is used to collect measurements from the pressure sensor.
// screw_speed_controller.rs
#[derive(Debug)]
pub struct ScrewSpeedController {
pub pid: ClampingTimeagnosticPidController,
pub target_pressure: Pressure,
pub target_rpm: AngularVelocity,
pub inverter: MitsubishiCS80,
pressure_sensor: AnalogInput,
last_update: Instant,
uses_rpm: bool,
forward_rotation: bool,
transmission_converter: TransmissionConverter,
frequency: Frequency,
maximum_frequency: Frequency,
minimum_frequency: Frequency,
motor_on: bool,
nozzle_pressure_limit: Pressure,
nozzle_pressure_limit_enabled: bool,
}
// Under the hood for ExtruderV2 a El3021 is used:
// (from new.rs of extruder)
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// But the extruder does not directly use the AnalogInput, instead the logic is decoupled in the screw_speed_controller:
let mut extruder: ExtruderV2 = Self {
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: Instant::now(),
mode: ExtruderV2Mode::Standby,
temperature_controller_front: temperature_controller_front,
temperature_controller_middle: temperature_controller_middle,
temperature_controller_back: temperature_controller_back,
temperature_controller_nozzle: temperature_controller_nozzle,
screw_speed_controller: screw_speed_controller,
emitted_default_state: false,
};
// Then in act.rs
// The extruder simply calls the update function of the screw_speed_controller,
// which contains "all" of the logic needed to properly drive the motor
self.screw_speed_controller.update(now_ts, true);
```
This is desired behaviour for all implemented devices, the machines should stay as modular as possible.
# Representing physical Units (Volt,Ampere,Hz,RPM etc)
To represent physical units like voltage ampere etc... we use uom (units of measurement), which acts as a wrapper for units and supplies all conversions between certain units.
For example:
```rust
// Lets say a device measures voltage, you just got the raw value
let volts : ElectricPotential = ElectricPotential::new::<volt>(raw_value_here);
// Then you can do this to get milli_volts
// In api responses we always use the RAW float value (64bit):
let milli_volts : f64 = volts.get::<millivolt>();
```
# General Advice
- Try to avoid duplicate code (DRY principle)
- Try to avoid lifetimes if at all possible
- Avoid excessive abstractions
- Avoid async code unless it is required
- Avoid taking Ownership of values, instead borrow the value
- When implementing a Trait for a struct, do it in the same file as the struct definition
- Split up large impl blocks into smaller impl blocks
+84
View File
@@ -0,0 +1,84 @@
# Environment Setup
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
## Ubuntu/Debian Installation
```bash
# Just press enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
apt install npm
apt install nodejs
# assuming you dont have git already, Git setup not included in here
apt install git
git clone git@github.com:qitechgmbh/control.git
cd control
cd electron
npm install
```
# Running Backend and Frontend Linux
## Backend
To Compile backend code and run it on Linux:
```bash
./cargo_run_linux.sh
```
The script sets capabilities on the compiled binary like raw socket access.
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
## Mock-Machine
The Mock-Machine can be used to test code that does not require an actual machine connection.
Like Frontend Code for example.
Mock machines can be used if the compile feature mock-machine is enabled.
```bash
# in root git folder
./cargo_run_linux.sh mock-machine
```
## Frontend
To run the Frontend Code:
```bash
# need to be in the electron folder and an additional terminal
cd electron
npm run start
```
# Contributing
Generally you contribute to the codebase by:
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
2. Work on it locally
3. Commit your changes locally
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
```bash
git fetch
git rebase origin/master
```
5. Push changes
```bash
# IF you needed to rebase
git push --force-with-lease
# ELSE you just push
git push
```
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
7. Request a Review and hope for the best :)
# Recommended Editor Setup
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
@@ -0,0 +1,128 @@
# Performance/Stability Tweaks
## EtherCrab Config
We set ethercrab to default settings for everything except for RetryBehaviour, so that one unlikely failure doesnt destroy the EtherCAT network.
```rust
let maindevice = MainDevice::new(
pdu,
Timeouts {
// Default 5000ms
state_transition: Duration::from_millis(5000),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
retry_behaviour: RetryBehaviour::Count(5),
// Default 10_000
dc_static_sync_iterations: 10_000,
},
);
```
wait_loop_delay CAN in theory also decrease CPU usage if set exactly as the loop_delay we set, however this setting has caused us some issues, so we just leave it as is.
## Realtime Kernel
In our nixOs Config we use the latest realtime kernel and isolate TWO cores out of the Four available for the machines we use.
```nix
#From configuration.nix
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelModules = [ "i915" ];
```
Notice how we hardcode the intel gpu kernel module. This is because only the GPU driver gets blacklisted for some reason. By specifying it manually it loads like before.
One Core is used for the EtherCrab TXRX Thread and one is used for the machine Thread.
```
"isolcpus=2,3" # Isolate cpus 2 and 3 from scheduler for better latency, 2 runs ethercatthread and 3 runs server control-loop
"nohz_full=2,3" # In this mode, the periodic scheduler tick is stopped when only one task is running, reducing kernel interruptions on those CPUs.
"rcu_nocbs=2,3" # Moves RCU (Read-Copy Update) callback processing away from CPUs 2 and 3.
```
Essentially we completely disable acces from the operating system to these two cpus, which saves scheduler overhead, which would introduce noticable latency.
Its not entirely clear yet if what we did here is overkill. Maybe we could use only one core and get away with that?
Processor load normally hovers around 18% for one of the cores and like 9% for the other.
We also have some minor cpu tweaks in configuration.nix
```
# Graphical
"logo.nologo" # Remove kernel logo during boot
# Performance
# Specific Vulnerabilities Addressed by Mitigations:
# - Spectre variants (V1, V2, V4, SWAPGS, SpectreRSB, etc.)
# - Meltdown (Rogue Data Cache Load)
# - Foreshadow/L1TF (L1 Terminal Fault)
# - Microarchitectural Data Sampling (MDS, RIDL, Fallout, ZombieLoad)
# - SRBDS (Special Register Buffer Data Sampling)
# - TSX Asynchronous Abort (TAA)
# - iTLB Multihit
# - And others as they're discovered and mitigated
#
# With mitigations=off
# - PROS: Maximum performance, equivalent to pre-2018 behavior
# - CONS: Vulnerable to Spectre, Meltdown, Foreshadow, ZombieLoad, etc.
# Should ONLY be used in completely trusted environments
# - Improves performance by 7-43%
"mitigation=off"
"intel_pstate=performance" # Intel CPU-specific performance mode (if applicable)
# Memory Management
"transparent_hugepage=always" # Use larger memory pages for memory intense applications
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
# High-throughput ethernet parameters
"pcie_aspm=off" # Disable PCIe power management for NICs
"intel_iommu=off" # Disable IOMMU (performance gain)
# Reliability
"panic=10" # Auto-reboot 10 seconds after kernel panic
"oops=panic" # Treat kernel oops as panic for auto-recovery
"usbcore.autosuspend=-1" # Possibly fixes dre disconnect issue?
```
We should check if its really required to turn mitigations off as well.
## Rust Code Optimizations
In General we try to avoid memory allocations in our loop as much as possible, as it is not deterministic
One problem we found, is that due to the realtime kernel our ethernet driver caused us quite large spikes in our tx_rx cycle time for EtherCAT.
These spikes happen when CPU Load is very high on the two usable cores, causing the interrupt handler for Ethernet to stall.
Which is why we reassign the interrupt handler on startup to the same core that the EtherCatTxRx Thread runs on:
```rust
#[cfg(target_os = "linux")]
match set_irq_affinity(&interface, 3) {
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
Err(e) => tracing::error!("set_irq_affinity failed: {:?}", e),
}
// Set core affinity to 4th core
let _ = set_core_affinity(3);
// Set the thread to real-time priority
let _ = set_realtime_priority();
```
This means that the Interrupt Handler ALWAYS has enough ressources to cope with our demand for realtime communication with EtherCAT
One Problem we have at the moment (we think) is the locking and async code blocking our server loop.
Note that this doesnt affect the EtherCAT communication at all.
## Electron Optimizations (Not Yet Implemented)
Currently we are struggling with the fact, that apparently Electron doesnt really have good support for HW acceleration on Linux.
Electron runs with the software-renderer, which is VERY slow compared to a gpu.
This would eliminate the weird stuttering seen on Graphs and make them run buttery smooth, while also alleviating load off of the CPU.
Loaded 100 of 578 files, more files were not shown because too many files have changed in this diff. Show more