Compare commits

...
240 Commits
Author SHA1 Message Date
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
Christian df0f86ce7e feat(extruder): Add total energy consumption tracking and related metrics 2025-08-28 16:55:26 +02:00
263 changed files with 74681 additions and 4739 deletions

No files matched your search

+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",
}
+98 -9
View File
@@ -1,57 +1,145 @@
# `2.6.0`
_03.09.2025_
# `2.10.0`
_16.10.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
## 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
## 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
- [#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
## 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
@@ -67,6 +155,7 @@ _15.07.2025_
- [#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
Generated
+32 -159
View File
@@ -99,9 +99,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "approx"
@@ -284,9 +284,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
dependencies = [
"axum-core",
"axum-macros",
@@ -304,8 +304,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
@@ -319,9 +318,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
dependencies = [
"bytes",
"futures-core",
@@ -330,7 +329,6 @@ dependencies = [
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
@@ -596,6 +594,7 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"tokio",
"tracing",
"uom",
]
@@ -954,11 +953,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
dependencies = [
"serde",
"serde_core",
"typeid",
]
@@ -1019,6 +1019,7 @@ dependencies = [
"futures",
"rand",
"smol",
"tracing",
"uom",
]
@@ -1677,15 +1678,6 @@ dependencies = [
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
@@ -1697,16 +1689,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@@ -1746,9 +1728,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "libloading"
@@ -2000,17 +1982,6 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "open"
version = "5.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "opentelemetry"
version = "0.30.0"
@@ -2120,12 +2091,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pcap"
version = "2.2.0"
@@ -2415,9 +2380,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.2"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
@@ -2427,9 +2392,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
@@ -2575,18 +2540,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -2698,14 +2673,12 @@ dependencies = [
"bitvec",
"control_core",
"control_core_derive",
"crc",
"dhat",
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
@@ -2716,9 +2689,6 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"thread-priority",
"tikv-jemalloc-ctl",
"tikv-jemallocator",
"tokio",
"tonic",
"tower-http",
@@ -3031,20 +3001,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread-priority"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"libc",
"log",
"rustversion",
"windows",
]
[[package]]
name = "thread_local"
version = "1.1.8"
@@ -3055,37 +3011,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tikv-jemalloc-ctl"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
dependencies = [
"libc",
"paste",
"tikv-jemalloc-sys",
]
[[package]]
name = "tikv-jemalloc-sys"
version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "tikv-jemallocator"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
dependencies = [
"libc",
"tikv-jemalloc-sys",
]
[[package]]
name = "timerfd"
version = "1.6.0"
@@ -3658,28 +3583,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -3693,17 +3596,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -3732,16 +3624,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
@@ -3843,15 +3725,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
+18 -1
View File
@@ -4,7 +4,24 @@ members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"ethercat-eeprom-dump", "control-core-derive",
"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 }
+9 -6
View File
@@ -7,6 +7,9 @@ QiTech Control is an industrial control panel software for the next generation o
[![](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
@@ -32,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.
@@ -64,12 +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 | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | 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
+5 -3
View File
@@ -21,10 +21,12 @@ fi
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
echo "Setting permissions for serial ports"
sudo chown "root:$USER" /dev/ttyUSB*
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
+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
#
+4 -1
View File
@@ -3,10 +3,13 @@ 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"] }
syn = { version = "2.0", features = ["full"] }
File diff suppressed because it is too large. Load diff
+34
View File
@@ -29,3 +29,37 @@ fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
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)
}
+21 -6
View File
@@ -3,30 +3,45 @@ name = "control_core"
version = "0.1.0"
edition = "2024"
[lints]
workspace = true
[dependencies]
anyhow = "1.0.99"
axum = "0.8.4"
anyhow = "1.0.100"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.2"
uom = "0.36.0"
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.6"
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.175"
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"]
@@ -33,7 +33,7 @@ pub struct ClampingTimeagnosticPidController {
}
impl ClampingTimeagnosticPidController {
pub fn new(
pub const fn new(
kp: f64,
ki: f64,
kd: f64,
@@ -65,7 +65,7 @@ impl ClampingTimeagnosticPidController {
}
}
pub fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
pub const fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
@@ -85,26 +85,26 @@ impl ClampingTimeagnosticPidController {
}
}
pub fn get_kp(&self) -> f64 {
pub const fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
pub const fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_kd(&self) -> f64 {
pub const fn get_kd(&self) -> f64 {
self.kd
}
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
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),
@@ -114,7 +114,7 @@ impl ClampingTimeagnosticPidController {
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let signal = match self.last {
match self.last {
// First update
None => {
// Calculate error
@@ -122,15 +122,10 @@ impl ClampingTimeagnosticPidController {
// Calculate signal
let signal = self.kp * ep;
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
signal,
self.min_signal,
self.max_signal,
);
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
// Set values
self.ep =
ClampingTimeagnosticPidController::optional_clamp(ep, self.min_ep, self.max_ep);
self.ep = Self::optional_clamp(ep, self.min_ep, self.max_ep);
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
@@ -143,23 +138,11 @@ impl ClampingTimeagnosticPidController {
let dt = t.duration_since(last).as_secs_f64();
// Calculate errors
let ep = ClampingTimeagnosticPidController::optional_clamp(
error,
self.min_ep,
self.max_ep,
);
let ep = Self::optional_clamp(error, self.min_ep, self.max_ep);
let ei = ClampingTimeagnosticPidController::optional_clamp(
self.ei + ep * dt,
self.min_ei,
self.max_ei,
);
let ei = Self::optional_clamp(ep.mul_add(dt, self.ei), self.min_ei, self.max_ei);
let ed = ClampingTimeagnosticPidController::optional_clamp(
(ep - self.ep) / dt,
self.min_ed,
self.max_ed,
);
let ed = Self::optional_clamp((ep - self.ep) / dt, self.min_ed, self.max_ed);
// Make factors timeagnostic
let kp = self.kp * dt;
@@ -167,12 +150,8 @@ impl ClampingTimeagnosticPidController {
let kd = self.kd * dt;
// Calculate signal
let signal = kp * ep + ki * ei + kd * ed;
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
signal,
self.min_signal,
self.max_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;
@@ -182,12 +161,10 @@ impl ClampingTimeagnosticPidController {
clamped_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;
@@ -22,7 +22,7 @@ pub struct AccelerationSpeedController {
}
impl AccelerationSpeedController {
pub fn new(
pub const fn new(
min_speed: Option<f64>,
max_speed: Option<f64>,
min_acceleration: f64,
@@ -98,16 +98,16 @@ impl AccelerationSpeedController {
new_speed
}
pub fn reset(&mut self, initial_speed: f64) {
pub const fn reset(&mut self, initial_speed: f64) {
self.last_speed = initial_speed;
self.last_t = None; // Reset the last update time
}
pub fn set_max_acceleration(&mut self, acceleration: f64) {
pub const fn set_max_acceleration(&mut self, acceleration: f64) {
self.max_acceleration = acceleration;
}
fn apply_speed_limits(&self, speed: f64) -> f64 {
const fn apply_speed_limits(&self, speed: f64) -> f64 {
let mut limited_speed = speed;
if let Some(min) = self.min_speed {
@@ -121,23 +121,23 @@ impl AccelerationSpeedController {
limited_speed
}
pub fn get_min_speed(&self) -> Option<f64> {
pub const fn get_min_speed(&self) -> Option<f64> {
self.min_speed
}
pub fn get_max_speed(&self) -> Option<f64> {
pub const fn get_max_speed(&self) -> Option<f64> {
self.max_speed
}
pub fn set_min_speed(&mut self, min_speed: Option<f64>) {
pub const fn set_min_speed(&mut self, min_speed: Option<f64>) {
self.min_speed = min_speed;
}
pub fn set_max_speed(&mut self, max_speed: Option<f64>) {
pub const fn set_max_speed(&mut self, max_speed: Option<f64>) {
self.max_speed = max_speed;
}
pub fn set_min_acceleration(&mut self, deceleration: f64) {
pub const fn set_min_acceleration(&mut self, deceleration: f64) {
self.min_acceleration = deceleration;
}
}
@@ -53,7 +53,7 @@ impl AngularAccelerationSpeedController {
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);
return AngularVelocity::new::<radian_per_second>(new_speed);
AngularVelocity::new::<radian_per_second>(new_speed)
}
pub fn reset(&mut self, initial_speed: AngularVelocity) {
@@ -73,13 +73,13 @@ impl AngularAccelerationSpeedController {
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_min_speed()
.map(|s| AngularVelocity::new::<radian_per_second>(s))
.map(AngularVelocity::new::<radian_per_second>)
}
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_max_speed()
.map(|s| AngularVelocity::new::<radian_per_second>(s))
.map(AngularVelocity::new::<radian_per_second>)
}
pub fn set_min_speed(&mut self, min_speed: Option<AngularVelocity>) {
@@ -48,7 +48,7 @@ impl LinearAccelerationLimitingController {
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);
return Velocity::new::<meter_per_second>(new_speed);
Velocity::new::<meter_per_second>(new_speed)
}
pub fn reset(&mut self, initial_speed: Velocity) {
let initial_speed = initial_speed.get::<meter_per_second>();
@@ -65,12 +65,12 @@ impl LinearAccelerationLimitingController {
pub fn get_min_speed(&self) -> Option<Velocity> {
self.controller
.get_min_speed()
.map(|s| Velocity::new::<meter_per_second>(s))
.map(Velocity::new::<meter_per_second>)
}
pub fn get_max_speed(&self) -> Option<Velocity> {
self.controller
.get_max_speed()
.map(|s| Velocity::new::<meter_per_second>(s))
.map(Velocity::new::<meter_per_second>)
}
pub fn set_min_speed(&mut self, min_speed: Option<Velocity>) {
self.controller
+10 -12
View File
@@ -21,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,
@@ -33,27 +33,27 @@ impl PidController {
}
}
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub fn get_kp(&self) -> f64 {
pub const fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
pub const fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_kd(&self) -> f64 {
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
@@ -78,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;
@@ -92,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;
@@ -3,7 +3,7 @@ use std::fmt;
// Remove the anyhow::Ok import as it conflicts with std::result::Ok
/// Errors that can occur when creating or using the motion controller
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotionControllerError {
InvalidSpeedLimits,
InvalidAccelerationLimits,
@@ -14,25 +14,25 @@ pub enum MotionControllerError {
impl fmt::Display for MotionControllerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MotionControllerError::InvalidSpeedLimits => {
Self::InvalidSpeedLimits => {
write!(
f,
"Invalid speed limits: min_speed must be ≤ 0 and max_speed must be ≥ 0"
)
}
MotionControllerError::InvalidAccelerationLimits => {
Self::InvalidAccelerationLimits => {
write!(
f,
"Invalid acceleration limits: min_acceleration must be < 0 and max_acceleration must be > 0"
)
}
MotionControllerError::InvalidPositionLimits => {
Self::InvalidPositionLimits => {
write!(
f,
"Invalid position limits: min_position must be ≤ max_position"
)
}
MotionControllerError::ZeroDeceleration => {
Self::ZeroDeceleration => {
write!(
f,
"Zero deceleration rate: cannot plan motion without deceleration capability"
@@ -148,7 +148,7 @@ struct MotionConstants {
}
/// Represents the current phase of motion
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionPhase {
Idle,
IncreasingSpeed,
@@ -170,7 +170,7 @@ pub struct ControllerBuilder {
impl ControllerBuilder {
/// Create a new builder
pub fn new() -> Self {
pub const fn new() -> Self {
Self {
min_speed: None,
max_speed: None,
@@ -184,28 +184,36 @@ impl ControllerBuilder {
}
/// Set speed limits (min_speed ≤ 0, max_speed ≥ 0)
pub fn speed_limits(mut self, min_speed: f64, max_speed: f64) -> Self {
pub const fn speed_limits(mut self, min_speed: f64, max_speed: f64) -> Self {
self.min_speed = Some(min_speed);
self.max_speed = Some(max_speed);
self
}
/// Set acceleration limits (min_acceleration < 0, max_acceleration > 0)
pub fn acceleration_limits(mut self, min_acceleration: f64, max_acceleration: f64) -> Self {
pub const fn acceleration_limits(
mut self,
min_acceleration: f64,
max_acceleration: f64,
) -> Self {
self.min_acceleration = Some(min_acceleration);
self.max_acceleration = Some(max_acceleration);
self
}
/// Set position limits (optional)
pub fn position_limits(mut self, min_position: Option<f64>, max_position: Option<f64>) -> Self {
pub const fn position_limits(
mut self,
min_position: Option<f64>,
max_position: Option<f64>,
) -> Self {
self.min_position = min_position;
self.max_position = max_position;
self
}
/// Set tolerances for position and speed comparisons
pub fn tolerances(mut self, position_tolerance: f64, speed_tolerance: f64) -> Self {
pub const fn tolerances(mut self, position_tolerance: f64, speed_tolerance: f64) -> Self {
self.position_tolerance = position_tolerance;
self.speed_tolerance = speed_tolerance;
self
@@ -322,7 +330,7 @@ impl AccelerationPositionController {
let motion_constants = Self::calculate_motion_constants(&config);
Ok(AccelerationPositionController {
Ok(Self {
config,
current_position: 0.0,
current_speed: 0.0,
@@ -366,19 +374,19 @@ impl AccelerationPositionController {
}
Self::new(
-speed, // min_speed
speed, // max_speed
-acceleration, // min_acceleration
acceleration, // max_acceleration
position.map(|p| -p), // min_position
position.map(|p| p), // max_position
1e-6, // position_tolerance (default)
1e-6, // speed_tolerance (default)
-speed, // min_speed
speed, // max_speed
-acceleration, // min_acceleration
acceleration, // max_acceleration
position.map(|p| -p.abs()), // min_position
position.map(|p| p.abs()), // max_position
1e-6, // position_tolerance (default)
1e-6, // speed_tolerance (default)
)
}
/// Create a new builder for the controller
pub fn builder() -> ControllerBuilder {
pub const fn builder() -> ControllerBuilder {
ControllerBuilder::new()
}
@@ -564,8 +572,10 @@ impl AccelerationPositionController {
// Calculate distance needed to reach cruise speed from current speed
let speed_change_distance = if current_speed_in_direction < cruise_speed.abs() {
(cruise_speed.abs().powi(2) - current_speed_in_direction.max(0.0).powi(2))
/ (2.0 * acceleration_rate)
current_speed_in_direction.max(0.0).mul_add(
-current_speed_in_direction.max(0.0),
cruise_speed.abs().powi(2),
) / (2.0 * acceleration_rate)
} else {
0.0
};
@@ -579,8 +589,10 @@ impl AccelerationPositionController {
// Using corrected kinematic equation: v_peak² = v_current² + 2*a*d_total*a_accel*a_decel/(a_accel + a_decel)
let accel_decel_factor =
(acceleration_rate * deceleration_rate) / (acceleration_rate + deceleration_rate);
let discriminant = current_speed_in_direction.max(0.0).powi(2)
+ 2.0 * abs_position_change * accel_decel_factor;
let discriminant = current_speed_in_direction.max(0.0).mul_add(
current_speed_in_direction.max(0.0),
2.0 * abs_position_change * accel_decel_factor,
);
if discriminant >= 0.0 {
self.peak_speed = discriminant.sqrt() * cruise_speed.signum();
@@ -634,7 +646,7 @@ impl AccelerationPositionController {
self.current_acceleration = self.config.max_acceleration * self.direction as f64;
// Update speed
let new_speed = self.current_speed + self.current_acceleration * dt;
let new_speed = self.current_acceleration.mul_add(dt, self.current_speed);
// Check if we've reached peak speed
if (self.direction > 0 && new_speed >= self.peak_speed)
@@ -665,7 +677,7 @@ impl AccelerationPositionController {
self.current_acceleration = self.config.min_acceleration * self.direction as f64;
// Update speed
let new_speed = self.current_speed + self.current_acceleration * dt;
let new_speed = self.current_acceleration.mul_add(dt, self.current_speed);
// Check if we've reached zero speed or target
if (self.current_speed > 0.0 && new_speed <= 0.0)
@@ -689,7 +701,7 @@ impl AccelerationPositionController {
}
// Update position based on current speed
let new_position = self.current_position + self.current_speed * dt;
let new_position = self.current_speed.mul_add(dt, self.current_position);
// Check position limits and handle violations
let mut position_limited = false;
@@ -742,7 +754,7 @@ impl AccelerationPositionController {
///
/// Returns the current position of the controlled object.
/// This value is updated with each call to `update()`.
pub fn get_position(&self) -> f64 {
pub const fn get_position(&self) -> f64 {
self.current_position
}
@@ -751,7 +763,7 @@ impl AccelerationPositionController {
/// Returns the current speed (velocity) of the controlled object.
/// Positive values indicate motion towards increasing position,
/// negative values indicate motion towards decreasing position.
pub fn get_speed(&self) -> f64 {
pub const fn get_speed(&self) -> f64 {
self.current_speed
}
@@ -759,32 +771,32 @@ impl AccelerationPositionController {
///
/// Returns the current acceleration of the controlled object.
/// Positive values indicate increasing speed, negative values indicate decreasing speed.
pub fn get_acceleration(&self) -> f64 {
pub const fn get_acceleration(&self) -> f64 {
self.current_acceleration
}
/// Get the target position
pub fn get_target_position(&self) -> f64 {
pub const fn get_target_position(&self) -> f64 {
self.target_position
}
/// Get the current motion phase
pub fn get_motion_phase(&self) -> MotionPhase {
pub const fn get_motion_phase(&self) -> MotionPhase {
self.motion_phase
}
/// Get the direction of motion
pub fn get_direction(&self) -> i8 {
pub const fn get_direction(&self) -> i8 {
self.direction
}
/// Get the calculated peak speed for current motion
pub fn get_peak_speed(&self) -> f64 {
pub const fn get_peak_speed(&self) -> f64 {
self.peak_speed
}
/// Get the position at which deceleration will begin
pub fn get_deceleration_position(&self) -> f64 {
pub const fn get_deceleration_position(&self) -> f64 {
self.deceleration_position
}
@@ -837,7 +849,7 @@ impl AccelerationPositionController {
}
/// Force the controller to stop at current position
pub fn emergency_stop(&mut self) {
pub const fn emergency_stop(&mut self) {
self.target_position = self.current_position;
self.current_speed = 0.0;
self.current_acceleration = 0.0;
@@ -1006,32 +1018,32 @@ impl AccelerationPositionController {
}
/// Get the minimum speed limit
pub fn get_min_speed(&self) -> f64 {
pub const fn get_min_speed(&self) -> f64 {
self.config.min_speed
}
/// Get the maximum speed limit
pub fn get_max_speed(&self) -> f64 {
pub const fn get_max_speed(&self) -> f64 {
self.config.max_speed
}
/// Get the minimum acceleration limit
pub fn get_min_acceleration(&self) -> f64 {
pub const fn get_min_acceleration(&self) -> f64 {
self.config.min_acceleration
}
/// Get the maximum acceleration limit
pub fn get_max_acceleration(&self) -> f64 {
pub const fn get_max_acceleration(&self) -> f64 {
self.config.max_acceleration
}
/// Get the minimum position limit
pub fn get_min_position(&self) -> Option<f64> {
pub const fn get_min_position(&self) -> Option<f64> {
self.config.min_position
}
/// Get the maximum position limit
pub fn get_max_position(&self) -> Option<f64> {
pub const fn get_max_position(&self) -> Option<f64> {
self.config.max_position
}
@@ -1068,17 +1080,17 @@ impl AccelerationPositionController {
}
/// Get the position tolerance
pub fn get_position_tolerance(&self) -> f64 {
pub const fn get_position_tolerance(&self) -> f64 {
self.config.position_tolerance
}
/// Get the speed tolerance
pub fn get_speed_tolerance(&self) -> f64 {
pub const fn get_speed_tolerance(&self) -> f64 {
self.config.speed_tolerance
}
/// Set tolerances for position and speed comparisons
pub fn set_tolerances(&mut self, position_tolerance: f64, speed_tolerance: f64) {
pub const fn set_tolerances(&mut self, position_tolerance: f64, speed_tolerance: f64) {
self.config.position_tolerance = position_tolerance;
self.config.speed_tolerance = speed_tolerance;
}
@@ -102,16 +102,12 @@ impl AngularAccelerationPositionController {
/// Get the minimum angle position limit
pub fn get_min_position(&self) -> Option<Angle> {
self.controller
.get_min_position()
.map(|angle| Angle::new::<radian>(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| Angle::new::<radian>(angle))
self.controller.get_max_position().map(Angle::new::<radian>)
}
/// Set the minimum angle position limit
@@ -236,7 +236,7 @@ impl AngularJerkSpeedController {
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_min_speed()
.map(|speed| AngularVelocity::new::<radian_per_second>(speed))
.map(AngularVelocity::new::<radian_per_second>)
}
/// Get the maximum angular velocity limit
@@ -248,7 +248,7 @@ impl AngularJerkSpeedController {
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
self.controller
.get_max_speed()
.map(|speed| AngularVelocity::new::<radian_per_second>(speed))
.map(AngularVelocity::new::<radian_per_second>)
}
/// Set the minimum angular velocity limit
@@ -88,7 +88,7 @@ impl JerkSpeedController {
)
.expect("Failed to create AccelerationPositionController");
JerkSpeedController { base_controller }
Self { base_controller }
}
/// Create a new speed controller with simple symmetric limits
@@ -183,7 +183,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Current speed as a floating-point value
pub fn get_speed(&self) -> f64 {
pub const fn get_speed(&self) -> f64 {
self.base_controller.get_position()
}
@@ -193,7 +193,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Current acceleration as a floating-point value
pub fn get_acceleration(&self) -> f64 {
pub const fn get_acceleration(&self) -> f64 {
self.base_controller.get_speed()
}
@@ -203,7 +203,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Current jerk as a floating-point value
pub fn get_jerk(&self) -> f64 {
pub const fn get_jerk(&self) -> f64 {
self.base_controller.get_acceleration()
}
@@ -213,7 +213,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Target speed as a floating-point value
pub fn get_target_speed(&self) -> f64 {
pub const fn get_target_speed(&self) -> f64 {
self.base_controller.get_target_position()
}
@@ -223,7 +223,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Minimum speed limit as an Option<f64>, None if no limit is set
pub fn get_min_speed(&self) -> Option<f64> {
pub const fn get_min_speed(&self) -> Option<f64> {
self.base_controller.get_min_position()
}
@@ -233,7 +233,7 @@ impl JerkSpeedController {
///
/// # Returns
/// Maximum speed limit as an Option<f64>, None if no limit is set
pub fn get_max_speed(&self) -> Option<f64> {
pub const fn get_max_speed(&self) -> Option<f64> {
self.base_controller.get_max_position()
}
@@ -205,9 +205,7 @@ impl LinearAccelerationPositionController {
/// # 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| Length::new::<meter>(length))
self.controller.get_min_position().map(Length::new::<meter>)
}
/// Get the maximum position limit
@@ -217,9 +215,7 @@ impl LinearAccelerationPositionController {
/// # 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| Length::new::<meter>(length))
self.controller.get_max_position().map(Length::new::<meter>)
}
/// Set the minimum position limit
@@ -195,14 +195,14 @@ impl LinearJerkSpeedController {
pub fn get_min_speed(&self) -> Option<Velocity> {
self.controller
.get_min_speed()
.map(|speed| Velocity::new::<meter_per_second>(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(|speed| Velocity::new::<meter_per_second>(speed))
.map(Velocity::new::<meter_per_second>)
}
/// Set the minimum velocity limit
+40 -58
View File
@@ -1,8 +1,6 @@
use std::f32::consts::PI;
use uom::si::{angle::radian, f64::Angle};
use uom::si::angle::radian;
#[derive(Debug, Clone, Copy, PartialEq)]
#[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)
@@ -11,7 +9,7 @@ pub struct AngleConverter {
impl AngleConverter {
/// Create a custom coordinate system
pub fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
pub const fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
Self {
flip_x,
flip_y,
@@ -20,52 +18,52 @@ impl AngleConverter {
}
/// Standard mathematical coordinate system (CCW positive, 0° at positive X)
pub fn mathematical() -> Self {
pub const fn mathematical() -> Self {
Self::new(false, false, false)
}
/// Screen/graphics coordinate system (CW positive, 0° at positive X, Y-flipped)
pub fn screen() -> Self {
pub const fn screen() -> Self {
Self::new(false, true, false)
}
/// System where 0° points up, clockwise positive
pub fn y_up_cw() -> Self {
pub const fn y_up_cw() -> Self {
Self::new(false, false, true)
}
/// System where 0° points down, counter-clockwise positive
pub fn y_down_ccw() -> Self {
pub const fn y_down_ccw() -> Self {
Self::new(false, true, true)
}
/// System where 0° points left, clockwise positive
pub fn x_left_cw() -> Self {
pub const fn x_left_cw() -> Self {
Self::new(true, false, false)
}
/// System where 0° points right, counter-clockwise positive (same as mathematical)
pub fn x_right_ccw() -> Self {
pub const fn x_right_ccw() -> Self {
Self::mathematical()
}
/// System where 0° points up, counter-clockwise positive
pub fn y_up_ccw() -> Self {
pub const fn y_up_ccw() -> Self {
Self::new(false, false, false)
}
/// System where 0° points down, clockwise positive
pub fn y_down_cw() -> Self {
pub const fn y_down_cw() -> Self {
Self::new(false, true, false)
}
/// System where 0° points left, counter-clockwise positive
pub fn x_left_ccw() -> Self {
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: f32) -> 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;
@@ -75,7 +73,7 @@ impl AngleConverter {
(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 + result, // Both flips: 180° rotation
(true, true) => result += 180.0, // Both flips: 180° rotation
}
// Handle rotation direction
@@ -87,7 +85,7 @@ impl AngleConverter {
}
/// Convert angle from this system to mathematical system (f32)
pub fn degrees_decode(&self, system_angle_degrees: f32) -> 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;
@@ -102,24 +100,24 @@ impl AngleConverter {
(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 = result - 180.0, // Both flips: inverse 180° rotation
(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: f32) -> f32 {
let degrees = math_angle_radians * 180.0 / PI;
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 * PI / 180.0
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f32)
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
let degrees = system_angle_radians * 180.0 / PI;
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 * PI / 180.0
converted_degrees.to_radians()
}
/// Convert angle from mathematical system to this system (f64)
@@ -133,7 +131,7 @@ impl AngleConverter {
(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 + result, // Both flips: 180° rotation
(true, true) => result += 180.0, // Both flips: 180° rotation
}
// Handle rotation direction
@@ -160,7 +158,7 @@ impl AngleConverter {
(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 = result - 180.0, // Both flips: inverse 180° rotation
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
}
self.normalize_angle_f64(result)
@@ -168,20 +166,20 @@ impl AngleConverter {
/// 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 * 180.0 / std::f64::consts::PI;
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode_f64(degrees);
converted_degrees * std::f64::consts::PI / 180.0
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 * 180.0 / std::f64::consts::PI;
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode_f64(degrees);
converted_degrees * std::f64::consts::PI / 180.0
converted_degrees.to_radians()
}
/// Normalize angle to [0, 360) range (f32)
fn normalize_angle(&self, angle: f32) -> f32 {
fn normalize_angle(&self, angle: f64) -> f64 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
@@ -205,39 +203,23 @@ pub struct AngleConverterUom {
}
impl AngleConverterUom {
pub fn new(angle_converter: AngleConverter) -> Self {
pub const fn new(angle_converter: AngleConverter) -> Self {
Self { angle_converter }
}
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_encode(angle.get::<radian>()),
)
pub fn encode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
}
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_decode(angle.get::<radian>()),
)
}
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_encode_f64(angle.get::<radian>()),
)
}
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_decode_f64(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]
@@ -297,8 +279,8 @@ mod tests {
let converter = AngleConverter::screen();
// Test radians_encode
let result = converter.radians_encode(PI / 2.0); // 90° in radians
let expected = 270.0 * PI / 180.0; // 270° in radians
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);
}
@@ -306,10 +288,10 @@ mod tests {
fn test_roundtrip_conversion() {
let converter = AngleConverter::new(true, false, true); // X-flip + CW
let original = 75.0_f32;
let original = 75.0_f64;
let converted = converter.degrees_encode(original);
let roundtrip = converter.degrees_decode(converted);
assert!((original - roundtrip).abs() < 1e-6);
assert!((original - roundtrip as f64).abs() < 1e-6);
}
}
@@ -18,7 +18,7 @@ pub struct AngularStepConverter {
impl AngularStepConverter {
/// Create a new converter with the specified steps per revolution
pub fn new(steps_per_revolution: i16) -> Self {
pub const fn new(steps_per_revolution: i16) -> Self {
Self {
steps_per_revolution,
}
@@ -60,7 +60,7 @@ impl LinearStepConverter {
}
/// Get the steps per revolution
pub fn steps_per_revolution(&self) -> i16 {
pub const fn steps_per_revolution(&self) -> i16 {
self.angular_step_converter.steps_per_revolution
}
}
+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!()))
}
}
}
@@ -18,6 +18,8 @@ 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::el3204::EL3204_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,
};
@@ -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,
@@ -74,7 +81,7 @@ pub async fn read_device_identifications<'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
@@ -221,11 +228,11 @@ 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(),
@@ -233,22 +240,25 @@ pub fn get_identification_addresses<'maindevice>(
EL1008_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 | EL2008_IDENTITY_B => MachineIdentificationAddresses::default(),
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
MachineIdentificationAddresses::default()
}
EL2522_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(),
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7041_0052_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(),
_ => {
// block_on(u16dump(&subdevice, maindevice, 0x00, 0xff))?;
Err(anyhow!(
@@ -274,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
@@ -290,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);
@@ -307,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,14 +12,46 @@ 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> {
tracing::info!("Discovering EtherCAT interface...");
@@ -37,17 +69,17 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
} else {
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
@@ -56,60 +88,42 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
})
.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::Builder::new()
.name(format!("ethercat-test-{}", name))
.spawn(move || {
std::panic::catch_unwind(|| {
smol::block_on(async {
match test_interface(&name) {
Ok(_) => {
tracing::info!("Found working EtherCAT interface: {}", name);
Some(name.clone())
}
Err(_) => {
tracing::debug!("Interface {} failed", name);
None
}
}
})
})
.unwrap_or(None)
})
.expect("Failed to spawn thread")
})
.collect::<Vec<_>>();
for i in 0..interfaces.len() {
// Avoid NetworkManager hangups
set_interface_managed(&interfaces[i].name, false);
// Wait for all tasks to complete and collect successful results
let successful_interfaces: Vec<String> = tasks
.into_iter()
.filter_map(|task| match task.join().expect("Should join thread") {
Some(name) => Some(name),
None => None,
})
.collect();
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(_) => (),
}
}
// Return the first successful interface (they're sorted by name)
let result = successful_interfaces.into_iter().next();
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> {
tracing::trace!("Testing interface: {}", interface);
tracing::info!("Testing interface: {}", interface);
let pdu_storage = Box::leak(Box::new(PduStorage::<
IFACE_DISCOVERY_MAX_FRAMES,
@@ -147,7 +161,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
// Default 10000
dc_static_sync_iterations: 10000,
// Default None
retry_behaviour: ethercrab::RetryBehaviour::None,
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
},
));
+11 -34
View File
@@ -25,12 +25,12 @@ impl serde::ser::Error for HashSerializerError {
where
T: Display,
{
HashSerializerError {}
Self {}
}
}
pub struct HashSerializer<'a, H: Hasher>(&'a mut H);
impl<'a, H: Hasher> SerializeSeq for HashSerializer<'a, H> {
impl<H: Hasher> SerializeSeq for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -46,7 +46,7 @@ impl<'a, H: Hasher> SerializeSeq for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeTuple for HashSerializer<'a, H> {
impl<H: Hasher> SerializeTuple for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -62,7 +62,7 @@ impl<'a, H: Hasher> SerializeTuple for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeTupleStruct for HashSerializer<'a, H> {
impl<H: Hasher> SerializeTupleStruct for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -78,7 +78,7 @@ impl<'a, H: Hasher> SerializeTupleStruct for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeTupleVariant for HashSerializer<'a, H> {
impl<H: Hasher> SerializeTupleVariant for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -94,7 +94,7 @@ impl<'a, H: Hasher> SerializeTupleVariant for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeMap for HashSerializer<'a, H> {
impl<H: Hasher> SerializeMap for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -117,7 +117,7 @@ impl<'a, H: Hasher> SerializeMap for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeStruct for HashSerializer<'a, H> {
impl<H: Hasher> SerializeStruct for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -135,7 +135,7 @@ impl<'a, H: Hasher> SerializeStruct for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> SerializeStructVariant for HashSerializer<'a, H> {
impl<H: Hasher> SerializeStructVariant for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
@@ -152,7 +152,7 @@ impl<'a, H: Hasher> SerializeStructVariant for HashSerializer<'a, H> {
}
}
impl<'a, H: Hasher> Serializer for HashSerializer<'a, H> {
impl<H: Hasher> Serializer for HashSerializer<'_, H> {
type Ok = ();
type Error = HashSerializerError;
type SerializeSeq = Self;
@@ -329,32 +329,9 @@ impl<'a, H: Hasher> Serializer for HashSerializer<'a, H> {
}
// A helper that uses Serialize to feed into a Hasher without allocating a buffer:
pub fn hash_with_serde_model<T: Serialize, H: Hasher>(value: T, hasher: H) -> u64 {
let mut h = hasher;
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()
}
/// if hash of a and b are different this function returns true
/// a and b need to impl Serialize
pub fn check_hash_different<T: Serialize>(a: T, b: T) -> bool {
// Yes DefaultHasher has to be defined multiple times
// If you use one for both the results will never match even if they are the same value
let hasher = DefaultHasher::new();
let hash = hash_with_serde_model(a, hasher);
let hasher = DefaultHasher::new();
let hash_old = hash_with_serde_model(b, hasher);
return hash != hash_old;
}
#[test]
fn test_same_values() {
let a = 10;
let b = 10;
assert_eq!(
check_hash_different(a, b),
false,
"Hashes should be equal for identical values"
);
}
+7 -7
View File
@@ -63,11 +63,11 @@ 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.into();
let normalized_value: f64 = normalized_value;
let new_min: f64 = new_min.into();
let new_max: f64 = new_max.into();
let result = normalized_value * (new_max - new_min) + new_min;
let result = normalized_value.mul_add(new_max - new_min, new_min);
T::from(result)
}
@@ -128,11 +128,11 @@ where
// 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 {
return normalize(value_f64, 0.0, x_f64) * y_f64;
normalize(value_f64, 0.0, x_f64) * y_f64
} else if value_f64 > x_f64 {
return normalize(value_f64, x_f64, 1.0) * (1.0 - y_f64) + y_f64;
normalize(value_f64, x_f64, 1.0).mul_add(1.0 - y_f64, y_f64)
} else {
return y_f64;
y_f64
}
}
@@ -273,7 +273,7 @@ pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
return normalized_value;
}
let numerator = steepness.exp().powf(normalized_value) - 1.0;
let denominator = steepness.exp() - 1.0;
let denominator = steepness.exp_m1();
clip(numerator / denominator)
}
@@ -414,7 +414,7 @@ pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
// Use standard exponential interpolation
let result = interpolate_exponential(x_norm, steepness);
0.5 + 0.5 * result
0.5f64.mul_add(result, 0.5)
}
}
-189
View File
@@ -1,189 +0,0 @@
use smol::Timer;
use std::time::{Duration, Instant};
use tracing::instrument;
pub struct LoopThrottle {
target_cycle_time: Duration,
update_interval_cycles: usize,
window_start: Instant,
total_cycles: usize,
delay: Duration,
last_avg_cycle_time: Duration,
on_update: Option<Box<dyn Fn(LoopThrottleUpdate)>>,
}
pub struct LoopThrottleUpdate {
pub delay: Duration,
pub avg_cycle_time: Duration,
pub error: i128, // Error in nanoseconds
}
impl LoopThrottle {
pub fn new(
target_cycle_time: Duration,
update_interval_cycles: usize,
on_update: Option<Box<dyn Fn(LoopThrottleUpdate)>>,
) -> Self {
Self {
target_cycle_time,
update_interval_cycles,
window_start: Instant::now(),
total_cycles: 0,
delay: target_cycle_time,
last_avg_cycle_time: target_cycle_time,
on_update,
}
}
#[instrument(skip_all)]
pub async fn sleep(&mut self) {
self.total_cycles += 1;
// Check if it's time to update (every N cycles)
if self.total_cycles % self.update_interval_cycles == 0 {
let actual_total_time = self.window_start.elapsed();
// Use integer arithmetic where possible
let avg_actual_cycle_time_nanos =
actual_total_time.as_nanos() / self.update_interval_cycles as u128;
let target_cycle_time_nanos = self.target_cycle_time.as_nanos();
// Calculate error in nanoseconds (signed)
let error_nanos = target_cycle_time_nanos as i128 - avg_actual_cycle_time_nanos as i128;
// Update current delay, allowing zero or negative values to become zero
let new_delay_nanos = (self.delay.as_nanos() as i128 + error_nanos).max(0) as u64;
self.delay = Duration::from_nanos(new_delay_nanos);
self.last_avg_cycle_time = Duration::from_nanos(avg_actual_cycle_time_nanos as u64);
// Reset for next measurement window
self.window_start = Instant::now();
self.total_cycles = 0; // Reset to prevent overflow
// if we have an update callback, call it
if let Some(on_update) = &self.on_update {
on_update(LoopThrottleUpdate {
delay: self.delay,
avg_cycle_time: self.last_avg_cycle_time,
error: error_nanos,
});
}
}
Timer::after(self.delay).await;
}
pub fn get_cycle_time(&self) -> Duration {
self.last_avg_cycle_time
}
pub fn get_delay(&self) -> Duration {
self.delay
}
}
#[cfg(test)]
mod tests {
use super::*;
use smol::block_on;
#[test]
fn test_adaptive_throttle_timing() {
block_on(async {
let update_interval_cycles = 100;
let target_cycle_time = Duration::from_micros(5);
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
let test_start = Instant::now();
let mut cycle_times = Vec::new();
// Run for several cycles to trigger multiple updates
for _ in 0..10000 {
let cycle_start = Instant::now();
// Simulate some work
Timer::after(Duration::from_micros(5)).await;
throttle.sleep().await;
let cycle_time = cycle_start.elapsed();
cycle_times.push(cycle_time);
}
let total_test_time = test_start.elapsed();
let avg_cycle_time = cycle_times.iter().sum::<Duration>() / cycle_times.len() as u32;
println!("Test completed in: {:?}", total_test_time);
println!("Average cycle time: {:?}", avg_cycle_time);
println!("Target cycle time: {:?}", target_cycle_time);
println!("Last measured avg: {:?}", throttle.get_cycle_time());
// Assert that we're reasonably close to target timing
let tolerance = Duration::from_millis(10);
assert!(
avg_cycle_time >= target_cycle_time.saturating_sub(tolerance)
&& avg_cycle_time <= target_cycle_time + tolerance,
"Average cycle time {:?} not within tolerance of target {:?}",
avg_cycle_time,
target_cycle_time
);
});
}
#[test]
fn test_throttle_adaptation() {
block_on(async {
let target_cycle_time = Duration::from_millis(100);
let update_interval_cycles = 3;
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
let initial_delay = throttle.get_delay();
// Run enough cycles to trigger multiple updates
for _ in 0..12 {
// Add variable work to test adaptation
Timer::after(Duration::from_millis(10)).await;
throttle.sleep().await;
}
println!("Initial delay: {:?}", initial_delay);
println!("Final delay: {:?}", throttle.get_delay());
println!("Last avg cycle time: {:?}", throttle.get_cycle_time());
assert!(throttle.get_cycle_time() > Duration::ZERO);
});
}
#[test]
fn test_throttle_initialization() {
let update_interval_cycles = 10;
let target_cycle_time = Duration::from_millis(75);
let throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
assert_eq!(throttle.get_delay(), target_cycle_time);
assert_eq!(throttle.get_cycle_time(), target_cycle_time);
assert_eq!(throttle.total_cycles, 0);
}
#[test]
fn test_overflow_protection() {
block_on(async {
let target_cycle_time = Duration::from_micros(1);
let update_interval_cycles = 5;
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
// Run many cycles to ensure total_cycles resets properly
for i in 0..50 {
throttle.sleep().await;
// Verify that total_cycles never exceeds update_interval_cycles
assert!(
throttle.total_cycles <= update_interval_cycles,
"Cycle {}: total_cycles ({}) exceeded update_interval_cycles ({})",
i,
throttle.total_cycles,
update_interval_cycles
);
}
});
}
}
-1
View File
@@ -2,6 +2,5 @@ pub mod compare_lists;
pub mod hasher_serializer;
pub mod hashing;
pub mod interpolation;
pub mod loop_trottle;
pub mod moving_time_window;
pub mod retry;
+12 -30
View File
@@ -11,56 +11,40 @@ pub trait DivideByCount {
impl DivideByCount for f64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as f64)
self / (count as Self)
}
}
impl DivideByCount for f32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as f32)
self / (count as Self)
}
}
impl DivideByCount for i32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as i32)
self / (count as Self)
}
}
impl DivideByCount for i64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as i64)
self / (count as Self)
}
}
impl DivideByCount for u32 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as u32)
self / (count as Self)
}
}
impl DivideByCount for u64 {
fn divide_by_count(self, count: usize) -> Self {
self / (count as u64)
self / (count as Self)
}
}
/// A generic moving time window that works with numeric types.
///
/// # Examples
///
/// ```rust
/// use std::time::{Duration, Instant};
/// use control_core::helpers::moving_time_window::MovingTimeWindow;
///
/// let mut window = MovingTimeWindow::new(Duration::from_secs(10), 100);
/// window.update(1.5, Instant::now());
/// window.update(2.5, Instant::now());
///
/// let average = window.average();
/// println!("Average: {}", average);
/// ```
// Type aliases for common use cases
pub type F64Window = MovingTimeWindow<f64>;
pub type F32Window = MovingTimeWindow<f32>;
@@ -194,15 +178,14 @@ where
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| match acc {
None => Some(val),
Some(current_max) => {
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_max| {
if val > current_max {
Some(val)
} else {
Some(current_max)
}
}
})
})
.unwrap_or_default();
@@ -229,15 +212,14 @@ where
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| match acc {
None => Some(val),
Some(current_min) => {
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_min| {
if val < current_min {
Some(val)
} else {
Some(current_min)
}
}
})
})
.unwrap_or_default();
+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(());
}
}
+5
View File
@@ -1,7 +1,9 @@
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;
@@ -11,5 +13,8 @@ pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
+20 -1
View File
@@ -1,8 +1,27 @@
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 Namespace;
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,
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)]
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,
}
}
}
+11 -11
View File
@@ -12,7 +12,7 @@ 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
}
}
@@ -36,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,
@@ -49,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
@@ -59,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,
@@ -67,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,
@@ -90,24 +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),
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)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
+95 -67
View File
@@ -1,3 +1,9 @@
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
@@ -6,6 +12,9 @@ use socketioxide::extract::SocketRef;
use crate::{
machines::{
connection::{
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
},
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
},
@@ -17,18 +26,16 @@ use std::{
sync::{Arc, Weak},
};
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
pub serial_machines:
HashMap<MachineIdentificationUnique, Result<Arc<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 {
@@ -45,11 +52,8 @@ impl MachineManager {
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<MachineManager>>,
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);
@@ -63,10 +67,18 @@ 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 {
@@ -74,23 +86,40 @@ impl MachineManager {
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<Arc<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(
@@ -99,7 +128,7 @@ impl MachineManager {
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<MachineManager>>,
machine_manager: Weak<RwLock<Self>>,
) {
let hardware = MachineNewHardwareSerial { device };
@@ -109,21 +138,28 @@ impl MachineManager {
.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(),
});
tracing::info!("Adding serial machine {:?}", new_machine);
slot.machine_connection = match new_machine {
Err(err) => MachineConnectionGeneric::Error(err),
Ok(machine) => MachineConnectionGeneric::Connected(machine),
};
self.serial_machines.insert(
device_identification_identified
.device_machine_identification
.machine_identification_unique,
new_machine,
);
tracing::info!("Adding serial machine {:?}", slot);
}
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
@@ -138,53 +174,45 @@ impl MachineManager {
device_identification_identified
);
self.serial_machines.remove(
&device_identification_identified
.device_machine_identification
.machine_identification_unique,
);
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_machine_weak(
pub fn get_ethercat_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.ethercat_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
return Some(machine_weak);
self.get_weak(&self.ethercat_machines, machine_identification)
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.serial_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
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)),
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
return Some(machine_weak);
});
}
}
+10 -8
View File
@@ -1,22 +1,25 @@
use std::{collections::HashMap, sync::Arc};
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<Arc<Mutex<dyn Machine>>, anyhow::Error>,
Arc<Mutex<MachineSlotGeneric>>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<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<Arc<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<Arc<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(),
+10 -67
View File
@@ -1,80 +1,23 @@
use anyhow::anyhow;
use api::MachineApi;
use new::MachineNewTrait;
use serde::Serialize;
use smol::lock::Mutex;
use std::any::Any;
use std::fmt::Debug;
use std::sync::{Arc, Weak};
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::new::MachineAct;
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: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
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;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let read_lock = machine.lock().await;
// Check if the inner type can be downcasted to T
if read_lock.as_any().is::<T>() {
// Clone the Arc and return it as the desired type
let cloned_machine = Arc::clone(&machine);
// Transmute the Arc to the desired type
unsafe {
Ok(Arc::from_raw(
Arc::into_raw(cloned_machine) as *const Mutex<T>
))
}
} else {
Err(anyhow!(
"[{}::downcast_machine] Downcast failed",
module_path!()
))
}
}
/// Trait to get strong count to see if Weak has reference
pub trait GetStrongCount {
fn get_strong_count(&self) -> usize;
}
impl<T> GetStrongCount for Weak<Mutex<T>> {
fn get_strong_count(&self) -> usize {
self.strong_count()
}
}
#[derive(Debug)]
pub struct ConnectedMachine<T: GetStrongCount> {
pub machine_identification_unique: MachineIdentificationUnique,
pub machine: T,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct ConnectedMachineData {
pub machine_identification_unique: MachineIdentificationUnique,
pub is_available: bool,
}
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
where
T: GetStrongCount,
{
fn from(value: &ConnectedMachine<T>) -> Self {
Self {
machine_identification_unique: value.machine_identification_unique.clone(),
is_available: value.machine.get_strong_count() != 0,
}
}
fn as_any_mut(&mut self) -> &mut dyn Any;
}
+24 -7
View File
@@ -1,4 +1,8 @@
use crate::{machines::manager::MachineManager, serial::SerialDevice};
use crate::{
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
serial::SerialDevice,
socketio::namespace::Namespace,
};
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
@@ -6,10 +10,12 @@ use ethercat_hal::{
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use std::{
pin::Pin,
sync::{Arc, Weak},
time::Instant,
};
@@ -17,15 +23,13 @@ use std::{
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) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
fn act(&mut self, now: Instant);
}
pub struct MachineNewParams<
@@ -52,6 +56,19 @@ pub struct MachineNewParams<
>,
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<
+7 -1
View File
@@ -10,6 +10,12 @@ 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 {
@@ -24,7 +30,7 @@ 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(Arc::new(Mutex::new(T::new(machine_new_params)?)))
+28 -34
View File
@@ -62,17 +62,17 @@ impl From<ModbusExceptionCode> for u8 {
impl From<u8> for ModbusExceptionCode {
fn from(value: u8) -> Self {
match value {
0 => ModbusExceptionCode::None,
1 => ModbusExceptionCode::IllegalFunction,
2 => ModbusExceptionCode::IllegalDataAddress,
3 => ModbusExceptionCode::IllegalDataValue,
4 => ModbusExceptionCode::SlaveDeviceFailure,
5 => ModbusExceptionCode::Acknowledge,
6 => ModbusExceptionCode::SlaveDeviceBusy,
8 => ModbusExceptionCode::MemoryParityError,
10 => ModbusExceptionCode::GatewayPathUnavailable,
11 => ModbusExceptionCode::GatewayTargetDeviceFailedToRespond,
other => ModbusExceptionCode::Unknown(other),
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),
}
}
}
@@ -97,10 +97,10 @@ impl TryFrom<u8> for ModbusFunctionCode {
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x03 => Ok(ModbusFunctionCode::ReadHoldingRegister),
0x04 => Ok(ModbusFunctionCode::ReadInputRegister),
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!")),
}
}
@@ -119,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
@@ -132,7 +132,7 @@ 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.
@@ -153,7 +153,7 @@ pub fn receive_data_modbus(port: &mut dyn SerialPort) -> Result<Option<Vec<u8>>,
}
/// Computes Modbus CRC-16 using `crc` crate.
pub fn modbus_crc16(data: &[u8]) -> u16 {
pub const fn modbus_crc16(data: &[u8]) -> u16 {
let modbus: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
modbus.checksum(data)
}
@@ -204,23 +204,17 @@ fn extract_crc(raw_data: &Vec<u8>) -> Result<u16, Error> {
}
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,
})
}
}
@@ -232,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)]
@@ -66,7 +66,7 @@ impl ModbusSerialInterface {
}
}
pub fn is_initialized(&self) -> bool {
pub const fn is_initialized(&self) -> bool {
!matches!(self.state, State::Uninitialized)
}
@@ -199,7 +199,7 @@ impl ModbusSerialInterface {
Some(Duration::from_nanos(total_timeout))
}
pub fn get_response(&self) -> Option<&ModbusResponse> {
pub const fn get_response(&self) -> Option<&ModbusResponse> {
self.response.as_ref()
}
+7 -7
View File
@@ -1,7 +1,7 @@
use tracing::debug;
use tracing::error;
#[cfg(unix)]
#[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:
@@ -47,7 +47,7 @@ use tracing::error;
///
/// - `Ok(())` if the real-time priority was successfully set
/// - `Err` with the error message if setting the priority failed
#[cfg(unix)]
#[cfg(target_os = "linux")]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
use anyhow::anyhow;
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
@@ -84,9 +84,9 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
Ok(())
}
#[cfg(not(unix))]
#[cfg(not(target_os = "linux"))]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
log::error!("This platform does not support realtime threads");
tracing::error!("This platform does not support realtime threads");
Ok(())
}
@@ -137,7 +137,7 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
/// # How it works
///
/// [Rest of documentation as before...]
#[cfg(unix)]
#[cfg(target_os = "linux")]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
use libc::{MCL_FUTURE, mlockall};
@@ -154,9 +154,9 @@ pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(not(unix))]
#[cfg(not(target_os = "linux"))]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
log::error!("This platform does not support memory locking");
tracing::error!("This platform does not support memory locking");
Ok(())
}
+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>,
}
+1 -1
View File
@@ -30,7 +30,7 @@ pub struct SerialDeviceNewParams {
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
}
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
+6
View File
@@ -20,6 +20,12 @@ 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 {
+3 -6
View File
@@ -82,15 +82,12 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
for port in port_list {
match &port.port_type {
SerialPortType::UsbPort(usb_info) => {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
}
_ => {}
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
};
}
return usb_ports;
usb_ports
}
pub async fn check_ports(&mut self) -> CheckPortsResult {
+8 -12
View File
@@ -30,7 +30,7 @@ impl Namespace {
#[instrument(skip_all)]
pub fn subscribe(&mut self, socket: SocketRef) {
// add the socket to the list
self.sockets.push(socket.clone());
self.sockets.push(socket);
}
/// Removes a socket from the namespace.
@@ -81,7 +81,7 @@ impl Namespace {
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");
self.send_to_queue(&socket, event, "reemit");
}
}
}
@@ -97,13 +97,10 @@ impl Namespace {
fn cache(
&mut self,
event: Arc<GenericEvent>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<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);
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.
@@ -117,10 +114,10 @@ impl Namespace {
pub fn emit(
&mut self,
event: Arc<GenericEvent>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
) {
// cache the event
self.cache(event.clone(), &buffer_fn);
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
@@ -191,7 +188,7 @@ pub trait CacheableEvents<Events> {
fn event_cache_fn(&self) -> CacheFn;
}
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>;
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>;
/// [`BufferFn`] that stores the last n events
pub fn cache_n_events(n: usize) -> CacheFn {
@@ -223,7 +220,6 @@ pub fn cache_first_and_last_event() -> CacheFn {
if events.len() == 2 {
events.remove(1);
events.push(event.clone());
return;
}
})
}
+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/{}/{}/{}",
+1 -1
View File
@@ -21,7 +21,7 @@ impl FixedTransmission {
/// # Returns
///
/// A new instance of FixedTransmission.
pub fn new(ratio: f64) -> Self {
pub const fn new(ratio: f64) -> Self {
Self { ratio }
}
}
+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);
}
}
+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 },
}
```
@@ -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.
+127
View File
@@ -115,3 +115,130 @@ pub fn (
}
}
```
# Device Implementation Guide
This section describes how to implement EtherCAT devices in the framework used by the QiTech Control project (via `ethercat-hal`, `server`, etc.). We include a running example of a simple digital output device (EL2004) and discuss how it fits into the larger architecture.
## Architecture & Context
The QiTech Control system is built as:
- `ethercat-hal` / `ethercat-hal-derive`: Hardware abstraction, PDO / CoE handling, device traits.
- `server`: Glue code that reads device identities, instantiates the correct device types, performs CoE configuration, and runs cyclic process data.
- The `docs` directory documents APIs and device patterns.
:contentReference[oaicite:0]{index=0}
In this architecture:
1. During initialization, subdevice identity tuples (Vendor ID, Product ID, Revision) are read.
2. The `server` matches identity tuples to concrete `Device` types and constructs them (via `NewEthercatDevice`).
3. The device instance then handles PDO exchange, and optional CoE (configuration) writes.
4. Higher-level application code interacts with devices via traits like `DigitalOutputDevice`, `DigitalInputDevice`, etc.
---
## Example: Simple Digital Output Device (EL2004)
Below is a minimal example of a digital-output-only EtherCAT device. It shows how to map PDOs, implement traits, and define identity constants.
```rust
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
/// EL2004 4-channel digital output device — 24 V DC, 0.5 A per channel
#[derive(EthercatDevice)]
pub struct EL2004 {
/// The Rx PDO where output bits are written by the master
pub rxpdo: EL2004RxPdo,
is_used: bool,
}
impl EthercatDeviceProcessing for EL2004 {}
impl std::fmt::Debug for EL2004 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EL2004")
}
}
impl NewEthercatDevice for EL2004 {
fn new() -> Self {
Self {
rxpdo: EL2004RxPdo::default(),
is_used: false,
}
}
}
impl DigitalOutputDevice<EL2004Port> for EL2004 {
fn set_output(&mut self, port: EL2004Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2004Port::DO1 => {
self.rxpdo.channel1.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO2 => {
self.rxpdo.channel2.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO3 => {
self.rxpdo.channel3.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO4 => {
self.rxpdo.channel4.as_mut().expect(expect_text).value = value.into()
}
}
}
fn get_output(&self, port: EL2004Port) -> DigitalOutputOutput {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2004Port::DO1 => self.rxpdo.channel1.as_ref().expect(expect_text).value,
EL2004Port::DO2 => self.rxpdo.channel2.as_ref().expect(expect_text).value,
EL2004Port::DO3 => self.rxpdo.channel3.as_ref().expect(expect_text).value,
EL2004Port::DO4 => self.rxpdo.channel4.as_ref().expect(expect_text).value,
})
}
}
#[derive(Debug, Clone)]
pub enum EL2004Port {
DO1,
DO2,
DO3,
DO4,
}
#[derive(Debug, Clone, RxPdo)]
pub struct EL2004RxPdo {
#[pdo_object_index(0x1600)]
pub channel1: Option<BoolPdoObject>,
#[pdo_object_index(0x1601)]
pub channel2: Option<BoolPdoObject>,
#[pdo_object_index(0x1602)]
pub channel3: Option<BoolPdoObject>,
#[pdo_object_index(0x1603)]
pub channel4: Option<BoolPdoObject>,
}
impl Default for EL2004RxPdo {
fn default() -> Self {
Self {
channel1: Some(BoolPdoObject::default()),
channel2: Some(BoolPdoObject::default()),
channel3: Some(BoolPdoObject::default()),
channel4: Some(BoolPdoObject::default()),
}
}
}
/// Identity constants for EL2004
pub const EL2004_VENDOR_ID: u32 = 0x2;
pub const EL2004_PRODUCT_ID: u32 = 131346514;
pub const EL2004_REVISION_A: u32 = 1179648;
pub const EL2004_IDENTITY_A: SubDeviceIdentityTuple =
(EL2004_VENDOR_ID, EL2004_PRODUCT_ID, EL2004_REVISION_A);
+40
View File
@@ -0,0 +1,40 @@
# Ethercat Tab shows no Ethercat Terminals!
When the Ethercat page does not show any terminals, this can be due to multiple reasons
## 1: Incorrect Connection of EK1100
Check that the computers ethernet cable is properly connected to the EK1100 IN port
Check the cabling if more than one EK1100 is used.
When you have multiple EK the connection should be like this:
Master (Your pc) -> Ethernet cable -> IN EK1100 (A)
OUT EK1100 (A) -> Ethernet cable -> IN EK1100 (B)
If you connected any part of this incorrectly the ethercat software will try to communicate but will fail, because the protocol works only if all devices are connect in series.
## 2: Defective EtherCAT Terminal
If a Terminal is somehow defective it can disrupt the communication flow expected by EtherCAT thus causing no devices to show up, as ethercat requires all terminals to work properly.
To identify the defective terminal start unplugging terminals starting from the End terminal. Working your way backwards. Every time you unplug a terminal restart the backend process.
You know that you have removed the defective terminal when the ethercat tab shows devices after your restart.
Replace that terminal with an appropriate one and repeat this if the error persists.
## 3: Corrupt Firmware on EtherCAT Terminal
Another reason, that an Ethercat Terminal doesnt work properly, is that the firmware is corrupt. This could happen due to overvoltage,undervoltage or shorts, in some cases this is recoverable by reflashing the firmware.
Our repository contains dumps for all of the EtherCAT Terminals, that we have used. To upload the firmware you can follow this guide: https://infosys.beckhoff.com/english.php?content=../content/1033/ethercatsystem/402909835.html&id=
To follow this guide, you need to have TwinCat installed and a License.
# Motor does not start!
If the Motor does not start after trying to talk to the inverter, the most common reason is that the inverter's serial connection settings (baudrate,encoding ...) are incorrect.
The correct settings can be found in mitsubishi_inverter.md under `All Settings needed for the Inverter Communication/Operation with Extruder`
+195 -42
View File
@@ -11,11 +11,11 @@
"@hookform/resolvers": "^4.1.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.8",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.3",
@@ -24,7 +24,7 @@
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.2",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-router": "^1.105.0",
"@tanstack/react-router": "^1.130.17",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
"@tanstack/zod-adapter": "^1.116.0",
@@ -43,6 +43,7 @@
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
@@ -54,7 +55,7 @@
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zod-validation-error": "^3.4.0",
"zustand": "^5.0.3"
"zustand": "^5.0.8"
},
"devDependencies": {
"@electron-forge/cli": "^7.6.1",
@@ -77,9 +78,10 @@
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/node": "^24.2.1",
"@types/react": "^19.0.10",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"electron": "^37.2.6",
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
@@ -95,7 +97,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.6",
"vite": "^6.3.6",
"vitest": "^3.0.6"
}
},
@@ -2323,15 +2325,16 @@
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
"integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
@@ -2351,6 +2354,36 @@
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -2624,18 +2657,19 @@
}
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz",
"integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
@@ -2658,6 +2692,63 @@
}
}
},
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
@@ -3933,13 +4024,14 @@
}
},
"node_modules/@tanstack/react-router": {
"version": "1.130.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.12.tgz",
"integrity": "sha512-7BYgOpGc1vK8MH1LIFLLBudGpH46GQy+hewnP7dNQJ4KHmkwPHv958L1IMA9jU/rs5g1ZH5n1f33BAMOBXUMYQ==",
"version": "1.130.17",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
"@tanstack/router-core": "1.130.12",
"@tanstack/router-core": "1.130.17",
"isbot": "^5.1.22",
"tiny-invariant": "^1.3.3",
"tiny-warning": "^1.0.3"
@@ -4013,9 +4105,10 @@
}
},
"node_modules/@tanstack/router-core": {
"version": "1.130.12",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.12.tgz",
"integrity": "sha512-emq3cRU9Na1hnIToojzkfJcOZm/MG2bv9M+Kr/elUxEf83enGEwQXC1EKezTuwNgeJrOv8vPJdEhWM7IQodnHQ==",
"version": "1.130.17",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4411,10 +4504,18 @@
"undici-types": "~7.10.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"version": "19.1.16",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4428,6 +4529,17 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-virtualized": {
"version": "9.22.2",
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"@types/react": "*"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
@@ -6264,6 +6376,16 @@
"dev": true,
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -6285,9 +6407,9 @@
"dev": true
},
"node_modules/electron": {
"version": "37.2.6",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz",
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==",
"version": "37.3.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.3.1.tgz",
"integrity": "sha512-7DhktRLqhe6OJh/Bo75bTI0puUYEmIwSzMinocgO63mx3MVjtIn2tYMzLmAleNIlud2htkjpsMG2zT4PiTCloA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -9886,7 +10008,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -11270,7 +11391,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12056,7 +12176,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -12066,8 +12185,7 @@
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/property-information": {
"version": "7.1.0",
@@ -12195,6 +12313,12 @@
"dev": true,
"peer": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"license": "MIT"
},
"node_modules/react-markdown": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
@@ -12295,6 +12419,33 @@
}
}
},
"node_modules/react-virtualized": {
"version": "9.22.6",
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.7.2",
"clsx": "^1.0.4",
"dom-helpers": "^5.1.3",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-virtualized/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@@ -14521,9 +14672,10 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -15204,9 +15356,10 @@
}
},
"node_modules/zustand": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz",
"integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
+9 -7
View File
@@ -42,9 +42,10 @@
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/node": "^24.2.1",
"@types/react": "^19.0.10",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"electron": "^37.2.6",
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
@@ -60,18 +61,18 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.6",
"vite": "^6.3.6",
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.8",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.3",
@@ -80,7 +81,7 @@
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.2",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-router": "^1.105.0",
"@tanstack/react-router": "^1.130.17",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
"@tanstack/zod-adapter": "^1.116.0",
@@ -99,6 +100,7 @@
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
@@ -110,6 +112,6 @@
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zod-validation-error": "^3.4.0",
"zustand": "^5.0.3"
"zustand": "^5.0.8"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

@@ -697,3 +697,26 @@ The following persons must not work on the machine:
| Cleaning, maintenance | |
| Maintenance | Trained and instructed specialised personnel of the operator or service technicians of the manufacturer |
| Decommissioning | Trained and instructed specialised personnel of the operator |
<br>
<br>
## Electrical Diagrams
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 1](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025-001.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 2](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_2-002.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 3](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_3-003.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 4](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_4-004.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 5](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_5-005.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 6](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_6-006.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 7](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_7-007.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
@@ -416,6 +416,29 @@ Then start the machine as follows:
<br>
**Understanding Padding**
The **padding** parameter creates a safety margin that prevents the filament from winding too close to the spool edges. When you set the inner and outer limits during calibration, the padding value is automatically subtracted from the actual travel distance.
**How padding works:**
- When you have a 1.75mm diameter filament and the traverse moves to a flank position, the **center** of the filament aligns with that flank
- If you set 1mm padding, the system automatically maintains at least 1mm distance from the flanks
- This prevents the filament from overlapping or winding beyond the spool edges
**Example:**
- Outer limit: 90mm, Inner limit: 22mm
- Padding: 1mm
- **Actual winding area:** 89mm to 23mm (reduced by 1mm on each side)
- This ensures the filament stays within the spool boundaries with a safe margin
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
![Padding example](images/manuals/winder/winder_padding_example_image.jpg)
<br>
**4. Start pulling**
- Select “Control” in the function bar
@@ -687,3 +710,18 @@ The following persons may not work on the machine:
| K Cleaning, maintenance | |
| L Maintenance | Trained and instructed specialized personnel of the operator or service technicians of the manufacturer |
| M Decommissioning | Trained and instructed specialized personnel of the operator |
<br>
<br>
## Electrical Diagrams
![QiTech Pro Winder Electrical Diagram - Sheet 1](images/manuals/winder/QiTech_Pro_Winder_Electrical_Diagram-001.png)
![QiTech Pro Winder Electrical Diagram - Sheet 2](images/manuals/winder/QiTech_Pro_Winder_Electrical_Diagram_2-002.png)
![QiTech Pro Winder Electrical Diagram - Sheet 3](images/manuals/winder/QiTech_Pro_Winder_Electrical_Diagram_3-003.png)
![QiTech Pro Winder Electrical Diagram - Sheet 4](images/manuals/winder/QiTech_Pro_Winder_Electrical_Diagram_4-004.png)
![QiTech Pro Winder Electrical Diagram - Sheet 5](images/manuals/winder/QiTech_Pro_Winder_Electrical_Diagram_5-005.png)
+2 -7
View File
@@ -58,15 +58,10 @@ export function MarkdownWithToc({
className={`sticky ${stickyTop}`}
style={{ maxHeight: "calc(100vh - 6rem)" }}
>
{/* Hide scrollbar for the TOC container across browsers */}
<style>{`
[data-toc-scroll-container] { -ms-overflow-style: none; scrollbar-width: none; }
[data-toc-scroll-container]::-webkit-scrollbar { display: none; }
`}</style>
{/* Table of Contents with hidden scrollbar */}
<div
className="overflow-y-auto"
className="scrollbar-hidden overflow-y-auto"
style={{ maxHeight: "calc(100vh - 8rem)" }}
data-toc-scroll-container
>
<MarkdownToc
markdownContent={markdownContent}
+78 -123
View File
@@ -1,20 +1,32 @@
import { cva } from "class-variance-authority";
import React, { useEffect, useRef, useState } from "react";
import { Icon } from "./Icon";
import {
List,
CellMeasurer,
CellMeasurerCache,
ListRowRenderer,
AutoSizer,
} from "react-virtualized";
// Create cache (outside the component so it persists across renders)
const cache = new CellMeasurerCache({
defaultHeight: 20,
fixedWidth: true,
});
type Props = {
lines: string[];
autoScroll?: boolean; // Optional prop to control if terminal should auto-scroll
className?: string; // Optional prop to control terminal height
autoScroll?: boolean;
className?: string;
title?: string;
exportPrefix?: string; // Optional prefix for exported log files
exportPrefix?: string;
};
const terminalStyle = cva([
"flex flex-col overflow-hidden rounded-md border border-neutral-700 font-mono text-sm",
]);
// Color mapping for ANSI color codes
const colorMap: Record<string, string> = {
"30": "text-gray-900",
"31": "text-red-600",
@@ -32,7 +44,6 @@ const colorMap: Record<string, string> = {
"95": "text-purple-400",
"96": "text-cyan-400",
"97": "text-white",
// Background colors
"40": "bg-gray-900",
"41": "bg-red-600",
"42": "bg-green-600",
@@ -43,36 +54,26 @@ const colorMap: Record<string, string> = {
"47": "bg-gray-200",
};
// Parse ANSI color codes in text
const parseColorCodes = (text: string) => {
// Split by ANSI escape sequences
// eslint-disable-next-line no-control-regex
/* eslint-disable-next-line no-control-regex */
const parts = text.split(/(\x1b\[\d+m)/g);
let currentClass = "";
const result: { text: string; className: string }[] = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.startsWith("\x1b[")) {
// This is a color code
const code = part.slice(2, -1); // Extract the number from \x1b[XXm
const code = part.slice(2, -1);
currentClass = colorMap[code] || "";
} else if (part) {
// This is text content
result.push({ text: part, className: currentClass });
}
}
return result;
};
// Function to strip ANSI color codes for plain text copy
const stripColorCodes = (text: string): string => {
const stripColorCodes = (text: string): string =>
// eslint-disable-next-line no-control-regex
return text.replace(/\x1b\[\d+m/g, "");
};
text.replace(/\x1b\[\d+m/g, "");
export function Terminal({
lines,
@@ -81,99 +82,88 @@ export function Terminal({
title = "Terminal",
exportPrefix,
}: Props) {
const terminalRef = useRef<HTMLDivElement>(null);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
const listRef = useRef<List>(null);
const [copySuccess, setCopySuccess] = useState(false);
const [exportSuccess, setExportSuccess] = useState(false);
// Handle scrolling
// Auto-scroll to bottom when new lines arrive
useEffect(() => {
const terminal = terminalRef.current;
if (!terminal) return;
// If auto-scroll is enabled and user was at bottom, scroll to bottom when lines change
if (autoScroll && isScrolledToBottom) {
terminal.scrollTop = terminal.scrollHeight;
if (autoScroll && listRef.current) {
listRef.current.scrollToRow(lines.length - 1);
}
}, [lines, autoScroll, isScrolledToBottom]);
}, [lines, autoScroll]);
// Handle scroll events to detect if user is at bottom
const handleScroll = () => {
const terminal = terminalRef.current;
if (!terminal) return;
const isAtBottom =
Math.abs(
terminal.scrollHeight - terminal.clientHeight - terminal.scrollTop,
) < 10; // Small threshold to account for rounding errors
setIsScrolledToBottom(isAtBottom);
};
// Handle copy to clipboard
const handleCopy = async () => {
// Strip ANSI color codes and join lines
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
const plainText = lines.map(stripColorCodes).join("\n");
try {
await navigator.clipboard.writeText(plainText);
setCopySuccess(true);
// Reset copy success message after 2 seconds
setTimeout(() => {
setCopySuccess(false);
}, 2000);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
// Handle export to file
const handleExport = () => {
if (!exportPrefix) return;
// Strip ANSI color codes and join lines
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
// Create timestamp for filename
const plainText = lines.map(stripColorCodes).join("\n");
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, -5);
const filename = `${exportPrefix}_${timestamp}.log`;
// Create blob and download
const blob = new Blob([plainText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
// Create temporary link element and trigger download
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up
URL.revokeObjectURL(url);
// Show success feedback
setExportSuccess(true);
setTimeout(() => {
setExportSuccess(false);
}, 2000);
setTimeout(() => setExportSuccess(false), 2000);
};
const rowRenderer: ListRowRenderer = ({ index, key, parent, style }) => {
const colorParts = parseColorCodes(lines[index] || "");
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<div
style={style}
className="bg-black px-2 py-1 whitespace-pre-wrap text-white"
>
{colorParts.length > 0
? colorParts.map((part, partIndex) => (
<span
key={partIndex}
className={part.className || "text-white"}
>
{part.text || " "}
</span>
))
: lines[index] || " "}
</div>
</CellMeasurer>
);
};
return (
<div className={terminalStyle({ className })}>
{/* Terminal header */}
{/* Header */}
<div className="flex items-center justify-between border-b border-neutral-700 bg-neutral-800 px-4 py-2">
<div className="text-xs text-neutral-400">{title}</div>
<div className="flex items-center gap-2">
<button
onClick={handleCopy}
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
title="Copy to clipboard"
>
{copySuccess ? (
<>
@@ -191,7 +181,6 @@ export function Terminal({
<button
onClick={handleExport}
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
title="Export logs to file"
>
{exportSuccess ? (
<>
@@ -209,62 +198,28 @@ export function Terminal({
</div>
</div>
{/* Terminal content with custom scrollbar styling */}
<div
ref={terminalRef}
onScroll={handleScroll}
className={`scrollbar-thin scrollbar-thumb-neutral-600 scrollbar-track-transparent flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300`}
style={{
scrollbarWidth: "thin",
scrollbarColor: "rgb(82 82 91) transparent",
}}
>
<style>{`
Add commentMore actions
/* For Webkit browsers (Chrome, Safari) */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(82 82 91);
border-radius: 3px;
}
/* For Firefox */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(82 82 91) transparent;
}
`}</style>
{lines.map((line, index) => {
const colorParts = parseColorCodes(line);
return (
<div key={index} className="whitespace-pre-wrap">
{colorParts.length > 0
? colorParts.map((part, partIndex) => (
<span key={partIndex} className={part.className}>
{part.text || " "}
</span>
))
: line || " "}
</div>
);
})}
{/* Virtualized List */}
<div className="flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300">
<AutoSizer>
{({ width, height }) => (
<List
ref={listRef}
width={width}
height={height}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowCount={lines.length}
rowRenderer={rowRenderer}
overscanRowCount={5}
/>
)}
</AutoSizer>
</div>
{/* Status bar */}
{/* Footer */}
<div className="flex items-center justify-between bg-neutral-800 px-4 py-1 text-xs text-neutral-400">
<div>{lines.length} lines</div>
<div>
{isScrolledToBottom ? "At bottom" : "Scrolled up"} |
{autoScroll ? " Auto-scroll enabled" : " Auto-scroll disabled"}
</div>
<div>{autoScroll ? "Auto-scroll enabled" : "Auto-scroll disabled"}</div>
</div>
</div>
);
@@ -474,6 +474,7 @@ export function BigGraph({
isLiveMode,
selectedTimeWindow,
visibleSeries, // Keep for initial creation
showFromTimestamp: syncGraph?.showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
setIsLiveMode,
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TimeInput } from "@/components/time/TimeInput";
import {
DropdownMenu,
DropdownMenuContent,
@@ -21,8 +22,14 @@ export function GraphControls({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -35,7 +42,10 @@ export function GraphControls({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -60,6 +70,15 @@ export function GraphControls({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
@@ -109,10 +128,16 @@ export function FloatingControlPanel({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const [isExpanded, setIsExpanded] = useState(false);
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -134,7 +159,10 @@ export function FloatingControlPanel({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -159,6 +187,15 @@ export function FloatingControlPanel({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
+6 -7
View File
@@ -31,10 +31,8 @@ export function createChart({
setCursorValue,
setCursorValues,
visibleSeries,
}: CreateChartParams & {
setCursorValues?: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries?: boolean[];
}): (() => void) | undefined {
showFromTimestamp,
}: CreateChartParams): (() => void) | undefined {
if (!containerRef.current) return;
// Get ALL series from original data
@@ -73,7 +71,7 @@ export function createChart({
initialMin = manualScaleRef.current.x.min;
initialMax = manualScaleRef.current.x.max;
} else if (selectedTimeWindow === "all") {
initialMin = fullStart;
initialMin = showFromTimestamp ?? fullStart;
initialMax = getHistoricalEndTimestamp();
} else {
const endTimestamp = getHistoricalEndTimestamp();
@@ -83,9 +81,10 @@ export function createChart({
endTimestamp - (selectedTimeWindow as number),
fullStart,
);
initialMin = defaultViewStart;
initialMin = showFromTimestamp ?? defaultViewStart;
} else {
initialMin = endTimestamp - (selectedTimeWindow as number);
initialMin =
showFromTimestamp ?? endTimestamp - (selectedTimeWindow as number);
}
initialMax = endTimestamp;
+6
View File
@@ -10,6 +10,7 @@ export type PropGraphSync = {
isLiveMode: boolean;
xRange?: { min: number; max: number };
historicalFreezeTimestamp?: number | null;
showFromTimestamp?: number | null;
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
onViewModeChange?: (
graphId: string,
@@ -81,6 +82,8 @@ export type ControlProps = {
onSwitchToHistorical: () => void;
onExport?: () => void;
timeWindowOptions?: TimeWindowOption[];
showFromTimestamp?: number | null;
onShowFromChange?: (timestamp: number | null) => void;
};
export interface LiveModeHandlers {
@@ -128,6 +131,9 @@ export interface CreateChartParams {
>;
setIsLiveMode: React.Dispatch<React.SetStateAction<boolean>>;
setCursorValue: React.Dispatch<React.SetStateAction<number | null>>;
setCursorValues: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries: boolean[];
showFromTimestamp?: number | null;
}
export interface AnimationState {
@@ -41,6 +41,7 @@ interface UseBigGraphEffectsProps {
isLiveMode: boolean;
selectedTimeWindow: number | "all";
visibleSeries: boolean[];
showFromTimestamp?: number | null;
// State setters
setSelectedTimeWindow: (value: number | "all") => void;
@@ -90,6 +91,7 @@ export function useBigGraphEffects({
isLiveMode,
selectedTimeWindow,
visibleSeries,
showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
@@ -275,6 +277,7 @@ export function useBigGraphEffects({
setCursorValue,
setCursorValues,
visibleSeries,
showFromTimestamp,
});
setIsChartCreated(true);
@@ -15,6 +15,9 @@ export function useGraphSync(exportGroupId?: string) {
const [xRange, setXRange] = useState<
{ min: number; max: number } | undefined
>();
const [showFromTimestamp, setShowFromTimestamp] = useState<number | null>(
null,
);
const historicalFreezeTimestampRef = useRef<number | null>(null);
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
@@ -59,6 +62,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode?: "default" | "all" | "manual";
isLiveMode?: boolean;
xRange?: { min: number; max: number } | undefined;
showFromTimestamp?: number | null;
clearHistoricalFreeze?: boolean;
setHistoricalFreeze?: boolean;
},
@@ -90,6 +94,8 @@ export function useGraphSync(exportGroupId?: string) {
if (updates.viewMode !== undefined) setViewMode(updates.viewMode);
if (updates.isLiveMode !== undefined) setIsLiveMode(updates.isLiveMode);
if (updates.xRange !== undefined) setXRange(updates.xRange);
if (updates.showFromTimestamp !== undefined)
setShowFromTimestamp(updates.showFromTimestamp);
// Handle historical freeze timestamp
if (updates.clearHistoricalFreeze) {
@@ -195,6 +201,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode: timeWindow === "all" ? "all" : "default",
xRange: undefined,
clearHistoricalFreeze: true,
showFromTimestamp: null, // Clear the show from timestamp when switching to live
});
}, [updateSyncState, timeWindow]);
@@ -206,6 +213,15 @@ export function useGraphSync(exportGroupId?: string) {
});
}, [updateSyncState]);
const handleShowFromChange = useCallback(
(timestamp: number | null) => {
updateSyncState("control", {
showFromTimestamp: timestamp,
});
},
[updateSyncState],
);
const handleControlTimeWindowChange = useCallback(
(newTimeWindow: number | "all") => {
updateSyncState("control", {
@@ -225,6 +241,7 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
onTimeWindowChange: handleTimeWindowChange,
onViewModeChange: handleViewModeChange,
onZoomChange: handleZoomChangeThrottled,
@@ -237,6 +254,8 @@ export function useGraphSync(exportGroupId?: string) {
onSwitchToLive: handleSwitchToLive,
onSwitchToHistorical: handleSwitchToHistorical,
onExport: handleExport,
showFromTimestamp,
onShowFromChange: handleShowFromChange,
};
return {
@@ -250,5 +269,6 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
};
}
+367
View File
@@ -0,0 +1,367 @@
import React, { useState, useCallback, useEffect } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Separator } from "@/components/ui/separator";
type TimeInputProps = {
timestamp?: number | null;
onTimeChange?: (timestamp: number | null) => void;
onClear?: () => void;
};
const timeSchema = z.object({
hours: z.number().min(0).max(23),
minutes: z.number().min(0).max(59),
seconds: z.number().min(0).max(59),
});
type TimeFormData = z.infer<typeof timeSchema>;
export function TimeInput({
timestamp,
onTimeChange,
onClear,
}: TimeInputProps) {
const [isOpen, setIsOpen] = useState(false);
// Get current time for initial values
const getCurrentTime = useCallback(() => {
const now = new Date();
return {
hours: now.getHours(),
minutes: now.getMinutes(),
seconds: now.getSeconds(),
};
}, []);
// Get initial values based on timestamp or current time
const getInitialValues = useCallback(() => {
if (timestamp) {
const date = new Date(timestamp);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}
return getCurrentTime();
}, [timestamp, getCurrentTime]);
const form = useForm<TimeFormData>({
resolver: zodResolver(timeSchema),
defaultValues: getInitialValues(),
mode: "all",
});
// Convert timestamp to time components
const timestampToTime = useCallback((ts: number) => {
const date = new Date(ts);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}, []);
// Check if the selected time would be in the future (today)
const isTimeInFuture = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime() > now.getTime();
}, []);
// Convert time components to timestamp (today's date only)
const timeToTimestamp = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime();
}, []);
// Initialize form with timestamp or current time when dialog opens
useEffect(() => {
if (isOpen) {
const initialValues = getInitialValues();
form.reset(initialValues);
}
}, [isOpen, getInitialValues, form]);
const formatTimeDisplay = useCallback(() => {
if (!timestamp) return "Show from Time";
const time = timestampToTime(timestamp);
return `${time.hours.toString().padStart(2, "0")}:${time.minutes.toString().padStart(2, "0")}:${time.seconds.toString().padStart(2, "0")}`;
}, [timestamp, timestampToTime]);
// Increment/decrement functions with bounds checking and cascading
const adjustTimeValue = useCallback(
(field: "hours" | "minutes" | "seconds", increment: boolean) => {
// Define the order of fields for cascading
const fields: ("hours" | "minutes" | "seconds")[] = [
"seconds",
"minutes",
"hours",
];
const startIndex = fields.indexOf(field);
let carry = increment ? 1 : -1;
for (let i = startIndex; i < fields.length; i++) {
const currentField = fields[i];
const currentValue = form.getValues(currentField);
const max = currentField === "hours" ? 23 : 59;
let newValue = currentValue + carry;
carry = 0;
if (increment) {
if (newValue > max) {
newValue = 0;
carry = 1;
}
} else {
if (newValue < 0) {
newValue = max;
carry = -1;
}
}
form.setValue(currentField, newValue, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
// If no carry, stop cascading
if (carry === 0) {
break;
}
}
},
[form],
);
const handleSubmit = () => {
form.handleSubmit((data) => {
// Prevent submission if time is in the future
if (isTimeInFuture(data)) {
return;
}
const newTimestamp = timeToTimestamp(data);
onTimeChange?.(newTimestamp);
setIsOpen(false);
})();
};
const handleAbort = () => {
if (timestamp) {
const time = timestampToTime(timestamp);
form.reset(time);
} else {
const currentTime = getCurrentTime();
form.reset(currentTime);
}
setIsOpen(false);
};
const handleClear = () => {
onClear?.();
setIsOpen(false);
};
// Watch individual form fields for real-time updates
const hours = useWatch({
control: form.control,
name: "hours",
defaultValue: getInitialValues().hours,
});
const minutes = useWatch({
control: form.control,
name: "minutes",
defaultValue: getInitialValues().minutes,
});
const seconds = useWatch({
control: form.control,
name: "seconds",
defaultValue: getInitialValues().seconds,
});
// Get current form values with fallback
const currentValues = {
hours: hours ?? 0,
minutes: minutes ?? 0,
seconds: seconds ?? 0,
};
// TimeValueInput component for individual time fields
const TimeValueInput = ({
field,
label,
value,
}: {
field: "hours" | "minutes" | "seconds";
label: string;
value: number;
}) => (
<div className="flex flex-col items-center gap-3">
<label className="text-sm font-medium text-gray-700">{label}</label>
<div className="flex flex-col items-center gap-2">
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, true);
}}
>
<Icon name="lu:ChevronUp" className="size-4" />
</TouchButton>
<div className="flex h-16 w-20 items-center justify-center rounded border-2 border-gray-300 bg-white font-mono text-3xl">
{value.toString().padStart(2, "0")}
</div>
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, false);
}}
>
<Icon name="lu:ChevronDown" className="size-4" />
</TouchButton>
</div>
</div>
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
>
<Icon name="lu:Clock" className="mr-2 size-4" />
{formatTimeDisplay()}
</TouchButton>
</DialogTrigger>
<DialogContent className="max-w-2xl p-8">
<div className="flex flex-col gap-8">
{/* Header */}
<div className="flex min-h-[2.5rem] items-center justify-between">
<div className="flex items-center gap-3">
<Icon name="lu:Clock" className="size-6" />
<h3 className="text-xl font-semibold">Show from Time</h3>
</div>
{timestamp && (
<TouchButton
variant="ghost"
size="sm"
onClick={handleClear}
className="text-red-600 hover:text-red-700"
>
<Icon name="lu:X" className="mr-2 size-4" />
Clear
</TouchButton>
)}
</div>
{/* Description */}
<div className="-mt-4 text-base text-gray-600">
Set a specific time to display data from that point forward in the
graph.
</div>
{/* Error for future time */}
{isTimeInFuture(currentValues) && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<div className="flex items-start gap-3">
<Icon name="lu:X" className="mt-0.5 size-5 text-red-600" />
<div>
<div className="font-medium text-red-800">
Cannot set future time
</div>
<div className="mt-1 text-sm text-red-700">
The selected time is in the future. Please choose a time
that has already occurred today.
</div>
</div>
</div>
</div>
)}
<Separator />
{/* Time inputs */}
<div className="flex items-center justify-center gap-8">
<TimeValueInput
field="hours"
label="Hours"
value={currentValues.hours}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="minutes"
label="Minutes"
value={currentValues.minutes}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="seconds"
label="Seconds"
value={currentValues.seconds}
/>
</div>
<Separator />
{/* Action buttons */}
<div className="flex gap-4">
<TouchButton
variant="outline"
className="h-14 flex-1"
onClick={handleAbort}
>
<Icon name="lu:X" className="mr-2 size-4" />
Cancel
</TouchButton>
<TouchButton
className={`h-14 flex-1 ${
isTimeInFuture(currentValues)
? "cursor-not-allowed bg-gray-300 text-gray-500"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
onClick={handleSubmit}
disabled={isTimeInFuture(currentValues)}
>
<Icon name="lu:Check" className="mr-2 size-4" />
Apply
</TouchButton>
</div>
</div>
</DialogContent>
</Dialog>
);
}
+14
View File
@@ -31,6 +31,10 @@ export function getUnitIcon(unit: Unit): IconName {
return "lu:Zap";
case "A":
return "lu:Zap";
case "kWh":
return "lu:BatteryFull";
case "l/min":
return "lu:Waves";
default:
return "lu:ChartNoAxesColumn";
}
@@ -64,6 +68,10 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
return "V";
case "A":
return "A";
case "kWh":
return "kWh";
case "l/min":
return "l/min";
default:
return "";
}
@@ -119,6 +127,10 @@ export function renderUnitSymbolLong(unit: Unit): string {
return "volts";
case "A":
return "amperes";
case "kWh":
return "kilowatt-hours";
case "l/min":
return "liters/minute";
default:
return "";
}
@@ -138,6 +150,8 @@ export const units = [
"W",
"V",
"A",
"kWh",
"l/min",
] as const;
export type Unit = (typeof units)[number];
@@ -145,7 +145,7 @@ async function deleteNixOSGeneration(generationId: string): Promise<{
const process = spawn("sudo", [
"sh",
"-c",
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && nix store gc && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
]);
let stderr = "";
@@ -18,17 +18,39 @@ type UpdateExecuteInvokeParams = {
export function exposeUpdateContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
let currentLogListener: ((event: any, log: string) => void) | null = null;
let currentEndListener: ((event: any, params: any) => void) | null = null;
contextBridge.exposeInMainWorld("update", {
execute: (params: UpdateExecuteInvokeParams) =>
ipcRenderer.invoke(UPDATE_EXECUTE, params),
cancel: () => ipcRenderer.invoke(UPDATE_CANCEL),
onLog: (callback: (log: string) => void) =>
ipcRenderer.on(UPDATE_LOG, (_event, log: string) => {
onLog: (callback: (log: string) => void) => {
if (currentLogListener) {
ipcRenderer.removeListener(UPDATE_LOG, currentLogListener);
}
currentLogListener = (_event, log: string) => {
callback(log);
}),
onEnd: (callback: (params: { success: boolean; error?: string }) => void) =>
ipcRenderer.on(UPDATE_END, (_event, params) => {
};
ipcRenderer.on(UPDATE_LOG, currentLogListener);
},
onEnd: (
callback: (params: { success: boolean; error?: string }) => void,
) => {
if (currentEndListener) {
ipcRenderer.removeListener(UPDATE_END, currentEndListener);
}
currentEndListener = (_event, params) => {
callback(params);
}),
};
ipcRenderer.on(UPDATE_END, currentEndListener);
},
});
}
+18 -3
View File
@@ -1,16 +1,28 @@
import type { UpdateInfo } from "@/stores/updateStore";
import { useUpdateStore } from "@/stores/updateStore";
let currentLogListener: ((line: string) => void) | null = null;
export async function updateExecute(
source: UpdateInfo,
onLog: (log: string) => void,
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
window.update.onLog(onLog);
// Remove previous listener if exists
if (currentLogListener) {
window.update.onLog(() => {}); // clear previous
currentLogListener = null;
}
currentLogListener = onLog;
window.update.onLog(currentLogListener);
window.update.execute(source);
window.update.onEnd((params) => {
window.update.onLog(() => {});
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {});
currentLogListener = null;
resolve(params);
});
});
@@ -40,7 +52,10 @@ export async function updateCancel(): Promise<{
success: boolean;
error?: string;
}> {
return window.update.cancel();
const result = await window.update.cancel();
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {}); // remove listener
return result;
}
// Enhanced helper that automatically manages store state
@@ -0,0 +1,207 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { useAquapath1 } from "./useAquapath";
import { SelectionGroup } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
export function Aquapath1ControlPage() {
const {
state,
defaultState,
front_flow,
back_flow,
front_temperature,
back_temperature,
front_temp_reservoir,
back_temp_reservoir,
setAquapathMode,
setFrontTemperature,
setBackTemperature,
setFrontFlow,
setBackFlow,
} = useAquapath1();
const frontTargetTemperature =
state?.temperature_states?.front.target_temperature ?? 0;
const backTargetTemperature =
state?.temperature_states?.back.target_temperature ?? 0;
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Flow Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Front Flow"
unit="l/min"
timeseries={front_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Back Flow"
unit="l/min"
timeseries={back_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
</div>
<div className="mt-8">
<Label label="Front Pump">
<SelectionGroup<"On" | "Off">
value={frontTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setFrontFlow(true);
} else {
setFrontFlow(false);
}
}}
/>
</Label>
<Label label="Back Pump">
<SelectionGroup<"On" | "Off">
value={backTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setBackFlow(true);
} else {
setBackFlow(false);
}
}}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Temperature Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Front in"
unit="C"
timeseries={front_temperature}
renderValue={(value) => value.toFixed(1)}
/>
<TimeSeriesValueNumeric
label="Front Reservoir"
unit="C"
timeseries={front_temp_reservoir}
renderValue={(value) => value.toFixed(1)}
/>
<Label label="Set Front Target Temperature">
<EditValue
title="Set Front Target Temperature"
value={frontTargetTemperature}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontTemperature(val);
}}
defaultValue={
defaultState?.temperature_states.front.target_temperature
}
/>
</Label>
</div>
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Back in"
unit="C"
timeseries={back_temperature}
renderValue={(value) => value.toFixed(1)}
/>
<TimeSeriesValueNumeric
label="Back Reservoir"
unit="C"
timeseries={back_temp_reservoir}
renderValue={(value) => value.toFixed(1)}
/>
<Label label="Set Back Target Temperature">
<EditValue
title="Set Back Target Temperature"
value={backTargetTemperature}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackTemperature(val);
}}
defaultValue={
defaultState?.temperature_states.back.target_temperature
}
/>
</Label>
</div>
</div>
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Standby" | "Auto">
value={state?.mode_state.mode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Auto: {
children: "Auto",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setAquapathMode(value)}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,186 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
} from "@/components/graph";
import React from "react";
import { useAquapath1 } from "./useAquapath";
import { TimeSeries } from "@/lib/timeseries";
export function Aquapath1GraphPage() {
const {
state,
front_flow,
back_flow,
front_temperature,
back_temperature,
front_temp_reservoir,
back_temp_reservoir,
} = useAquapath1();
const syncHook = useGraphSync("aquapath-group");
const front_temp_target =
state?.temperature_states?.front.target_temperature ?? 0;
const back_temp_target =
state?.temperature_states?.back.target_temperature ?? 0;
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<FlowGraph
syncHook={syncHook}
flow={front_flow}
name={"Front Flow"}
id={"front_flow"}
/>
<FlowGraph
syncHook={syncHook}
flow={back_flow}
name={"Back Flow"}
id={"back_flow"}
/>
<TemperatureGraph
syncHook={syncHook}
temp_in={front_temperature}
temp_out={front_temp_reservoir}
targetTemp={front_temp_target}
name={"Front Temperature"}
id={"front_temp"}
/>
<TemperatureGraph
syncHook={syncHook}
temp_in={back_temperature}
temp_out={back_temp_reservoir}
targetTemp={back_temp_target}
name={"Back Temperature"}
id={"back_temp"}
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
export function FlowGraph({
syncHook,
flow,
name,
id,
}: {
syncHook: ReturnType<typeof useGraphSync>;
flow: TimeSeries | null;
name: string;
id: string;
}) {
const config: GraphConfig = {
title: name,
icon: "lu:RotateCcw",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
background: "#ffffff",
},
exportFilename: "flow_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: flow,
color: "#3b82f6",
}}
unit="l/min"
renderValue={(value) => value.toFixed(1)}
config={config}
graphId={id}
/>
);
}
export function TemperatureGraph({
syncHook,
temp_in,
temp_out,
targetTemp,
name,
id,
}: {
syncHook: ReturnType<typeof useGraphSync>;
temp_in: TimeSeries | null;
temp_out: TimeSeries | null;
targetTemp: number;
name: string;
id: string;
}) {
const config: GraphConfig = {
title: name,
icon: "lu:RotateCcw",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
background: "#ffffff",
},
exportFilename: "temperature_data",
showLegend: true,
lines: [],
};
const combinedData = [
...(temp_in
? [
{
newData: temp_in,
title: "Temperature In",
color: "#3b82f6",
lines: [
{
type: "threshold" as const,
value: targetTemp,
label: "Target Temperature",
color: "#3b82f6",
show: true,
width: 2,
dash: [5, 5],
},
],
},
]
: []),
...(temp_out
? [
{
newData: temp_out,
title: "Temperature Out",
color: "#f87171",
lines: [
{
type: "threshold" as const,
value: targetTemp,
label: "Target Temperature",
color: "#f87171",
show: true,
width: 2,
dash: [5, 5],
},
],
},
]
: []),
];
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={combinedData}
unit="C"
renderValue={(value) => value.toFixed(1)}
config={config}
graphId={id}
/>
);
}
@@ -0,0 +1,26 @@
import { Topbar } from "@/components/Topbar";
import { aquapath1SerialRoute } from "@/routes/routes";
import React from "react";
export function Aquapath1Page() {
const { serial } = aquapath1SerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/aquapath1/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
{
link: "graph",
title: "Graph",
icon: "lu:ChartSpline",
activeLink: "graphs",
},
]}
/>
);
}
@@ -0,0 +1,246 @@
/**
* @file Aquapath1Namespace.ts
* @description TypeScript implementation of Aquapath1 namespace with Zod schema validation.
*/
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Mode enum for Aquapath Machine
*/
export const modeSchema = z.enum(["Standby", "Auto"]);
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
});
export const tempStateSchema = z.object({
temperature: z.number(),
target_temperature: z.number(),
});
/**
* Cooling states schema
*/
export const tempStatesSchema = z.object({
front: tempStateSchema,
back: tempStateSchema,
});
export const flowStateSchema = z.object({
flow: z.number(),
should_flow: z.boolean(),
});
export const flowStatesSchema = z.object({
front: flowStateSchema,
back: flowStateSchema,
});
/**
* Live values event schema (time-series data)
*/
export const liveValuesEventDataSchema = z.object({
front_flow: z.number(),
back_flow: z.number(),
front_temperature: z.number(),
back_temperature: z.number(),
front_temp_reservoir: z.number(),
back_temp_reservoir: z.number(),
});
/**
* State event schema (consolidated state)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
mode_state: modeStateSchema,
flow_states: flowStatesSchema,
temperature_states: tempStatesSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type Mode = z.infer<typeof modeSchema>;
export type ModeState = z.infer<typeof modeStateSchema>;
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
export type StateEvent = z.infer<typeof stateEventSchema>;
export type Aquapath1NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
front_flow: TimeSeries;
back_flow: TimeSeries;
front_temperature: TimeSeries;
back_temperature: TimeSeries;
front_temp_reservoir: TimeSeries;
back_temp_reservoir: TimeSeries;
};
// Constants for time durations
const TWENTY_MILLISECOND = 20;
const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: back_temperature, insert: addTemperature2 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: front_temp_reservoir, insert: addTempReserv1 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
/**
* Factory function to create a new Aquapath namespace store
* @returns A new Zustand store instance for Aquapath namespace
*/
export const createAquapath1NamespaceStore =
(): StoreApi<Aquapath1NamespaceStore> => {
return create<Aquapath1NamespaceStore>(() => {
return {
state: null,
defaultState: null,
front_temperature: front_temperature,
back_temperature: back_temperature,
front_flow: front_flow,
back_flow: back_flow,
front_temp_reservoir: front_temp_reservoir,
back_temp_reservoir: back_temp_reservoir,
};
});
};
/**
* Creates a message handler for Mock1 namespace events with validation and appropriate caching strategies
* @param store The store to update when messages are received
* @param throttledUpdater Throttled updater for batching updates at 60 FPS
* @returns A message handler function
*/
export function aquapath1MessageHandler(
store: StoreApi<Aquapath1NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Aquapath1NamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: Aquapath1NamespaceStore) => Aquapath1NamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
// State events (latest only)
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
}
// Live values events (time-series data)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
updateStore((state) => ({
...state,
front_temperature: addTemperature1(state.front_temperature, {
value: liveValuesEvent.data.front_temperature,
timestamp: event.ts,
}),
back_temperature: addTemperature2(state.back_temperature, {
value: liveValuesEvent.data.back_temperature,
timestamp: event.ts,
}),
front_flow: addFlow1(state.front_flow, {
value: liveValuesEvent.data.front_flow,
timestamp: event.ts,
}),
back_flow: addFlow2(state.back_flow, {
value: liveValuesEvent.data.back_flow,
timestamp: event.ts,
}),
front_temp_reservoir: addTempReserv1(state.front_temp_reservoir, {
value: liveValuesEvent.data.front_temp_reservoir,
timestamp: event.ts,
}),
back_temp_reservoir: addTempReserv2(state.back_temp_reservoir, {
value: liveValuesEvent.data.back_temp_reservoir,
timestamp: event.ts,
}),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(`Unexpected error processing ${eventName} event:`, error);
throw error;
}
};
}
/**
* Create the Aquapath1 namespace implementation
*/
const useAquapath1NamespaceImplementation =
createNamespaceHookImplementation<Aquapath1NamespaceStore>({
createStore: createAquapath1NamespaceStore,
createEventHandler: aquapath1MessageHandler,
});
export function useAquapath1Namespace(
machine_identification_unique: MachineIdentificationUnique,
): Aquapath1NamespaceStore {
// Generate namespace ID from validated machine ID
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
// Use the implementation with validated namespace ID
return useAquapath1NamespaceImplementation(namespaceId);
}
@@ -0,0 +1,175 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { MachineIdentificationUnique } from "@/machines/types";
import { aquapath1 } from "@/machines/properties";
import { aquapath1SerialRoute } from "@/routes/routes";
import { Mode, StateEvent, useAquapath1Namespace } from "./aquapath1Namespace";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
import { z } from "zod";
export function useAquapath1() {
const { serial: serialString } = aquapath1SerialRoute.useParams();
// Memoize the machine identification to keep it stable between renders
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: {
vendor: 0,
machine: 0,
},
serial: 0,
};
}
return {
machine_identification: aquapath1.machine_identification,
serial,
};
}, [serialString]);
// Get consolidated state and live values from namespace
const {
state,
defaultState,
front_temperature: front_temperature,
back_temperature: back_temperature,
front_flow: front_flow,
back_flow: back_flow,
front_temp_reservoir: front_temp_reservoir,
back_temp_reservoir: back_temp_reservoir,
} = useAquapath1Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
const setAquapathMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
},
() =>
requestAquapathMode({
machine_identification_unique: machineIdentification,
data: { SetAquaPathMode: mode },
}),
);
};
const setFrontTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.temperature_states.front.target_temperature = temperature;
},
() =>
requestFrontTemperature({
machine_identification_unique: machineIdentification,
data: { SetFrontTemperature: temperature },
}),
);
};
const setBackTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.temperature_states.back.target_temperature = temperature;
},
() =>
requestBackTemperature({
machine_identification_unique: machineIdentification,
data: { SetBackTemperature: temperature },
}),
);
};
const setFrontFlow = (flow: boolean) => {
updateStateOptimistically(
(current) => {
current.data.flow_states.front.should_flow = flow;
},
() =>
requestFrontFlow({
machine_identification_unique: machineIdentification,
data: { SetFrontFlow: flow },
}),
);
};
const setBackFlow = (flow: boolean) => {
updateStateOptimistically(
(current) => {
current.data.flow_states.back.should_flow = flow;
},
() =>
requestBackFlow({
machine_identification_unique: machineIdentification,
data: { SetBackFlow: flow },
}),
);
};
// Mutation hooks
const { request: requestAquapathMode } = useMachineMutation(
z.object({ SetAquaPathMode: z.enum(["Standby", "Auto"]) }),
);
const { request: requestFrontTemperature } = useMachineMutation(
z.object({ SetFrontTemperature: z.number() }),
);
const { request: requestBackTemperature } = useMachineMutation(
z.object({ SetBackTemperature: z.number() }),
);
const { request: requestFrontFlow } = useMachineMutation(
z.object({ SetFrontFlow: z.boolean() }),
);
const { request: requestBackFlow } = useMachineMutation(
z.object({ SetBackFlow: z.boolean() }),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
};
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
front_flow,
back_flow,
front_temperature,
back_temperature,
front_temp_reservoir,
back_temp_reservoir,
setAquapathMode,
setFrontTemperature,
setBackTemperature,
setFrontFlow,
setBackFlow,
};
}
@@ -50,12 +50,11 @@ export function useBuffer1() {
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -63,7 +62,7 @@ export function useBuffer1() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -30,6 +30,8 @@ export function Extruder2ControlPage() {
motorScrewRpm,
motorPower,
motorCurrent,
totalEnergyKWh,
combinedPower,
setExtruderMode,
@@ -140,7 +142,6 @@ export function Extruder2ControlPage() {
onChange={setInverterRegulation}
disabled={isDisabled}
loading={isLoading}
reverse
/>
</Label>
<div className="flex flex-row flex-wrap gap-4">
@@ -225,13 +226,31 @@ export function Extruder2ControlPage() {
/>
</ControlCard>
<ControlCard className="bg-blue" title="Total Power Consumption">
<ControlCard className="bg-blue" title="Power Consumption">
<TimeSeriesValueNumeric
label=""
label="Total Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 1)}
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={combinedPower}
/>
<TimeSeriesValueNumeric
label="Motor Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorPower}
/>
<TimeSeriesValueNumeric
label="Motor Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={motorCurrent}
/>
<TimeSeriesValueNumeric
label="Total Energy Consumption"
unit="kWh"
renderValue={(value) => roundToDecimals(value, 3)}
timeseries={totalEnergyKWh}
/>
</ControlCard>
</ControlGrid>
</Page>
@@ -43,6 +43,8 @@ export const liveValuesEventDataSchema = z.object({
front_power: z.number(),
back_power: z.number(),
middle_power: z.number(),
combined_power: z.number(),
total_energy_kwh: z.number(),
});
/**
@@ -195,8 +197,9 @@ export type Extruder2NamespaceStore = {
middlePower: TimeSeries;
backPower: TimeSeries;
// Combined power consumption
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
};
// Constants for time durations
@@ -205,13 +208,6 @@ const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: screwRpm, insert: addScrewRpm } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
@@ -250,6 +246,9 @@ const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
@@ -346,12 +345,11 @@ export function extruder2MessageHandler(
timestamp,
}),
combinedPower: addCombinedPower(state.combinedPower, {
value:
liveValuesEvent.data.motor_status.power +
liveValuesEvent.data.nozzle_power +
liveValuesEvent.data.front_power +
liveValuesEvent.data.middle_power +
liveValuesEvent.data.back_power,
value: liveValuesEvent.data.combined_power,
timestamp,
}),
totalEnergyKWh: addTotalEnergyKWh(state.totalEnergyKWh, {
value: liveValuesEvent.data.total_energy_kwh,
timestamp,
}),
}));
@@ -387,6 +385,7 @@ export const createExtruder2NamespaceStore =
backPower,
middlePower,
combinedPower,
totalEnergyKWh,
};
});
@@ -56,17 +56,17 @@ export function useExtruder2() {
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -74,7 +74,7 @@ export function useExtruder2() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -383,6 +383,7 @@ export function useExtruder2() {
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
// Loading states
isLoading: stateOptimistic.isOptimistic,
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more