Compare commits

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

No files matched your search

+43 -5
View File
@@ -1,57 +1,94 @@
# `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/V2
- [#731](https://github.com/qitechgmbh/control/pull/731) Added LaserV2 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 +104,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
+2 -105
View File
@@ -596,6 +596,7 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"tokio",
"tracing",
"uom",
]
@@ -1019,6 +1020,7 @@ dependencies = [
"futures",
"rand",
"smol",
"tracing",
"uom",
]
@@ -1677,15 +1679,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 +1690,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"
@@ -2000,17 +1983,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 +2092,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"
@@ -2698,14 +2664,12 @@ dependencies = [
"bitvec",
"control_core",
"control_core_derive",
"crc",
"dhat",
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
@@ -2716,7 +2680,6 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"thread-priority",
"tikv-jemalloc-ctl",
"tikv-jemallocator",
"tokio",
@@ -3031,20 +2994,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"
@@ -3658,28 +3607,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 +3620,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 +3648,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 +3749,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"
+1 -1
View File
@@ -24,7 +24,7 @@ if [ "$1" != "mock-machine" ]; then
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
echo "Setting permissions for serial ports"
sudo chown "root:$USER" /dev/ttyUSB*
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
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
#
+29
View File
@@ -29,3 +29,32 @@ 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 {}
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)
}
+12
View File
@@ -8,7 +8,9 @@ workspace = true
[dependencies]
anyhow = "1.0.99"
axum = "0.8.4"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = "0.6"
interfaces = "0.0.9"
@@ -25,6 +27,7 @@ tracing = "0.1.41"
erased-serde = "0.4.6"
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]
@@ -33,3 +36,12 @@ libc = "0.2.175"
[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"]
@@ -19,6 +19,7 @@ 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,
};
@@ -239,23 +240,25 @@ pub fn get_identification_addresses(
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(),
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
EL4002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7041_0052_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!(
@@ -144,7 +144,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(5),
},
));
+2 -25
View File
@@ -329,32 +329,9 @@ impl<H: Hasher> Serializer for HashSerializer<'_, 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);
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"
);
}
+6 -24
View File
@@ -45,22 +45,6 @@ impl DivideByCount for u64 {
}
}
/// 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();
+6 -6
View File
@@ -1,11 +1,10 @@
#[cfg(unix)]
#[cfg(target_os = "linux")]
use libc;
use std::ffi::CString;
use std::io;
use std::str;
#[cfg(unix)]
#[cfg(target_os = "linux")]
fn read_proc_interrupts() -> Result<String, io::Error> {
let path = CString::new("/proc/interrupts").unwrap();
// O_RDONLY
@@ -33,7 +32,7 @@ fn read_proc_interrupts() -> Result<String, io::Error> {
Ok(String::from_utf8_lossy(&buf).into_owned())
}
#[cfg(unix)]
#[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) {
@@ -53,7 +52,7 @@ fn get_interface_irq(proc_content: &str, interface_name: &str) -> Option<u32> {
/// With comma seperated values
/// This function takes the irq identifier and writes the cpu string
/// into /proc/irq/irq_number/smp_affinity_list
#[cfg(unix)]
#[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();
@@ -61,6 +60,7 @@ fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
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),
@@ -97,7 +97,7 @@ fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
/// 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(unix)]
#[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);
+3
View File
@@ -12,5 +12,8 @@ pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
+17
View File
@@ -5,4 +5,21 @@ 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;
/// 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
}
}
+4 -1
View File
@@ -16,9 +16,12 @@ 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 + AnyGetters + Debug + Send {}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
+1 -1
View File
@@ -30,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)?)))
+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(())
}
+16
View File
@@ -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>,
}
+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`
+5 -4
View File
@@ -95,7 +95,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"
}
},
@@ -14552,9 +14552,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",
+1 -1
View File
@@ -60,7 +60,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"
},
"dependencies": {
+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 = "";
@@ -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.isOptimistic) {
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,
@@ -224,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.isOptimistic) {
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,
@@ -2,11 +2,13 @@ import React from "react";
import { TimeSeries } from "@/lib/timeseries";
import { cva } from "class-variance-authority";
type AnimateCableProps = {
type DiameterVisualisationProps = {
targetDiameter: number;
lowTolerance: number;
highTolerance: number;
diameter: TimeSeries;
x_diameter?: TimeSeries;
y_diameter?: TimeSeries;
};
// Ring color depending on tolerance
@@ -24,7 +26,7 @@ const dashedRingClass = cva(
"absolute rounded-full border border-dashed border-black z-15",
);
// Dynamic animated circle
// Dynamic animated circle/ellipse
const dynamicCircleClass = cva(
"absolute rounded-full border-2 border-black bg-transparent z-30 transition-all duration-300 ease-in-out",
);
@@ -34,16 +36,19 @@ export function DiameterVisualisation({
lowTolerance,
highTolerance,
diameter,
}: AnimateCableProps) {
x_diameter,
y_diameter,
}: DiameterVisualisationProps) {
const actualDiameter = diameter.current?.value ?? 0.0;
const actualX = x_diameter?.current?.value ?? actualDiameter;
const actualY = y_diameter?.current?.value ?? actualDiameter;
const minDia = targetDiameter - lowTolerance;
const maxDia = targetDiameter + highTolerance;
const totalTolerance = lowTolerance + highTolerance;
const midDia = minDia + (maxDia - minDia) * (lowTolerance / totalTolerance);
const pixelMin = 100;
const pixelMax = 200;
const lerp = (
value: number,
inputMin: number,
@@ -51,86 +56,72 @@ export function DiameterVisualisation({
outputMin: number,
outputMax: number,
) => {
if (inputMax === inputMin) {
return (outputMin + outputMax) / 2;
}
return (
outputMin +
((value - inputMin) * (outputMax - outputMin)) / (inputMax - inputMin)
);
};
const pixelMid = lerp(midDia, minDia, maxDia, pixelMin, pixelMax);
let dynamicDiameterPx: number | null = null;
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
if (actualDiameter > 0) {
if (actualDiameter < minDia) {
dynamicDiameterPx = lerp(actualDiameter, 0, minDia, 0, pixelMin);
} else if (actualDiameter <= maxDia) {
dynamicDiameterPx = lerp(
actualDiameter,
minDia,
maxDia,
pixelMin,
pixelMax,
);
} else {
const dynamicDiameterPxLerp = lerp(
actualDiameter,
maxDia,
maxDia * 1.5,
pixelMax,
pixelMax * 1.1,
);
dynamicDiameterPx = clamp(
dynamicDiameterPxLerp,
pixelMin,
pixelMax * 1.1,
);
}
}
const radiusX = clamp(
lerp(actualX, minDia, maxDia, pixelMin / 2, pixelMax / 2),
pixelMin / 2,
pixelMax / 2,
);
const radiusY = clamp(
lerp(actualY, minDia, maxDia, pixelMin / 2, pixelMax / 2),
pixelMin / 2,
pixelMax / 2,
);
const bigRadius = pixelMax / 2;
const smallRadius = pixelMin / 2;
const midRadius = pixelMid / 2;
const dynamicRadius = dynamicDiameterPx ? dynamicDiameterPx / 2 : 0;
const inTolerance = actualDiameter >= minDia && actualDiameter <= maxDia;
const centerStyle = (radius: number) => ({
width: radius * 2,
height: radius * 2,
top: `${bigRadius - radius}px`,
left: `${bigRadius - radius}px`,
// Center the shape
const centerStyle = (rx: number, ry: number) => ({
width: rx * 2,
height: ry * 2,
top: `${bigRadius - ry}px`,
left: `${bigRadius - rx}px`,
});
// Check if current measurement is in tolerance
const inTolerance =
actualX >= minDia &&
actualX <= maxDia &&
actualY >= minDia &&
actualY <= maxDia;
return (
<div
className="relative mx-auto"
style={{ width: pixelMax, height: pixelMax }}
>
{/* Outer ring background */}
<div
className={`${ringClass({
state: inTolerance ? "inRange" : "outOfRange",
})} top-0 left-0 z-10`}
className={`${ringClass({ state: inTolerance ? "inRange" : "outOfRange" })} top-0 left-0 z-10`}
style={{ width: pixelMax, height: pixelMax }}
/>
{/* Inner white circle to cut out the center */}
<div
className="absolute z-20 rounded-full bg-white"
style={centerStyle(smallRadius)}
style={centerStyle(pixelMin / 2, pixelMin / 2)}
/>
<div
className={dashedRingClass()}
style={centerStyle(pixelMax / 2, pixelMax / 2)}
/>
<div
className={dynamicCircleClass()}
style={{
...centerStyle(radiusX, radiusY),
/* rotate by 45deg to mimic the rotated axis of the Laser */
transform: "rotate(45deg)",
}}
/>
{/* Dashed ring in the middle */}
<div className={dashedRingClass()} style={centerStyle(midRadius)} />
{/* Dynamic circle */}
{dynamicDiameterPx !== null && (
<div
className={dynamicCircleClass()}
style={centerStyle(dynamicRadius)}
/>
)}
</div>
);
}
@@ -14,6 +14,9 @@ import { DiameterVisualisation } from "../DiameterVisualisation";
export function Laser1ControlPage() {
const {
diameter,
x_diameter,
y_diameter,
roundness,
state,
defaultState,
setTargetDiameter,
@@ -34,15 +37,46 @@ export function Laser1ControlPage() {
lowTolerance={lowerTolerance}
highTolerance={higherTolerance}
diameter={diameter}
x_diameter={x_diameter}
y_diameter={y_diameter}
/>
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Current Diameter"
label="Diameter"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
{x_diameter?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="X-Diameter"
unit="mm"
timeseries={x_diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
{y_diameter?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Y-Diameter"
unit="mm"
timeseries={y_diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
{roundness?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
</ControlCard>
<ControlCard title="Settings">
<Label label="Set Target Diameter">
@@ -10,63 +10,130 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, state } = useLaser1();
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const syncHook = useGraphSync("diameter-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
const diameterColor = "#3b82f6";
const xDiameterColor = "#ef4444";
const yDiameterColor = "#22c55e";
const roundnessColor = "#eab308";
const config: GraphConfig = {
title: "Diameter",
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
exportFilename: "diameter_data",
colors: {
primary: "#3b82f6",
primary: diameterColor,
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: diameter,
color: "#3b82f6",
lines: [
if (!isTwoAxis) {
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: diameter,
color: diameterColor,
lines: [
{
type: "threshold",
value: targetDiameter + higherTolerance,
label: "Upper Threshold",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold",
value: targetDiameter - lowerTolerance,
label: "Lower Threshold",
color: diameterColor,
dash: [5, 5],
},
{
type: "target",
value: targetDiameter,
label: "Target",
color: diameterColor,
},
],
}}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={config}
graphId="diameter-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
} else {
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={[
{
type: "threshold",
value: targetDiameter + higherTolerance,
label: "Upper Threshold",
color: "#3b82f6",
dash: [5, 5],
newData: diameter,
color: diameterColor,
title: "Diameter",
lines: [
{
type: "threshold",
value: targetDiameter + higherTolerance,
label: "Upper Threshold",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold",
value: targetDiameter - lowerTolerance,
label: "Lower Threshold",
color: diameterColor,
dash: [5, 5],
},
{
type: "target",
value: targetDiameter,
label: "Target",
color: diameterColor,
},
],
},
{
type: "threshold",
value: targetDiameter - lowerTolerance,
label: "Lower Threshold",
color: "#3b82f6",
dash: [5, 5],
newData: x_diameter,
color: xDiameterColor,
title: "X-Diameter",
},
{
type: "target",
value: targetDiameter,
label: "Target",
color: "#3b82f6",
newData: y_diameter,
color: yDiameterColor,
title: "Y-Diameter",
},
],
}}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={config}
graphId="diameter-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
{
newData: roundness,
color: roundnessColor,
title: "Roundness",
},
]}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={config}
graphId="diameter-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
}
@@ -28,6 +28,9 @@ import {
*/
export const liveValuesEventDataSchema = z.object({
diameter: z.number(),
x_diameter: z.number().nullable(),
y_diameter: z.number().nullable(),
roundness: z.number().nullable(),
});
/**
@@ -57,6 +60,9 @@ export type Laser1NamespaceStore = {
// Time series data for live values
diameter: TimeSeries;
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
};
// Constants for time durations
@@ -70,6 +76,20 @@ const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries(
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
/**
* Factory function to create a new Laser1 namespace store
* @returns A new Zustand store instance for Laser1 namespace
@@ -80,6 +100,9 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
state: null,
defaultState: null,
diameter: diameter,
x_diameter,
y_diameter,
roundness: roundness,
};
});
@@ -119,14 +142,47 @@ export function laser1MessageHandler(
// Live values events (keep for 1 hour)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timeseriesValue: TimeSeriesValue = {
const diameterValue: TimeSeriesValue = {
value: liveValuesEvent.data.diameter,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
diameter: addDiameter(state.diameter, timeseriesValue),
diameter: addDiameter(state.diameter, diameterValue),
}));
if (liveValuesEvent.data.x_diameter !== null) {
const xValue: TimeSeriesValue = {
value: liveValuesEvent.data.x_diameter,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
x_diameter: addXDiameter(state.x_diameter, xValue),
}));
}
if (liveValuesEvent.data.y_diameter !== null) {
const yValue: TimeSeriesValue = {
value: liveValuesEvent.data.y_diameter,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
y_diameter: addYDiameter(state.y_diameter, yValue),
}));
}
if (liveValuesEvent.data.roundness !== null) {
const roundnessValue: TimeSeriesValue = {
value: liveValuesEvent.data.roundness,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
roundness: addRoundness(state.roundness, roundnessValue),
}));
}
} else {
handleUnhandledEventError(eventName);
}
@@ -11,19 +11,17 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter } = useLaser1Namespace(
machine_identification_unique,
);
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -31,7 +29,7 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -112,6 +110,9 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Live values (TimeSeries)
diameter,
x_diameter,
y_diameter,
roundness,
// Loading states
isLoading: stateOptimistic.isOptimistic,
+3 -4
View File
@@ -17,12 +17,11 @@ function useMock(machine_identification_unique: MachineIdentificationUnique) {
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -30,7 +29,7 @@ function useMock(machine_identification_unique: MachineIdentificationUnique) {
serverRequest: () => Promise<any>,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest()
+70
View File
@@ -271,12 +271,82 @@ export const buffer1: MachineProperties = {
},
],
};
export const aquapath1: MachineProperties = {
name: "Aquapath",
version: "V1",
slug: "aquapath1",
icon: "lu:Waves",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0009,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "EL2008",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x110000,
},
],
},
{
role: 2,
role_label: "EL4002",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xfa23052,
revision: 0x140000,
},
],
},
{
role: 3,
role_label: "EL3204",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x160000,
},
],
},
{
role: 4,
role_label: "EL5152",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x14203052,
revision: 0x140000,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder2,
laser1,
mock1,
buffer1,
aquapath1,
];
export const getMachineProperties = (
@@ -36,7 +36,6 @@ export function Winder2ControlPage() {
tensionArmAngle,
zeroTensionArmAngle,
spoolRpm,
spoolDiameter,
setMode,
pullerSpeed,
spoolProgress,
@@ -80,12 +79,6 @@ export function Winder2ControlPage() {
timeseries={spoolRpm}
renderValue={(value) => roundToDecimals(value, 0)}
/>
<TimeSeriesValueNumeric
label="Estimated Diameter"
unit="cm"
timeseries={spoolDiameter}
renderValue={(value) => roundToDecimals(value, 1)}
/>
</ControlCard>
<ControlCard className="bg-red" height={2} title="Traverse">
@@ -119,15 +112,14 @@ export function Winder2ControlPage() {
maxLabel="OUT"
max={180}
renderValue={(value) => roundToDecimals(value, 0)}
inverted
onChange={setTraverseLimitOuter}
/>
<TouchButton
variant="outline"
icon="lu:ArrowLeftToLine"
onClick={gotoTraverseLimitOuter}
disabled={isDisabled || !state?.traverse_state?.can_go_out}
isLoading={isLoading || state?.traverse_state?.is_going_out}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Outer Limit
</TouchButton>
@@ -156,8 +148,8 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:ArrowRightToLine"
onClick={gotoTraverseLimitInner}
disabled={isDisabled || !state?.traverse_state?.can_go_in}
isLoading={isLoading || state?.traverse_state?.is_going_in}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Inner Limit
</TouchButton>
@@ -178,8 +170,8 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:House"
onClick={() => gotoTraverseHome()}
disabled={isDisabled || !state?.traverse_state?.can_go_home}
isLoading={isLoading || state?.traverse_state?.is_going_home}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Home
</TouchButton>
@@ -16,7 +16,6 @@ export function Winder2GraphsPage() {
const {
state,
spoolRpm,
spoolDiameter,
traversePosition,
tensionArmAngle,
pullerSpeed,
@@ -60,13 +59,6 @@ export function Winder2GraphsPage() {
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolDiameterGraph
syncHook={syncHook}
newData={spoolDiameter}
unit="cm"
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolProgressGraph
syncHook={syncHook}
newData={spoolProgress}
@@ -296,37 +288,3 @@ export function PullerSpeedGraph({
/>
);
}
export function SpoolDiameterGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Estimated Spool Diameter",
icon: "lu:Circle",
colors: {
primary: "#ec4899",
grid: "#e2e8f0",
background: "#ffffff",
},
exportFilename: "spool_diameter_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{ newData }}
unit={unit}
renderValue={renderValue}
config={config}
graphId="spool-diameter"
/>
);
}
@@ -61,7 +61,6 @@ export function useWinder2() {
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
} = useWinder2Namespace(machineIdentification);
@@ -71,10 +70,10 @@ export function useWinder2() {
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Request functions for all operations
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
@@ -178,7 +177,7 @@ export function useWinder2() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -597,7 +596,6 @@ export function useWinder2() {
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
@@ -22,7 +22,6 @@ import {
TimeSeries,
TimeSeriesValue,
} from "@/lib/timeseries";
import { connected } from "process";
// ========== Event Schema Definitions ==========
@@ -33,7 +32,6 @@ export const liveValuesEventDataSchema = z.object({
traverse_position: z.number().nullable(),
puller_speed: z.number(),
spool_rpm: z.number(),
spool_diameter: z.number(),
tension_arm_angle: z.number(),
spool_progress: z.number(),
});
@@ -190,7 +188,6 @@ export type Winder2NamespaceStore = {
traversePosition: TimeSeries;
pullerSpeed: TimeSeries;
spoolRpm: TimeSeries;
spoolDiameter: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
};
@@ -213,8 +210,6 @@ const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: spoolDiameter, insert: addSpoolDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
@@ -234,7 +229,6 @@ export const createWinder2NamespaceStore =
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
};
@@ -265,6 +259,7 @@ export function winder2MessageHandler(
try {
if (eventName === "StateEvent") {
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
@@ -285,7 +280,6 @@ export function winder2MessageHandler(
traverse_position,
puller_speed,
spool_rpm,
spool_diameter,
tension_arm_angle,
spool_progress,
} = liveValuesEvent.data;
@@ -333,16 +327,6 @@ export function winder2MessageHandler(
};
newState.spoolRpm = addSpoolRpm(state.spoolRpm, spoolRpmValue);
// Add spool diameter
const spoolDiameterValue: TimeSeriesValue = {
value: spool_diameter,
timestamp,
};
newState.spoolDiameter = addSpoolDiameter(
state.spoolDiameter,
spoolDiameterValue,
);
// Add tension arm angle
const tensionArmAngleValue: TimeSeriesValue = {
value: tension_arm_angle,
+27
View File
@@ -35,6 +35,10 @@ import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { z } from "zod";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// make a route tree like this
@@ -194,6 +198,24 @@ export const mock1PresetsRoute = createRoute({
component: () => <Mock1PresetsPage />,
});
export const aquapath1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "aquapath1/$serial",
component: () => <Aquapath1Page />,
});
export const aquapath1GraphRoute = createRoute({
getParentRoute: () => aquapath1SerialRoute,
path: "graph",
component: () => <Aquapath1GraphPage />,
});
export const aquapath1ControlRoute = createRoute({
getParentRoute: () => aquapath1SerialRoute,
path: "control",
component: () => <Aquapath1ControlPage />,
});
export const buffer1SettingsRoute = createRoute({
getParentRoute: () => buffer1SerialRoute,
path: "settings",
@@ -292,6 +314,11 @@ export const rootTree = RootRoute.addChildren([
laser1PresetsRoute,
]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
]),
winder2SerialRoute.addChildren([
winder2ControlRoute,
winder2ManualRoute,
+4
View File
@@ -15,3 +15,7 @@ ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
futures = "0.3.31"
anyhow = "1.0.99"
[package.metadata.cargo-machete]
ignored = ["interfaces"]
Binary file not shown.
+1
View File
@@ -15,6 +15,7 @@ smol = "2.0.2"
uom = "0.36.0"
rand = "0.9.2"
futures = "0.3.31"
tracing = "0.1.41"
[dev-dependencies]
approx = "0.5.1"
+85 -8
View File
@@ -14,7 +14,7 @@ use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use ethercat_hal_derive::EthercatDevice;
use uom::si::{electric_potential::volt, f64::ElectricPotential};
#[derive(Debug, Clone)]
@@ -98,7 +98,9 @@ impl AnalogInputDevice<EL3062_0030Port> for EL3062_0030 {
EL30XXPresentation::Signed => raw_value.as_signed(),
EL30XXPresentation::SignedMagnitude => raw_value.as_signed_magnitude(),
};
let normalized = f32::from(value) / f32::from(i16::MAX);
AnalogInputInput {
normalized,
wiring_error: false,
@@ -138,22 +140,97 @@ pub enum EL3062_0030Port {
AI2,
}
#[derive(Debug, Clone, TxPdo)]
#[derive(Debug, Clone)]
pub struct EL3062_0030TxPdo {
#[pdo_object_index(0x1A00)]
pub ai_standard_channel1: Option<AiStandard>,
#[pdo_object_index(0x1A01)]
pub ai_compact_channel1: Option<AiCompact>,
#[pdo_object_index(0x1A02)]
pub ai_standard_channel2: Option<AiStandard>,
#[pdo_object_index(0x1A03)]
pub ai_compact_channel2: Option<AiCompact>,
}
#[derive(Debug, Clone, RxPdo)]
pub struct EL3062_0030RxPdo {}
impl crate::coe::Configuration for EL3062_0030TxPdo {
///Implemented by the ethercat_hal_derive::TxPdo derive macro
async fn write_config<'a>(
&self,
device: &EthercrabSubDevicePreoperational<'a>,
) -> Result<(), anyhow::Error> {
device.sdo_write(0x1C13, 0, 0u8).await?;
let mut len = 0;
if let Some(_) = &self.ai_standard_channel1 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A00u16).await?;
}
if let Some(_) = &self.ai_compact_channel1 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A01u16).await?;
}
if let Some(_) = &self.ai_standard_channel2 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A02u16).await?;
}
if let Some(_) = &self.ai_compact_channel2 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A03u16).await?;
}
device.sdo_write(0x1C13, 0, len).await?;
Ok(())
}
}
impl crate::pdo::TxPdo for EL3062_0030TxPdo {
///Implemented by the ethercat_hal_derive::TxPdo derive macro
fn get_objects(&self) -> Box<[Option<&dyn crate::pdo::TxPdoObject>]> {
Box::new([
self.ai_standard_channel1
.as_ref()
.map(|o| o as &dyn crate::pdo::TxPdoObject),
self.ai_compact_channel1
.as_ref()
.map(|o| o as &dyn crate::pdo::TxPdoObject),
self.ai_standard_channel2
.as_ref()
.map(|o| o as &dyn crate::pdo::TxPdoObject),
self.ai_compact_channel2
.as_ref()
.map(|o| o as &dyn crate::pdo::TxPdoObject),
])
}
///Implemented by the ethercat_hal_derive::TxPdo derive macro
fn get_objects_mut(&mut self) -> Box<[Option<&mut dyn crate::pdo::TxPdoObject>]> {
Box::new([
self.ai_standard_channel1
.as_mut()
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
self.ai_compact_channel1
.as_mut()
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
self.ai_standard_channel2
.as_mut()
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
self.ai_compact_channel2
.as_mut()
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
])
}
}
#[derive(Debug, Clone)]
pub struct EL3062_0030RxPdo {}
impl crate::coe::Configuration for EL3062_0030RxPdo {
///Implemented by the ethercat_hal_derive::RxPdo derive macro
async fn write_config<'a>(
&self,
_device: &EthercrabSubDevicePreoperational<'a>,
) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl crate::pdo::RxPdo for EL3062_0030RxPdo {
///Implemented by the ethercat_hal_derive::RxPdo derive macro
fn get_objects(&self) -> Box<[Option<&dyn crate::pdo::RxPdoObject>]> {
Box::new([])
}
}
impl Configuration for EL3062_0030Configuration {
async fn write_config<'a>(
&self,
+1
View File
@@ -102,6 +102,7 @@ impl EL4002 {
&mut self,
subdevice: &EthercrabSubDevicePreoperational<'a>,
) -> Result<(), anyhow::Error> {
tracing::info!("el4002");
self.configuration
.channel1
.write_channel_config(subdevice, 0x8000)
+349
View File
@@ -0,0 +1,349 @@
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::coe::{ConfigurableDevice, Configuration};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::encoder_input::{
EncoderInputCounter, EncoderInputDevice, EncoderInputFrequency, EncoderInputPeriod,
};
use crate::pdo::PredefinedPdoAssignment;
use crate::pdo::RxPdo;
use crate::pdo::TxPdo;
use crate::pdo::el5152::{
El5152EncoderControl, El5152EncoderFrequency, El5152EncoderPeriod, El5152EncoderStatus,
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
/// EL5152 2-channel incremental encoder interface
///
/// 24V HTL, 100 kHz, dual channel
#[derive(EthercatDevice)]
pub struct EL5152 {
pub configuration: EL5152Configuration,
pub rxpdo: EL5152RxPdo,
pub txpdo: EL5152TxPdo,
is_used: bool,
}
impl EthercatDeviceProcessing for EL5152 {}
#[derive(Debug, Clone)]
pub struct EL5152Configuration {
pub pdo_assignment: EL5152PredefinedPdoAssignment,
pub channel1: EL5152ChannelConfiguration,
pub channel2: EL5152ChannelConfiguration,
}
#[derive(Debug, Clone)]
pub enum EL5152PredefinedPdoAssignment {
Period,
Frequency,
}
impl std::fmt::Debug for EL5152 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EL5152")
}
}
#[derive(Debug, Clone, Copy)]
pub enum EL5152Port {
ENC1,
ENC2,
}
impl Default for EL5152Configuration {
fn default() -> Self {
Self {
pdo_assignment: EL5152PredefinedPdoAssignment::Period,
channel1: EL5152ChannelConfiguration::default(),
channel2: EL5152ChannelConfiguration::default(),
}
}
}
impl NewEthercatDevice for EL5152 {
fn new() -> Self {
let configuration: EL5152Configuration = EL5152Configuration::default();
Self {
configuration: configuration.clone(),
rxpdo: configuration.pdo_assignment.rxpdo_assignment(),
txpdo: configuration.pdo_assignment.txpdo_assignment(),
is_used: false,
}
}
}
impl ConfigurableDevice<EL5152Configuration> for EL5152 {
async fn write_config<'maindevice>(
&mut self,
device: &EthercrabSubDevicePreoperational<'maindevice>,
config: &EL5152Configuration,
) -> Result<(), anyhow::Error> {
config.write_config(device).await?;
self.configuration = config.clone();
self.txpdo = config.pdo_assignment.txpdo_assignment();
self.rxpdo = config.pdo_assignment.rxpdo_assignment();
Ok(())
}
fn get_config(&self) -> EL5152Configuration {
self.configuration.clone()
}
}
impl EncoderInputDevice<EL5152Port> for EL5152 {
fn get_counter_value(&self, port: EL5152Port) -> Result<EncoderInputCounter, anyhow::Error> {
let value = match port {
EL5152Port::ENC1 => self
.txpdo
.status_channel1
.as_ref()
.map_or(0, |status| status.counter_value),
EL5152Port::ENC2 => self
.txpdo
.status_channel2
.as_ref()
.map_or(0, |status| status.counter_value),
};
Ok(EncoderInputCounter { value })
}
fn get_frequency(
&self,
port: EL5152Port,
) -> Result<Option<EncoderInputFrequency>, anyhow::Error> {
let frequency = match port {
EL5152Port::ENC1 => {
self.txpdo
.frequency_channel1
.as_ref()
.map(|f| EncoderInputFrequency {
value: f.frequency_value,
})
}
EL5152Port::ENC2 => {
self.txpdo
.frequency_channel2
.as_ref()
.map(|f| EncoderInputFrequency {
value: f.frequency_value,
})
}
};
Ok(frequency)
}
fn get_period(&self, port: EL5152Port) -> Result<Option<EncoderInputPeriod>, anyhow::Error> {
let period = match port {
EL5152Port::ENC1 => self
.txpdo
.period_channel1
.as_ref()
.map(|p| EncoderInputPeriod {
value: p.period_value,
}),
EL5152Port::ENC2 => self
.txpdo
.period_channel2
.as_ref()
.map(|p| EncoderInputPeriod {
value: p.period_value,
}),
};
Ok(period)
}
fn set_counter(&mut self, port: EL5152Port, value: u32) -> Result<(), anyhow::Error> {
match port {
EL5152Port::ENC1 => {
if let Some(control) = self.rxpdo.control_channel1.as_mut() {
control.set_counter_value = value;
control.set_counter = true;
}
}
EL5152Port::ENC2 => {
if let Some(control) = self.rxpdo.control_channel2.as_mut() {
control.set_counter_value = value;
control.set_counter = true;
}
}
}
Ok(())
}
}
impl Configuration for EL5152Configuration {
async fn write_config<'a>(
&self,
device: &EthercrabSubDevicePreoperational<'a>,
) -> Result<(), anyhow::Error> {
// Configure channel 1
self.channel1.write_channel_config(device, 0x8000).await?;
// Configure channel 2
self.channel2.write_channel_config(device, 0x8010).await?;
// Write PDO assignments
self.pdo_assignment
.txpdo_assignment()
.write_config(device)
.await?;
self.pdo_assignment
.rxpdo_assignment()
.write_config(device)
.await?;
Ok(())
}
}
#[derive(Debug, Clone, RxPdo)]
pub struct EL5152RxPdo {
#[pdo_object_index(0x1600)]
pub control_channel1: Option<El5152EncoderControl>,
#[pdo_object_index(0x1602)]
pub control_channel2: Option<El5152EncoderControl>,
}
#[derive(Debug, Clone, TxPdo)]
pub struct EL5152TxPdo {
#[pdo_object_index(0x1A00)]
pub status_channel1: Option<El5152EncoderStatus>,
#[pdo_object_index(0x1A02)]
pub period_channel1: Option<El5152EncoderPeriod>,
#[pdo_object_index(0x1A03)]
pub frequency_channel1: Option<El5152EncoderFrequency>,
#[pdo_object_index(0x1A04)]
pub status_channel2: Option<El5152EncoderStatus>,
#[pdo_object_index(0x1A06)]
pub period_channel2: Option<El5152EncoderPeriod>,
#[pdo_object_index(0x1A07)]
pub frequency_channel2: Option<El5152EncoderFrequency>,
}
impl PredefinedPdoAssignment<EL5152TxPdo, EL5152RxPdo> for EL5152PredefinedPdoAssignment {
fn rxpdo_assignment(&self) -> EL5152RxPdo {
EL5152RxPdo {
control_channel1: Some(El5152EncoderControl::default()),
control_channel2: Some(El5152EncoderControl::default()),
}
}
fn txpdo_assignment(&self) -> EL5152TxPdo {
match self {
Self::Period => EL5152TxPdo {
status_channel1: Some(El5152EncoderStatus::default()),
period_channel1: Some(El5152EncoderPeriod::default()),
frequency_channel1: None,
status_channel2: Some(El5152EncoderStatus::default()),
period_channel2: Some(El5152EncoderPeriod::default()),
frequency_channel2: None,
},
Self::Frequency => EL5152TxPdo {
status_channel1: Some(El5152EncoderStatus::default()),
period_channel1: None,
frequency_channel1: Some(El5152EncoderFrequency::default()),
status_channel2: Some(El5152EncoderStatus::default()),
period_channel2: None,
frequency_channel2: Some(El5152EncoderFrequency::default()),
},
}
}
}
#[derive(Debug, Clone)]
pub struct EL5152ChannelConfiguration {
// 80n0:3
pub enable_counter: u8,
// 80n0:8
pub disable_filter: u8,
// 80n0:A
pub enable_micro_increment: u8,
// 80n0:E
pub reversion_rotation: u8,
// 80n0:F 0-µs, 1-ms
pub frequency_based_window: u8,
// 80n0:11
pub frequency_window: u16,
// 80n0:13
pub frequency_scaling: u16,
// 80n0:14
pub period_scaling: u16,
// 80n0:15 100: "0.01 Hz"
pub frequency_resolution: u16,
// 80n0:16 100: "100 ns"
pub period_resolution: u16,
// 80n0:17
pub frequency_wait_time: u16,
}
impl EL5152ChannelConfiguration {
pub async fn write_channel_config<'a>(
&self,
device: &EthercrabSubDevicePreoperational<'a>,
base_index: u16,
) -> Result<(), anyhow::Error> {
device
.sdo_write(base_index, 0x03, self.enable_counter)
.await?;
device
.sdo_write(base_index, 0x08, self.disable_filter)
.await?;
device
.sdo_write(base_index, 0x0A, self.enable_micro_increment)
.await?;
device
.sdo_write(base_index, 0x0E, self.reversion_rotation)
.await?;
device
.sdo_write(base_index, 0x0F, self.frequency_based_window)
.await?;
device
.sdo_write(base_index, 0x11, self.frequency_window)
.await?;
device
.sdo_write(base_index, 0x13, self.frequency_scaling)
.await?;
device
.sdo_write(base_index, 0x14, self.period_scaling)
.await?;
device
.sdo_write(base_index, 0x15, self.frequency_resolution)
.await?;
device
.sdo_write(base_index, 0x16, self.period_resolution)
.await?;
device
.sdo_write(base_index, 0x17, self.frequency_wait_time)
.await?;
Ok(())
}
}
impl Default for EL5152ChannelConfiguration {
fn default() -> Self {
Self {
enable_counter: 0x01u8,
disable_filter: 0x00u8,
enable_micro_increment: 0x00u8,
reversion_rotation: 0x00u8,
frequency_based_window: 0x00u8,
frequency_window: 0x2710u16,
frequency_scaling: 0x0064u16,
period_scaling: 0x0064u16,
frequency_resolution: 0x0064u16,
period_resolution: 0x0064u16,
frequency_wait_time: 0x0640u16,
}
}
}
pub const EL5152_VENDOR_ID: u32 = 0x2;
pub const EL5152_PRODUCT_ID: u32 = 0x14203052;
pub const EL5152_REVISION_A: u32 = 0x140000;
pub const EL5152_IDENTITY_A: SubDeviceIdentityTuple =
(EL5152_VENDOR_ID, EL5152_PRODUCT_ID, EL5152_REVISION_A);
+9 -5
View File
@@ -15,6 +15,7 @@ pub mod el3024;
pub mod el3062_0030;
pub mod el3204;
pub mod el4002;
pub mod el5152;
pub mod el6021;
pub mod el7031;
pub mod el7031_0030;
@@ -22,7 +23,7 @@ pub mod el7041_0052;
// pub mod el4008;
use super::devices::el1008::EL1008;
use crate::{
devices::{el2521::EL2521, el3062_0030::EL3062_0030_IDENTITY_A},
devices::{el2521::EL2521, el4002::EL4002},
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
};
use anyhow::anyhow;
@@ -38,8 +39,10 @@ use el2522::{EL2522, EL2522_IDENTITY_A};
use el3001::EL3001_IDENTITY_A;
use el3021::EL3021_IDENTITY_A;
use el3024::EL3024_IDENTITY_A;
use el3062_0030::EL3062_0030_IDENTITY_A;
use el3204::EL3204_IDENTITY_A;
use el4002::EL4002_IDENTITY_A;
use el5152::{EL5152, EL5152_IDENTITY_A};
use el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D};
use el3204::EL3204_IDENTITY_B;
@@ -191,9 +194,6 @@ pub fn device_from_subdevice_identity_tuple(
EL2008_IDENTITY_A | EL2008_IDENTITY_B => Ok(Arc::new(RwLock::new(EL2008::new()))),
// TODO: implement EL2024 identity
// EL2024 => Ok(Arc::new(RwLock::new(EL2024::new()))),
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
Ok(Arc::new(RwLock::new(EL2521::new())))
}
EL2522_IDENTITY_A => Ok(Arc::new(RwLock::new(EL2522::new()))),
// TODO: implement EL2634 identity
// "EL2634" => Ok(Arc::new(RwLock::new(EL2634::new()))),
@@ -203,7 +203,8 @@ pub fn device_from_subdevice_identity_tuple(
EL3021_IDENTITY_A => Ok(Arc::new(RwLock::new(el3021::EL3021::new()))),
EL3024_IDENTITY_A => Ok(Arc::new(RwLock::new(el3024::EL3024::new()))),
EL3062_0030_IDENTITY_A => Ok(Arc::new(RwLock::new(el3062_0030::EL3062_0030::new()))),
EL4002_IDENTITY_A => Ok(Arc::new(RwLock::new(el4002::EL4002::new()))),
EL4002_IDENTITY_A => Ok(Arc::new(RwLock::new(EL4002::new()))),
EL5152_IDENTITY_A => Ok(Arc::new(RwLock::new(EL5152::new()))),
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C | EL6021_IDENTITY_D => {
Ok(Arc::new(RwLock::new(el6021::EL6021::new())))
}
@@ -214,6 +215,9 @@ pub fn device_from_subdevice_identity_tuple(
EL7031_IDENTITY_A | EL7031_IDENTITY_B => Ok(Arc::new(RwLock::new(el7031::EL7031::new()))),
EL7031_0030_IDENTITY_A => Ok(Arc::new(RwLock::new(el7031_0030::EL7031_0030::new()))),
EL7041_0052_IDENTITY_A => Ok(Arc::new(RwLock::new(el7041_0052::EL7041_0052::new()))),
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
Ok(Arc::new(RwLock::new(EL2521::new())))
}
_ => Err(anyhow::anyhow!(
"[{}::device_from_subdevice] No Driver: vendor_id: 0x{:x}, product_id: 0x{:x}, revision: 0x{:x}",
module_path!(),
+120
View File
@@ -0,0 +1,120 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Encoder Input device
///
/// Reads encoder values (counter, frequency, period) from the device.
pub struct EncoderInput {
/// Read the counter value from the encoder
get_counter: Box<dyn Fn() -> Result<EncoderInputCounter, anyhow::Error> + Send + Sync>,
/// Read the frequency value from the encoder (if available)
get_frequency:
Box<dyn Fn() -> Result<Option<EncoderInputFrequency>, anyhow::Error> + Send + Sync>,
/// Read the period value from the encoder (if available)
get_period: Box<dyn Fn() -> Result<Option<EncoderInputPeriod>, anyhow::Error> + Send + Sync>,
/// Set the counter value
set_counter: Box<dyn Fn(u32) -> Result<(), anyhow::Error> + Send + Sync>,
}
impl fmt::Debug for EncoderInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "EncoderInput")
}
}
/// Implement on device that have encoder inputs
impl EncoderInput {
pub fn new<PORT>(device: Arc<RwLock<dyn EncoderInputDevice<PORT>>>, port: PORT) -> Self
where
PORT: Clone + Send + Sync + 'static,
{
// build sync get counter closure
let port_counter = port.clone();
let device_counter = device.clone();
let get_counter = Box::new(move || {
let device = block_on(device_counter.read());
device.get_counter_value(port_counter.clone())
});
// build sync get frequency closure
let port_frequency = port.clone();
let device_frequency = device.clone();
let get_frequency = Box::new(move || {
let device = block_on(device_frequency.read());
device.get_frequency(port_frequency.clone())
});
// build sync get period closure
let port_period = port.clone();
let device_period = device.clone();
let get_period = Box::new(move || {
let device = block_on(device_period.read());
device.get_period(port_period.clone())
});
// build sync set counter closure
let port_set = port;
let device_set = device;
let set_counter = Box::new(move |value: u32| {
let mut device = block_on(device_set.write());
device.set_counter(port_set.clone(), value)
});
Self {
get_counter,
get_frequency,
get_period,
set_counter,
}
}
/// Get the current counter value of the encoder
pub fn get_counter_value(&self) -> Result<u32, anyhow::Error> {
let counter = (self.get_counter)()?;
Ok(counter.value)
}
/// Get the current frequency value of the encoder (if available)
pub fn get_frequency_value(&self) -> Result<Option<u32>, anyhow::Error> {
let frequency = (self.get_frequency)()?;
Ok(frequency.map(|f| f.value))
}
/// Get the current period value of the encoder (if available)
pub fn get_period_value(&self) -> Result<Option<u32>, anyhow::Error> {
let period = (self.get_period)()?;
Ok(period.map(|p| p.value))
}
/// Set the counter value
pub fn set_counter_value(&self, value: u32) -> Result<(), anyhow::Error> {
(self.set_counter)(value)
}
}
#[derive(Debug, Clone)]
pub struct EncoderInputCounter {
pub value: u32,
}
#[derive(Debug, Clone)]
pub struct EncoderInputFrequency {
pub value: u32,
}
#[derive(Debug, Clone)]
pub struct EncoderInputPeriod {
pub value: u32,
}
pub trait EncoderInputDevice<PORTS>: Send + Sync
where
PORTS: Clone,
{
fn get_counter_value(&self, port: PORTS) -> Result<EncoderInputCounter, anyhow::Error>;
fn get_frequency(&self, port: PORTS) -> Result<Option<EncoderInputFrequency>, anyhow::Error>;
fn get_period(&self, port: PORTS) -> Result<Option<EncoderInputPeriod>, anyhow::Error>;
fn set_counter(&mut self, port: PORTS, value: u32) -> Result<(), anyhow::Error>;
}
+1
View File
@@ -3,6 +3,7 @@ pub mod analog_input_dummy;
pub mod analog_output;
pub mod digital_input;
pub mod digital_output;
pub mod encoder_input;
pub mod pulse_train_output;
pub mod serial_interface;
pub mod stepper_velocity_el70x1;
+4 -4
View File
@@ -1,24 +1,24 @@
use super::RxPdoObject;
use bitvec::prelude::*;
use ethercat_hal_derive::PdoObject;
use super::RxPdoObject;
/// PDO Object for EL40xx (analog output) devices
///
/// The "Analog Output" holds the output value and status information.
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
#[pdo_object(bits = 16)]
pub struct AnalogOutput {
/// Output value (-32768-32767 typically corresponds to -10 -10V or 0mA-20mA (from 4-20mA for EL402x))
/// Output value (-32768-32767 typically corresponds to -10V to +10V)
pub value: i16,
}
impl RxPdoObject for AnalogOutput {
fn write(&self, bits: &mut BitSlice<u8, Lsb0>) {
// Write the output value to bits 0-15
bits[0..16].store_le(self.value);
bits[0..16].store_le(self.value as u16);
}
}
#[cfg(test)]
mod tests {
use super::*;
+96
View File
@@ -0,0 +1,96 @@
use super::{RxPdoObject, TxPdoObject};
use bitvec::prelude::*;
use ethercat_hal_derive::PdoObject;
/// PDO Object for EL5152 encoder control (RxPDO)
/// Based on 1600/1602 mapping: Set counter (1 bit) + alignment + Set counter value (32/16 bit)
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
#[pdo_object(bits = 48)] // Total 48 bits for full mapping
pub struct El5152EncoderControl {
/// Set counter - execute counter setting (1 bit) - 0x7000:03
pub set_counter: bool,
/// Set counter value (32-bit) - 0x7000:11
pub set_counter_value: u32,
}
impl RxPdoObject for El5152EncoderControl {
fn write(&self, bits: &mut BitSlice<u8, Lsb0>) {
// bit 2: Set counter (0x7000:03)
bits.set(2, self.set_counter);
// Set counter value (bits 16-47, 32-bit value) - 0x7000:11
bits[16..48].store_le(self.set_counter_value);
}
}
/// PDO Object for EL5152 encoder status (TxPDO)
/// Based on 1A00/1A04 mapping: Status bits + Counter value (32-bit)
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
#[pdo_object(bits = 48)] // 16 bits status + 32 bits counter
pub struct El5152EncoderStatus {
/// Set counter done - counter was set (1 bit) - 0x6000:03
pub set_counter_done: bool,
/// Extrapolation stall - extrapolated counter invalid (1 bit) - 0x6000:08
pub extrapolation_stall: bool,
/// Status of input A (1 bit) - 0x6000:09
pub status_input_a: bool,
/// Status of input B (1 bit) - 0x6000:0A
pub status_input_b: bool,
/// Sync Error - synchronization error occurred (1 bit) - 0x1C32:20
pub sync_error: bool,
/// TxPDO Toggle - toggled when TxPDO data is updated (1 bit) - 0x1800:09
pub txpdo_toggle: bool,
/// Counter value (32-bit) - 0x6000:11
pub counter_value: u32,
}
impl TxPdoObject for El5152EncoderStatus {
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
// bit 2: Set counter done (0x6000:03)
self.set_counter_done = bits[2];
// bit 7: Extrapolation stall (0x6000:08)
self.extrapolation_stall = bits[7];
// bit 8: Status of input A (0x6000:09)
self.status_input_a = bits[8];
// bit 9: Status of input B (0x6000:0A)
self.status_input_b = bits[9];
// bit 13: Sync error (0x1C32:20)
self.sync_error = bits[13];
// bit 15: TxPDO Toggle (0x1800:09)
self.txpdo_toggle = bits[15];
// Counter value (bits 16-47, 32-bit value) - 0x6000:11
self.counter_value = bits[16..48].load_le::<u32>();
}
}
/// PDO Object for EL5152 encoder frequency measurement (TxPDO)
/// Based on 1A03/1A07 mapping: 32-bit frequency value only
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
#[pdo_object(bits = 32)]
pub struct El5152EncoderFrequency {
/// Frequency value (32-bit) - 0x6000:13 / 0x6010:13
pub frequency_value: u32,
}
impl TxPdoObject for El5152EncoderFrequency {
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
// Frequency value (bits 0-31) - 0x6000:13 / 0x6010:13
self.frequency_value = bits[0..32].load_le::<u32>();
}
}
/// PDO Object for EL5152 encoder period measurement (TxPDO)
/// Based on 1A02/1A06 mapping: 32-bit period value only
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
#[pdo_object(bits = 32)]
pub struct El5152EncoderPeriod {
/// Period value (32-bit) - 0x6000:14 / 0x6010:14
pub period_value: u32,
}
impl TxPdoObject for El5152EncoderPeriod {
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
// Period value (bits 0-31) - 0x6000:14 / 0x6010:14
self.period_value = bits[0..32].load_le::<u32>();
}
}
+1
View File
@@ -3,6 +3,7 @@ pub mod basic;
pub mod el252x;
pub mod el32xx;
pub mod el40xx;
pub mod el5152;
pub mod el70x1;
use crate::coe::Configuration;
use bitvec::prelude::*;
+2
View File
@@ -113,6 +113,8 @@ impl EL40XXChannelConfiguration {
device: &EthercrabSubDevicePreoperational<'a>,
base_index: u16,
) -> Result<(), anyhow::Error> {
tracing::info!("write_channel_config");
// Write all configuration parameters according to the documentation table
device
.sdo_write(base_index, 0x01, self.enable_user_scale as u8)
+28 -27
View File
@@ -3,23 +3,23 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
# Crane for Rust builds with dependency caching
crane = {
url = "github:ipetkov/crane";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
# Add the QiTech Control repository
qitech-control = {
url = "github:qitechgmbh/control";
inputs.nixpkgs.follows = "nixpkgs";
};
# Add flake-utils which was missing
flake-utils = {
url = "github:numtide/flake-utils";
@@ -27,31 +27,27 @@
};
outputs = { self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
let
# Import git info at the top level so it's available everywhere
installInfo = import ./nixos/os/installInfo.nix;
in
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [
overlays = [
# Add our own overlay for QiTech packages
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
server = final.callPackage ./nixos/packages/server.nix {
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
electron = final.callPackage ./nixos/packages/electron.nix {
nodejs = final.nodejs_22;
};
};
})
];
pkgs = import nixpkgs { inherit system overlays; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
craneLib = crane.mkLib pkgs;
# Use Rust 1.86 stable from nixpkgs
rust = pkgs.rustc;
in {
@@ -60,7 +56,7 @@
electron = pkgs.qitechPackages.electron;
default = self.packages.${system}.server;
};
devShells.default = craneLib.devShell {
packages = with pkgs; [
pkg-config
@@ -69,7 +65,9 @@
nodejs_22
nodePackages.npm
];
hardeningDisable = [ "fortify" ];
shellHook = ''
echo "QiTech Control Development Environment"
echo "Rust version: $(rustc --version)"
@@ -80,14 +78,19 @@
) // {
nixosModules.qitech = import ./nixos/modules/qitech.nix;
nixosModules.default = self.nixosModules.qitech;
# Define nixosConfigurations outside of eachDefaultSystem
nixosConfigurations = {
nixosConfigurations =
let
system = builtins.currentSystem;
pkgs = import nixpkgs { inherit system; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
in {
# Replace "nixos" with your actual hostname
nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; # Specify the correct system
system = system;
specialArgs = {
installInfo = installInfo; # Pass installInfo to modules
gitInfo = gitInfo; # Pass gitInfo to modules
};
modules = [
# Apply the overlays to the system
@@ -95,24 +98,22 @@
# Add our own overlay for QiTech packages with commit hash support
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = installInfo.gitCommit;
server = final.callPackage ./nixos/packages/server.nix {
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = installInfo.gitCommit;
nodejs = final.nodejs_22;
};
};
})
];
}
./nixos/os/configuration.nix
# QiTech Control module
self.nixosModules.qitech
# Home Manager module
home-manager.nixosModules.home-manager
{
@@ -124,4 +125,4 @@
};
};
};
}
}
+30
View File
@@ -0,0 +1,30 @@
touch /tmp/installInfo.nix
# Capture all git information
GIT_TIMESTAMP=$(git --no-pager show -s --format=%cI HEAD) # e.g., "2025-06-10T14:30:45+02:00"
GIT_COMMIT=$(git rev-parse HEAD) # e.g., "b2c7f6e0b138174770798f84ada8b0aa65afeb"
GIT_URL=$(git config --get remote.origin.url) # e.g., "https://github.com/qitechindustries/control.git"
# Determine the git abbreviation (tag, branch, or commit hash)
GIT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || echo "")
if [ -n "$GIT_TAG" ]; then
GIT_ABBREVIATION="$GIT_TAG" # e.g., "2.0.0" (when on a tag)
else
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$GIT_BRANCH" = "HEAD" ]; then
GIT_ABBREVIATION=$(git rev-parse --short HEAD) # e.g., "b2c7f6e" (when on detached HEAD/commit)
else
GIT_ABBREVIATION="$GIT_BRANCH" # e.g., "main", "develop" (when on a branch)
fi
fi
tee /tmp/installInfo.nix > /dev/null << EOF
{
gitTimestamp = "$GIT_TIMESTAMP";
gitCommit = "$GIT_COMMIT";
gitAbbreviation = "$GIT_ABBREVIATION";
gitUrl = "$GIT_URL";
# Like abbreviation but can be used in system.nixos.label
gitAbbreviationEscaped = "$GIT_ABBREVIATION_ESCAPED";
}
EOF
+24 -38
View File
@@ -1,53 +1,39 @@
# write the installInfo.nix while
# we capture information about the current git commit
sudo touch ./nixos/os/installInfo.nix
#!/bin/sh
set -euo pipefail
# Capture all git information
GIT_TIMESTAMP=$(git --no-pager show -s --format=%cI HEAD) # e.g., "2025-06-10T14:30:45+02:00"
GIT_COMMIT=$(git rev-parse HEAD) # e.g., "b2c7f6e0b138174770798f84ada8b0aa65afeb"
GIT_URL=$(git config --get remote.origin.url) # e.g., "https://github.com/qitechindustries/control.git"
export GIT_TIMESTAMP=$(git --no-pager show -s --format=%cI HEAD) # e.g., "2025-06-10T14:30:45+02:00"
export GIT_COMMIT=$(git rev-parse HEAD) # e.g., "b2c7f6e0b138174770798f84ada8b0aa65afeb"
export GIT_URL=$(git config --get remote.origin.url) # e.g., "https://github.com/qitechindustries/control.git"
# Determine the git abbreviation (tag, branch, or commit hash)
GIT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || echo "")
export GIT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || echo "")
if [ -n "$GIT_TAG" ]; then
GIT_ABBREVIATION="$GIT_TAG" # e.g., "2.0.0" (when on a tag)
export GIT_ABBREVIATION="$GIT_TAG" # e.g., "2.0.0" (when on a tag)
else
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$GIT_BRANCH" = "HEAD" ]; then
GIT_ABBREVIATION=$(git rev-parse --short HEAD) # e.g., "b2c7f6e" (when on detached HEAD/commit)
export GIT_ABBREVIATION=$(git rev-parse --short HEAD) # e.g., "b2c7f6e" (when on detached HEAD/commit)
else
GIT_ABBREVIATION="$GIT_BRANCH" # e.g., "main", "develop" (when on a branch)
export GIT_ABBREVIATION="$GIT_BRANCH" # e.g., "main", "develop" (when on a branch)
fi
fi
# Create escaped version for system.nixos.label
GIT_ABBREVIATION_ESCAPED=$(echo "$GIT_ABBREVIATION" | sed -e 's/+/-/g' -e 's/[^a-zA-Z0-9:_\.-]//g') # e.g., "2-0-0", "main", "b2c7f6e"
export GIT_ABBREVIATION_ESCAPED=$(echo "$GIT_ABBREVIATION" | sed -e 's/+/-/g' -e 's/[^a-zA-Z0-9:_\.-]//g') # e.g., "2-0-0", "main", "b2c7f6e"
sudo tee ./nixos/os/installInfo.nix > /dev/null << EOF
{
gitTimestamp = "$GIT_TIMESTAMP";
gitCommit = "$GIT_COMMIT";
gitAbbreviation = "$GIT_ABBREVIATION";
gitUrl = "$GIT_URL";
# Like abbreviation but can be used in system.nixos.label
gitAbbreviationEscaped = "$GIT_ABBREVIATION_ESCAPED";
}
EOF
sudo \
--preserve-env=GIT_TIMESTAMP \
--preserve-env=GIT_COMMIT \
--preserve-env=GIT_URL \
--preserve-env=GIT_ABBREVIATION \
--preserve-env=GIT_ABBREVIATION_ESCAPED \
nixos-rebuild boot \
--flake .#nixos \
--show-trace \
--impure \
--option sandbox false \
--option eval-cache false
# make sure the installInfo.nix file is tracked by git
# otherwise it will be ignored by nix
# configure a git user
git config --global user.name "nixos-install.sh"
git config --global user.email "nixos-install.sh@localhost"
# add the installInfo.nix file to the git index
git add ./nixos/os/installInfo.nix
# commit the changes
git commit -m "Add installInfo.nix file with current commit information"
# Now we can install the system
# Now we install the new system
if sudo nixos-rebuild boot --flake .#nixos --show-trace --impure --option sandbox false --option eval-cache false; then
reboot
else
exit 1
fi
sudo reboot
+25
View File
@@ -0,0 +1,25 @@
{ pkgs, ... }:
let
# Generate Git info in the store
gitInfoDerivation = pkgs.runCommand "git-info" {} ''
gitTimestamp="${builtins.getEnv "GIT_TIMESTAMP"}"
gitCommit="${builtins.getEnv "GIT_COMMIT"}"
gitUrl="${builtins.getEnv "GIT_URL"}"
gitAbbreviation="${builtins.getEnv "GIT_ABBREVIATION"}"
gitAbbreviationEscaped="${builtins.getEnv "GIT_ABBREVIATION_ESCAPED"}"
mkdir -p $out
cat <<EOF > $out/git-info.json
{
"gitTimestamp": "$gitTimestamp",
"gitCommit": "$gitCommit",
"gitAbbreviation": "$gitAbbreviation",
"gitUrl": "$gitUrl",
"gitAbbreviationEscaped": "$gitAbbreviationEscaped"
}
EOF
'';
in
builtins.fromJSON (builtins.readFile "${gitInfoDerivation}/git-info.json")
+33 -29
View File
@@ -70,36 +70,40 @@ in {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Grant specific capabilities needed for EtherCAT
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE";
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false; # Need access to devices
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Capabilities (unchanged, as you wanted)
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
# Open only /proc/irq explicitly
ReadWritePaths = [ "/proc/irq" ];
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false;
# Must disable this to allow /proc/irq writes
ProtectKernelTunables = false;
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
environment = {
RUST_BACKTRACE = "full";
+19 -20
View File
@@ -1,9 +1,8 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, installInfo, ... }:
{ config, pkgs, ... }:
let
gitInfo = import ../gitInfo.nix { inherit pkgs; };
in
{
imports =
[ # Include the results of the hardware scan.
@@ -16,17 +15,17 @@
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
# Graphical
"logo.nologo" # Remove kernel logo during boot
# Performance
# Performance
# Specific Vulnerabilities Addressed by Mitigations:
# - Spectre variants (V1, V2, V4, SWAPGS, SpectreRSB, etc.)
# - Meltdown (Rogue Data Cache Load)
@@ -36,7 +35,7 @@
# - 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.
@@ -48,7 +47,7 @@
# 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)
@@ -78,7 +77,7 @@
experimental-features = nix-command flakes
'';
settings = {
sandbox = false;
sandbox = false;
};
};
@@ -144,7 +143,7 @@
autoSuspend = false;
wayland = true;
};
services.xserver.desktopManager.gnome.enable = true;
# Disable sleep/suspend
@@ -217,7 +216,7 @@
enable = true;
openFirewall = true;
user = "qitech-service";
group = "qitech-service";
group = "qitech-service";
package = pkgs.qitechPackages.server;
};
@@ -259,7 +258,7 @@
pkgs.qitechPackages.electron
htop
wireshark
pciutils
pciutils
neofetch
];
@@ -295,15 +294,15 @@
# Set system wide env variables
environment.variables = {
QITECH_OS = "true";
QITECH_OS_GIT_TIMESTAMP = installInfo.gitTimestamp;
QITECH_OS_GIT_COMMIT = installInfo.gitCommit;
QITECH_OS_GIT_ABBREVIATION = installInfo.gitAbbreviation;
QITECH_OS_GIT_URL = installInfo.gitUrl;
QITECH_OS_GIT_TIMESTAMP = gitInfo.gitTimestamp;
QITECH_OS_GIT_COMMIT = gitInfo.gitCommit;
QITECH_OS_GIT_ABBREVIATION = gitInfo.gitAbbreviation;
QITECH_OS_GIT_URL = gitInfo.gitUrl;
};
# Set revision labe;
system.nixos.label = "${installInfo.gitAbbreviationEscaped}_${installInfo.gitCommit}";
system.nixos.label = "${gitInfo.gitAbbreviationEscaped}_${gitInfo.gitCommit}";
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
+1 -3
View File
@@ -7,13 +7,11 @@
, git
, cacert
, fetchFromGitHub
, commitHash
}:
stdenv.mkDerivation rec {
pname = "qitech-control-electron";
version = commitHash;
version = "1.0.0";
src = lib.cleanSource ../../electron;
nativeBuildInputs = [
+22 -50
View File
@@ -3,64 +3,36 @@
, pkg-config
, libudev-zero
, libpcap
, commitHash
, craneLib
}:
let
# Use crane's source cleaning which is more intelligent for Cargo projects
# Bind variables so they can be inherited inside inner calls
pname = "server";
version = "1.0.0";
strictDeps = true;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
# Cleaned source
src = craneLib.cleanCargoSource ../..;
# Common arguments for both dependency and app builds
commonArgs = {
inherit src;
strictDeps = true;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
# Build only the server package with journald logging for NixOS
pname = "server";
version = commitHash;
# Reduce memory usage during build
CARGO_BUILD_JOBS = "2";
};
# Build *just* the cargo dependencies (of the entire workspace),
# so we can reuse all of that work when running in CI
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
# Safe fallback for CARGO_BUILD_JOBS in impure builds
cargoJobs = if (builtins.tryEval (builtins.getEnv "CARGO_BUILD_JOBS")).success
then builtins.getEnv "CARGO_BUILD_JOBS"
else "2";
in
# Uses Rust 1.86 stable from nixpkgs 25.05 with Crane for dependency caching
craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
# Enable journald logging feature for NixOS systems and build only server package
# Anbale io_uring support
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
# Create a swap file if building on a memory-constrained system
preBuild = ''
if [ $(free -m | grep Mem | awk '{print $2}') -lt 6000 ]; then
mkdir -p $TMPDIR/swap
dd if=/dev/zero of=$TMPDIR/swap/swapfile bs=1M count=8192
chmod 600 $TMPDIR/swap/swapfile
mkswap $TMPDIR/swap/swapfile
swapon $TMPDIR/swap/swapfile
fi
'';
craneLib.buildPackage {
inherit pname version src strictDeps nativeBuildInputs buildInputs;
postBuild = ''
if [ -f $TMPDIR/swap/swapfile ]; then
swapoff $TMPDIR/swap/swapfile
rm $TMPDIR/swap/swapfile
fi
'';
# Use the safe cargoJobs variable
CARGO_BUILD_JOBS = cargoJobs;
meta = with lib; {
description = "QiTech Control Server";
homepage = "https://qitech.de";
platforms = platforms.linux;
# Build cargo dependencies once for caching
cargoArtifacts = craneLib.buildDepsOnly {
inherit src strictDeps nativeBuildInputs buildInputs pname version;
};
})
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
}
+4 -3
View File
@@ -18,7 +18,6 @@ uom = { version = "0.36.0", default-features = false, features = ["f64"] }
serde = "1.0.217"
anyhow = "1.0.99"
open = "5.3.2"
bitvec = "1.0.1"
lazy_static = "1.5.0"
dhat = { version = "0.3.3", optional = true }
@@ -27,7 +26,6 @@ euclid = "0.22.11"
# concurrency
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
thread-priority = "2.1.0"
regex = "1.11.2"
futures = "0.3.31"
@@ -38,7 +36,6 @@ tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
axum = { version = "0.8.4", features = ["macros"] }
# serial
crc = "3.3.0"
serialport = "4.7.3"
#tracing
@@ -81,7 +78,11 @@ tracing-otel = [
]
mock-machine = []
io-uring = []
development-build = []
[profile.release]
codegen-units = 1
lto = "fat"
[package.metadata.cargo-machete]
ignored = ["tonic"]
+15 -9
View File
@@ -9,7 +9,7 @@ use crate::{
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
use control_core::ethercat::eeprom_identification::read_device_identifications;
#[cfg(unix)]
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
use control_core::irq_handling::set_irq_affinity;
use control_core::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
@@ -23,6 +23,9 @@ use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeou
use smol::channel::Sender;
use std::{sync::Arc, time::Duration};
const SM_OUTPUT: u16 = 0x1C32;
const SM_INPUT: u16 = 0x1C33;
pub async fn setup_loop(
thread_panic_tx: Sender<PanicDetails>,
interface: &str,
@@ -47,7 +50,7 @@ pub async fn setup_loop(
.spawn(move || {
send_panic(thread_panic_tx_clone);
#[cfg(unix)]
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
match set_irq_affinity(&interface, 3) {
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
Err(e) => tracing::error!("set_irq_handler_affinity failed: {:?}", e),
@@ -90,13 +93,13 @@ pub async fn setup_loop(
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(10),
eeprom: Duration::from_millis(100),
// Default 0ms
wait_loop_delay: Duration::from_millis(1),
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(100),
mailbox_echo: Duration::from_millis(1000),
// Default 1000ms
mailbox_response: Duration::from_millis(1000),
mailbox_response: Duration::from_millis(10000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
@@ -157,14 +160,12 @@ pub async fn setup_loop(
},
)
.collect::<Vec<_>>();
let devices = device_identifications
.into_iter()
.zip(devices)
.zip(&subdevices)
.map(|((a, b), c)| (a, b, c))
.collect::<Vec<_>>();
// filter devices and if Option<DeviceMachineIdentification> is Some
// return identified_devices, identified_device_identifications, identified_subdevices
let (identified_device_identifications, identified_devices, identified_subdevices): (
@@ -198,7 +199,6 @@ pub async fn setup_loop(
acc
},
);
// construct machines
{
let mut machines_guard = app_state.machines.write().await;
@@ -220,6 +220,12 @@ pub async fn setup_loop(
.map(|(device_identification, device, _)| (device_identification.clone(), device.clone()))
.collect::<Vec<_>>();
for subdevice in subdevices.iter() {
if subdevice.name() == "EL5152" {
subdevice.sdo_write(SM_OUTPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
subdevice.sdo_write(SM_INPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
}
}
// Notify client via socketio
let app_state_clone = app_state.clone();
smol::block_on(async move {
+5 -1
View File
@@ -127,7 +127,11 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
)
})?;
}
// Apparently 500 Microseconds is a safe starting point for ethercat
#[cfg(feature = "development-build")]
smol::Timer::after(Duration::from_micros(2000)).await;
#[cfg(not(feature = "development-build"))]
smol::Timer::after(Duration::from_micros(500)).await;
}
+26
View File
@@ -0,0 +1,26 @@
use super::{AquaPathV1, AquaPathV1Mode};
use control_core::machines::new::MachineAct;
use std::time::{Duration, Instant};
impl MachineAct for AquaPathV1 {
fn act(&mut self, now_ts: Instant) {
match self.mode {
AquaPathV1Mode::Standby => {
self.switch_to_standby();
}
AquaPathV1Mode::Auto => {
self.switch_to_auto();
}
}
let now = Instant::now();
self.front_controller.update(now_ts);
self.back_controller.update(now_ts);
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::{sync::Arc, time::Duration};
use super::{AquaPathV1, AquaPathV1Mode};
use control_core::{
machines::api::MachineApi,
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use socketioxide::extract::SocketRef;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
pub front_flow: f64,
pub back_flow: f64,
pub front_temperature: f64,
pub back_temperature: f64,
pub front_temp_reservoir: f64,
pub back_temp_reservoir: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub is_default_state: bool,
/// mode state
pub mode_state: ModeState,
pub flow_states: FlowStates,
pub temperature_states: TempStates,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone)]
pub struct TempStates {
pub front: TempState,
pub back: TempState,
}
#[derive(Serialize, Debug, Clone)]
pub struct TempState {
pub temperature: f64,
pub target_temperature: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct ModeState {
pub mode: AquaPathV1Mode,
}
#[derive(Serialize, Debug, Clone)]
pub struct FlowStates {
pub front: FlowState,
pub back: FlowState,
}
#[derive(Serialize, Debug, Clone)]
pub struct FlowState {
pub flow: f64,
pub should_flow: bool,
}
pub enum AquaPathV1Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize)]
enum Mutation {
//Mode
SetAquaPathMode(AquaPathV1Mode),
SetFrontTemperature(f64),
SetBackTemperature(f64),
SetFrontFlow(bool),
SetBackFlow(bool),
}
#[derive(Debug)]
pub struct AquaPathV1Namespace {
pub namespace: Namespace,
}
impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
#[instrument(skip_all)]
fn emit(&mut self, events: AquaPathV1Events) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
self.namespace.emit(event, &buffer_fn);
}
}
impl AquaPathV1Namespace {
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
Self {
namespace: Namespace::new(socket_queue_tx),
}
}
}
impl CacheableEvents<AquaPathV1Events> for AquaPathV1Events {
fn event_value(&self) -> GenericEvent {
match self {
AquaPathV1Events::LiveValues(event) => event.into(),
AquaPathV1Events::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
AquaPathV1Events::LiveValues(_) => cache_one_hour,
AquaPathV1Events::State(_) => cache_first_and_last,
}
}
}
impl MachineApi for AquaPathV1 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetAquaPathMode(mode) => self.set_mode_state(mode),
Mutation::SetBackTemperature(temperature) => {
self.set_target_temperature(temperature, super::AquaPathSideType::Back)
}
Mutation::SetFrontTemperature(temperature) => {
self.set_target_temperature(temperature, super::AquaPathSideType::Front)
}
Mutation::SetBackFlow(should_pump) => {
self.set_should_pump(should_pump, super::AquaPathSideType::Back)
}
Mutation::SetFrontFlow(should_pump) => {
self.set_should_pump(should_pump, super::AquaPathSideType::Front)
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> &mut Namespace {
&mut self.namespace.namespace
}
}
+311
View File
@@ -0,0 +1,311 @@
use crate::machines::aquapath1::VolumeRate;
use crate::machines::aquapath1::{Flow, Temperature};
use control_core::controllers::pid::PidController;
use ethercat_hal::io::encoder_input::EncoderInput;
use ethercat_hal::io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
};
use std::time::{Duration, Instant};
use uom::si::f64::ThermodynamicTemperature;
use uom::si::thermodynamic_temperature::degree_celsius;
use uom::si::volume_rate::liter_per_minute;
#[derive(Debug)]
pub struct Controller {
pub pid: PidController,
temperature_pid_output: f64,
window_start: Instant,
pwm_period: Duration,
pub temperature: Temperature,
pub target_temperature: ThermodynamicTemperature,
pub current_temperature: ThermodynamicTemperature,
pub temp_reservoir: ThermodynamicTemperature,
pub min_temperature: ThermodynamicTemperature,
pub max_temperature: ThermodynamicTemperature,
pub cooling_controller: AnalogOutput,
pub cooling_relais: DigitalOutput,
pub heating_relais_1: DigitalOutput,
pub heating_relais_2: DigitalOutput,
pub temperature_sensor_in: TemperatureInput,
pub temperature_sensor_out: TemperatureInput,
pub cooling_allowed: bool,
pub heating_allowed: bool,
pub flow: Flow,
pump_relais: DigitalOutput,
pub should_pump: bool,
pub flow_sensor: EncoderInput,
pub pump_allowed: bool,
pub current_flow: VolumeRate,
pub max_flow: VolumeRate,
}
impl Controller {
pub fn new(
kp: f64,
ki: f64,
kd: f64,
pwm_duration: Duration,
temp: Temperature,
target_tempetature: ThermodynamicTemperature,
cooling_controller: AnalogOutput,
cooling_relais: DigitalOutput,
heating_relais_1: DigitalOutput,
heating_relais_2: DigitalOutput,
temp_sensor_in: TemperatureInput,
temp_sensor_out: TemperatureInput,
flow: Flow,
pump_relais: DigitalOutput,
flow_sensor: EncoderInput,
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
temperature_pid_output: 0.0,
window_start: Instant::now(),
pwm_period: pwm_duration,
target_temperature: target_tempetature,
current_temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
temp_reservoir: ThermodynamicTemperature::new::<degree_celsius>(25.0),
min_temperature: ThermodynamicTemperature::new::<degree_celsius>(10.0),
max_temperature: ThermodynamicTemperature::new::<degree_celsius>(50.0),
temperature: temp,
cooling_controller: cooling_controller,
cooling_relais: cooling_relais,
heating_relais_1: heating_relais_1,
heating_relais_2: heating_relais_2,
cooling_allowed: false,
heating_allowed: false,
temperature_sensor_in: temp_sensor_in,
temperature_sensor_out: temp_sensor_out,
flow: flow,
pump_relais: pump_relais,
flow_sensor: flow_sensor,
should_pump: false,
current_flow: VolumeRate::new::<liter_per_minute>(0.0),
pump_allowed: false,
max_flow: VolumeRate::new::<liter_per_minute>(10.0),
}
}
pub fn turn_pump_off(&mut self) {
tracing::info!("turn_pump_off");
self.flow.pump = false;
self.pump_relais.set(false);
}
pub fn turn_pump_on(&mut self) {
tracing::info!("turn_pump_on");
self.flow.pump = true;
self.pump_relais.set(true);
}
pub fn disallow_pump(&mut self) {
self.pump_allowed = false;
}
pub fn allow_pump(&mut self) {
self.pump_allowed = true;
}
pub fn get_pump(&mut self) -> bool {
self.pump_allowed
}
pub fn disable_cooling(&mut self) {
self.turn_cooling_off();
self.disallow_cooling();
}
pub fn disable_heating(&mut self) {
self.turn_heating_off();
self.disallow_heating();
}
pub fn enable_cooling(&mut self) {
self.turn_cooling_on();
self.allow_cooling();
}
pub fn enable_heating(&mut self) {
self.turn_heating_on();
self.allow_heating();
}
pub fn reset_pid(&mut self) {
self.pid.reset()
}
pub fn set_target_temperature(&mut self, temperature: ThermodynamicTemperature) {
self.reset_pid();
self.target_temperature = temperature;
}
pub fn get_temp_in(&mut self) -> ThermodynamicTemperature {
let temp = self.temperature_sensor_in.get_temperature();
match temp {
Ok(value) => ThermodynamicTemperature::new::<degree_celsius>(value),
Err(_) => ThermodynamicTemperature::new::<degree_celsius>(0.0),
}
}
pub fn get_temp_out(&mut self) -> ThermodynamicTemperature {
let temp = self.temperature_sensor_out.get_temperature();
match temp {
Ok(value) => ThermodynamicTemperature::new::<degree_celsius>(value),
Err(_) => ThermodynamicTemperature::new::<degree_celsius>(0.0),
}
}
pub fn disallow_cooling(&mut self) {
self.cooling_allowed = false;
}
pub fn allow_cooling(&mut self) {
self.cooling_allowed = true;
}
pub fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
pub fn allow_heating(&mut self) {
self.heating_allowed = true;
}
pub fn turn_cooling_on(&mut self) {
tracing::info!("turn_cooling_on");
self.cooling_relais.set(true);
self.cooling_controller.set(10.0);
self.temperature.cooling = true;
}
pub fn turn_cooling_off(&mut self) {
tracing::info!("turn_cooling_off");
self.cooling_relais.set(false);
self.temperature.cooling = false;
}
pub fn turn_heating_on(&mut self) {
tracing::info!("turn_heating_on");
self.heating_relais_1.set(true);
self.heating_relais_2.set(true);
self.temperature.heating = true;
}
pub fn turn_heating_off(&mut self) {
tracing::info!("turn_heating_off");
self.heating_relais_1.set(false);
self.heating_relais_2.set(false);
self.temperature.heating = false;
}
pub fn set_should_pump(&mut self, should_pump: bool) {
self.should_pump = should_pump;
}
pub fn get_should_pump(&mut self) -> bool {
self.should_pump
}
pub fn get_flow(&mut self) -> VolumeRate {
let value = match self.flow_sensor.get_frequency_value() {
Ok(val) => val,
Err(e) => {
tracing::debug!("Error calculating frequency: {}", e);
return VolumeRate::new::<liter_per_minute>(0.0);
}
};
match value {
Some(val) => {
if val == 0 {
return VolumeRate::new::<liter_per_minute>(0.0);
}
// Formula: f = 8.1*q - 3, so q = (f + 3) / 8.1
let actual_flow = ((val / 100) as f32 + 3.0) / 8.1;
VolumeRate::new::<liter_per_minute>(actual_flow.into())
}
None => {
return VolumeRate::new::<liter_per_minute>(0.0);
}
}
}
pub fn update(&mut self, now: Instant) -> () {
let current_flow = self.get_flow();
self.current_flow = current_flow;
self.flow.flow = current_flow;
let should_flow = self.get_should_pump();
self.flow.should_pump = should_flow;
if !self.flow.pump && self.get_pump() && should_flow {
self.turn_pump_on();
} else if self.flow.pump && (!self.get_pump() || !should_flow) {
self.turn_pump_off();
}
self.current_temperature = self.get_temp_in();
self.temp_reservoir = self.get_temp_out();
if self.current_temperature < self.min_temperature && self.temperature.cooling {
self.turn_cooling_off();
} else if self.current_temperature > self.max_temperature && self.temperature.heating {
self.turn_heating_off();
}
// Calculate PID error once
let error = self.target_temperature.get::<degree_celsius>()
- self.current_temperature.get::<degree_celsius>();
let control = self.pid.update(error, now);
self.temperature_pid_output = control;
let elapsed = now.duration_since(self.window_start);
if elapsed >= self.pwm_period {
self.window_start = now;
}
// Decide whether to heat or cool based on error
if error > 0.0 {
// Need heating (current < target)
if self.temperature.cooling {
self.turn_cooling_off();
}
if self.heating_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
// Only start heating if pump is on
let duty = control.clamp(0.0, 1.0);
let on_time = self.pwm_period.mul_f64(duty);
let on = elapsed < on_time;
if on && !self.temperature.heating {
self.turn_heating_on();
} else if !on && self.temperature.heating {
self.turn_heating_off();
}
} else {
// Pump is off or heating not allowed - don't heat
if self.temperature.heating {
self.turn_heating_off();
}
}
} else {
// Need cooling (current > target)
if self.temperature.heating {
self.turn_heating_off();
}
if self.cooling_allowed && !self.temperature.cooling {
self.turn_cooling_on();
}
}
}
}
+261
View File
@@ -0,0 +1,261 @@
use control_core::{
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use uom::si::{
f64::{ThermodynamicTemperature, VolumeRate},
thermodynamic_temperature::degree_celsius,
volume_rate::liter_per_minute,
};
use crate::machines::{
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
aquapath1::{
api::{
AquaPathV1Events, AquaPathV1Namespace, FlowState, FlowStates, LiveValuesEvent,
ModeState, StateEvent, TempState, TempStates,
},
controller::Controller,
// flow_controller::FlowController,
// temperature_controller::TemperatureController,
},
};
pub mod act;
pub mod api;
pub mod controller;
// pub mod flow_controller;
pub mod new;
// pub mod temperature_controller;
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub enum AquaPathV1Mode {
Standby,
Auto,
}
pub enum AquaPathSideType {
Front,
Back,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Temperature {
pub temperature: ThermodynamicTemperature,
pub cooling: bool,
pub heating: bool,
pub target_temperature: ThermodynamicTemperature,
}
impl Default for Temperature {
fn default() -> Self {
Self {
temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
cooling: false,
heating: false,
target_temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Flow {
pub flow: VolumeRate,
pub pump: bool,
pub should_pump: bool,
}
impl Default for Flow {
fn default() -> Self {
Self {
flow: VolumeRate::new::<liter_per_minute>(0.0),
pump: false,
should_pump: false,
}
}
}
#[derive(Debug, Machine)]
pub struct AquaPathV1 {
namespace: AquaPathV1Namespace,
mode: AquaPathV1Mode,
last_measurement_emit: Instant,
front_controller: Controller,
back_controller: Controller,
}
impl AquaPathV1 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_AQUAPATH_V1,
};
}
impl std::fmt::Display for AquaPathV1 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Aquapath")
}
}
impl AquaPathV1 {
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
front_temperature: self
.front_controller
.current_temperature
.get::<degree_celsius>(),
back_temperature: self
.back_controller
.current_temperature
.get::<degree_celsius>(),
front_flow: self.front_controller.current_flow.get::<liter_per_minute>(),
back_flow: self.back_controller.current_flow.get::<liter_per_minute>(),
front_temp_reservoir: self.front_controller.temp_reservoir.get::<degree_celsius>(),
back_temp_reservoir: self.back_controller.temp_reservoir.get::<degree_celsius>(),
};
let event = live_values.build();
self.namespace.emit(AquaPathV1Events::LiveValues(event));
}
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: false,
mode_state: ModeState {
mode: self.mode.clone(),
},
temperature_states: TempStates {
front: TempState {
temperature: self
.front_controller
.current_temperature
.get::<degree_celsius>(),
target_temperature: self
.front_controller
.target_temperature
.get::<degree_celsius>(),
},
back: TempState {
temperature: self
.back_controller
.current_temperature
.get::<degree_celsius>(),
target_temperature: self
.back_controller
.target_temperature
.get::<degree_celsius>(),
},
},
flow_states: FlowStates {
front: FlowState {
flow: self.front_controller.current_flow.get::<liter_per_minute>(),
should_flow: self.front_controller.should_pump,
},
back: FlowState {
flow: self.back_controller.current_flow.get::<liter_per_minute>(),
should_flow: self.back_controller.should_pump,
},
},
};
let event = state.build();
self.namespace.emit(AquaPathV1Events::State(event));
}
}
impl AquaPathV1 {
fn turn_cooling_off(&mut self) {
self.front_controller.disable_cooling();
self.back_controller.disable_cooling();
}
fn turn_cooling_on(&mut self) {
self.front_controller.allow_cooling();
self.back_controller.allow_cooling();
}
fn turn_heating_off(&mut self) {
self.front_controller.disallow_heating();
self.back_controller.disallow_heating();
}
fn turn_heating_on(&mut self) {
self.front_controller.allow_heating();
self.back_controller.allow_heating();
}
fn turn_pump_on(&mut self) {
self.front_controller.allow_pump();
self.back_controller.allow_pump();
}
fn turn_pump_off(&mut self) {
self.front_controller.disallow_pump();
self.back_controller.disallow_pump();
}
fn turn_off_all(&mut self) {
self.turn_cooling_off();
self.turn_heating_off();
self.turn_pump_off();
}
fn turn_on_all(&mut self) {
self.turn_cooling_on();
self.turn_heating_on();
self.turn_pump_on();
}
// Turn all OFF and do nothing
fn switch_to_standby(&mut self) {
match self.mode {
AquaPathV1Mode::Standby => (),
AquaPathV1Mode::Auto => self.turn_off_all(),
};
self.mode = AquaPathV1Mode::Standby;
}
fn switch_to_auto(&mut self) {
match self.mode {
AquaPathV1Mode::Auto => (),
AquaPathV1Mode::Standby => self.turn_on_all(),
}
self.mode = AquaPathV1Mode::Auto;
}
fn switch_mode(&mut self, mode: AquaPathV1Mode) {
if self.mode == mode {
return;
}
match mode {
AquaPathV1Mode::Standby => self.switch_to_standby(),
AquaPathV1Mode::Auto => self.switch_to_auto(),
}
}
}
impl AquaPathV1 {
fn set_mode_state(&mut self, mode: AquaPathV1Mode) {
self.switch_mode(mode.clone());
self.emit_state();
}
}
impl AquaPathV1 {
fn set_target_temperature(&mut self, temperature: f64, cooling_type: AquaPathSideType) {
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(temperature);
match cooling_type {
AquaPathSideType::Back => self.back_controller.set_target_temperature(target_temp),
AquaPathSideType::Front => self.front_controller.set_target_temperature(target_temp),
}
self.emit_state();
}
fn set_should_pump(&mut self, should_pump: bool, cooling_type: AquaPathSideType) {
match cooling_type {
AquaPathSideType::Back => self.back_controller.set_should_pump(should_pump),
AquaPathSideType::Front => self.front_controller.set_should_pump(should_pump),
}
self.emit_state();
}
}
+352
View File
@@ -0,0 +1,352 @@
use super::{AquaPathV1, AquaPathV1Mode};
use crate::machines::aquapath1::{
Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
};
use anyhow::Error;
use control_core::machines::{
identification::DeviceHardwareIdentification,
new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
},
};
use ethercat_hal::{
coe::ConfigurableDevice,
devices::{
EthercatDeviceUsed, downcast_device,
ek1100::{EK1100, EK1100_IDENTITY_A},
el2008::{EL2008, EL2008_IDENTITY_A, EL2008Port},
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
el4002::{EL4002, EL4002_IDENTITY_A, EL4002Port},
el5152::{
EL5152, EL5152_IDENTITY_A, EL5152Configuration, EL5152Port,
EL5152PredefinedPdoAssignment,
},
subdevice_identity_to_tuple,
},
io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, encoder_input::EncoderInput,
temperature_input::TemperatureInput,
},
};
use std::time::{Duration, Instant};
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
impl MachineNewTrait for AquaPathV1 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
smol::block_on(async {
// Role 0 - Buscoupler EK1100
{
let device_identification =
get_device_identification_by_role(params.device_group, 0)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EK1100_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EK1100>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not an EK1100",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
}
// Role 1 - EL2008 Digital Output Module
let el2008 = {
let device_identification =
get_device_identification_by_role(params.device_group, 1)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL2008_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL2008>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not an EL2008",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
// Role 2 - EL4002 Analog Output Module
let el4002 = {
let device_identification =
get_device_identification_by_role(params.device_group, 2)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not Ethercat",
module_path!()
))?,
};
let subdevice = get_subdevice_by_index(
hardware.subdevices,
device_hardware_identification_ethercat.subdevice_index,
)?;
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL4002_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL4002>(ethercat_device).await?
}
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not an EL4002",
module_path!()
))?,
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let el3204 = {
let device_identification =
get_device_identification_by_role(params.device_group, 3)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not Ethercat",
module_path!()
))?, //uncommented
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL3204_IDENTITY_A | EL3204_IDENTITY_B => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL3204>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not an EL3204",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let el5152 = {
let device_identification =
get_device_identification_by_role(params.device_group, 4)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL5152_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL5152>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not an EL5152",
module_path!()
));
}
};
let config = EL5152Configuration {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
device
.write()
.await
.write_config(&subdevice, &config)
.await?;
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let enc1 = EncoderInput::new(el5152.clone(), EL5152Port::ENC1);
let enc2 = EncoderInput::new(el5152.clone(), EL5152Port::ENC2);
//after heating
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
//in reservoir
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
//after heating
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
//in reservoir
let t4 = TemperatureInput::new(el3204.clone(), EL3204Port::T4);
//pump flow control
//phys 1
let do1 = DigitalOutput::new(el2008.clone(), EL2008Port::DO1);
//phys 5
let do2 = DigitalOutput::new(el2008.clone(), EL2008Port::DO2);
//heating
//phys 2
let do3 = DigitalOutput::new(el2008.clone(), EL2008Port::DO3);
//phys 6
let do4 = DigitalOutput::new(el2008.clone(), EL2008Port::DO4);
//phys 3
let do5 = DigitalOutput::new(el2008.clone(), EL2008Port::DO5);
//phys 7
let do6 = DigitalOutput::new(el2008.clone(), EL2008Port::DO6);
//cooling power cut
//phys 4
let do7 = DigitalOutput::new(el2008.clone(), EL2008Port::DO7);
//phys 8
let do8 = DigitalOutput::new(el2008.clone(), EL2008Port::DO8);
let ao1 = AnalogOutput::new(el4002.clone(), EL4002Port::AO1);
let ao2 = AnalogOutput::new(el4002.clone(), EL4002Port::AO2);
let front_controller = Controller::new(
0.10,
0.0,
0.015,
Duration::from_millis(500),
Temperature::default(),
ThermodynamicTemperature::new::<degree_celsius>(25.0),
ao1,
do7,
do3,
do5,
t1,
t2,
Flow::default(),
do1,
enc1,
);
let back_controller = Controller::new(
0.10,
0.0,
0.015,
Duration::from_millis(500),
Temperature::default(),
ThermodynamicTemperature::new::<degree_celsius>(25.0),
ao2,
do8,
do4,
do6,
t3,
t4,
Flow::default(),
do2,
enc2,
);
let mut water_cooling = Self {
namespace: AquaPathV1Namespace::new(params.socket_queue_tx.clone()),
mode: AquaPathV1Mode::Standby,
last_measurement_emit: Instant::now(),
front_controller,
back_controller,
};
water_cooling.emit_state();
Ok(water_cooling)
})
}
}
+3 -9
View File
@@ -7,17 +7,17 @@ use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEve
use buffer_tower_controller::BufferTowerController;
use control_core::{
machines::{
ConnectedMachine, ConnectedMachineData, Machine, downcast_machine,
ConnectedMachine, ConnectedMachineData, downcast_machine,
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use futures::executor::block_on;
use serde::{Deserialize, Serialize};
use smol::lock::{Mutex, RwLock};
use std::{
any::Any,
sync::{Arc, Weak},
time::Instant,
};
@@ -26,7 +26,7 @@ use crate::machines::{
MACHINE_BUFFER_V1, VENDOR_QITECH, buffer1::api::ConnectedMachineState, winder2::Winder2,
};
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct BufferV1 {
// controllers
pub buffer_tower_controller: BufferTowerController,
@@ -52,12 +52,6 @@ impl std::fmt::Display for BufferV1 {
}
}
impl Machine for BufferV1 {
fn as_any(&self) -> &dyn Any {
self
}
}
impl BufferV1 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
+5 -2
View File
@@ -1,8 +1,11 @@
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::MachineAct;
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
use crate::machines::extruder1::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
self.temperature_controller_back.update(now);
+19 -7
View File
@@ -1,4 +1,11 @@
use super::{ExtruderV2, ExtruderV2Mode, HeatingType, mitsubishi_cs80::MotorStatus};
use super::{ExtruderV2Mode, HeatingType, mitsubishi_cs80::MotorStatus};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV2;
#[cfg(feature = "mock-machine")]
use super::mock::ExtruderV2;
use control_core::{
machines::api::MachineApi,
socketio::{
@@ -23,11 +30,11 @@ use uom::si::{
#[derive(Debug, Clone, Default, Serialize)]
pub struct MotorStatusValues {
screw_rpm: f64, // rpm of motor
frequency: f64, // frequency of motor
voltage: f64, // volt used for motor
current: f64, // current used for the motor
power: f64, // power in watts
pub screw_rpm: f64, // rpm of motor
pub frequency: f64, // frequency of motor
pub voltage: f64, // volt used for motor
pub current: f64, // current used for the motor
pub power: f64, // power in watts
}
impl From<MotorStatus> for MotorStatusValues {
@@ -67,6 +74,10 @@ pub struct LiveValuesEvent {
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
/// total energy consumption in kWh
pub total_energy_kwh: f64,
}
impl LiveValuesEvent {
@@ -185,7 +196,7 @@ pub enum ExtruderV2Events {
}
#[derive(Deserialize, Serialize)]
enum Mutation {
pub enum Mutation {
/// INVERTER
/// Frequency Control
// Set Rotation also starts the motor
@@ -253,6 +264,7 @@ impl CacheableEvents<Self> for ExtruderV2Events {
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
+279
View File
@@ -0,0 +1,279 @@
#[cfg(not(feature = "mock-machine"))]
// Contains Implementations for All functions that use emit_state
use crate::machines::extruder1::{
ExtruderV2, ExtruderV2Mode, HeatingType,
api::{
ExtruderSettingsState, ExtruderV2Events, HeatingState, HeatingStates, InverterStatusState,
LiveValuesEvent, ModeState, PidSettings, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState, StateEvent,
},
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
};
use uom::si::{
angular_velocity::revolution_per_minute,
f64::{AngularVelocity, Pressure, ThermodynamicTemperature},
pressure::bar,
thermodynamic_temperature::degree_celsius,
};
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
mode_state: ModeState {
mode: self.mode.clone(),
},
regulation_state: RegulationState {
uses_rpm: self.screw_speed_controller.get_uses_rpm(),
},
pressure_state: PressureState {
target_bar: self
.screw_speed_controller
.get_target_pressure()
.get::<bar>(),
wiring_error: self.screw_speed_controller.get_wiring_error(),
},
screw_state: ScrewState {
target_rpm: self
.screw_speed_controller
.get_target_rpm()
.get::<revolution_per_minute>(),
},
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: self
.temperature_controller_nozzle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_nozzle.heating.wiring_error,
},
front: HeatingState {
target_temperature: self
.temperature_controller_front
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_front.heating.wiring_error,
},
back: HeatingState {
target_temperature: self
.temperature_controller_back
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_back.heating.wiring_error,
},
middle: HeatingState {
target_temperature: self
.temperature_controller_middle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_middle.heating.wiring_error,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: self
.screw_speed_controller
.get_nozzle_pressure_limit()
.get::<bar>(),
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
forward_running: self.screw_speed_controller.inverter.status.forward_running,
reverse_running: self.screw_speed_controller.inverter.status.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.status.su,
overload_warning: self.screw_speed_controller.inverter.status.ol,
no_function: self.screw_speed_controller.inverter.status.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.status.fu,
abc_fault: self.screw_speed_controller.inverter.status.abc_,
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
kp: self.screw_speed_controller.pid.get_kp(),
kd: self.screw_speed_controller.pid.get_kd(),
},
},
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let hash = hash_with_serde_model(self.screw_speed_controller.get_inverter_status());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
use std::time::Instant;
let now = Instant::now();
let combined_power = self.calculate_combined_power();
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
.temperature_controller_nozzle
.heating
.temperature
.get::<degree_celsius>(),
front_temperature: self
.temperature_controller_front
.heating
.temperature
.get::<degree_celsius>(),
back_temperature: self
.temperature_controller_back
.heating
.temperature
.get::<degree_celsius>(),
middle_temperature: self
.temperature_controller_middle
.heating
.temperature
.get::<degree_celsius>(),
nozzle_power: self
.temperature_controller_nozzle
.get_heating_element_wattage(),
front_power: self
.temperature_controller_front
.get_heating_element_wattage(),
back_power: self
.temperature_controller_back
.get_heating_element_wattage(),
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
.set_nozzle_pressure_limit_is_enabled(enabled);
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
use uom::si::pressure::bar;
self.screw_speed_controller
.set_nozzle_pressure_limit(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.temperature_controller_back.allow_heating();
self.temperature_controller_front.allow_heating();
self.temperature_controller_middle.allow_heating();
self.temperature_controller_nozzle.allow_heating();
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.screw_speed_controller.set_rotation_direction(forward);
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.switch_mode(mode);
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller
.set_target_screw_rpm(self.screw_speed_controller.target_rpm);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
if self.screw_speed_controller.get_uses_rpm() && !uses_rpm {
self.screw_speed_controller.set_uses_rpm(uses_rpm);
self.screw_speed_controller.start_pressure_regulation();
}
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.screw_speed_controller
.set_target_pressure(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
use uom::si::angular_velocity::revolution_per_minute;
self.screw_speed_controller
.set_target_screw_rpm(AngularVelocity::new::<revolution_per_minute>(rpm));
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(target_temperature);
match heating_type {
HeatingType::Nozzle => self
.temperature_controller_nozzle
.set_target_temperature(target_temp),
HeatingType::Front => self
.temperature_controller_front
.set_target_temperature(target_temp),
HeatingType::Back => self
.temperature_controller_back
.set_target_temperature(target_temp),
HeatingType::Middle => self
.temperature_controller_middle
.set_target_temperature(target_temp),
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
.configure(settings.ki, settings.kp, settings.kd);
self.emit_state();
}
}
@@ -4,6 +4,7 @@ use control_core::modbus::{
modbus_serial_interface::ModbusSerialInterface,
};
use ethercat_hal::io::serial_interface::SerialInterface;
use serde::Serialize;
use std::time::{Duration, Instant};
use uom::si::{
electric_current::centiampere,
@@ -260,7 +261,8 @@ impl From<MitsubishiCS80Requests> for MitsubishiCS80Request {
}
}
#[derive(Debug, Default)]
// Serialize is needed so we can hash it
#[derive(Debug, Default, Clone, Copy, Serialize)]
pub struct MitsubishiCS80Status {
pub running: bool,
pub forward_running: bool,
+15
View File
@@ -0,0 +1,15 @@
use crate::machines::extruder1::mock::ExtruderV2;
use control_core::machines::new::MachineAct;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use crate::machines::extruder1::{HeatingType, api::Mutation, mock::ExtruderV2};
use control_core::machines::api::MachineApi;
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => (),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
Mutation::SetMiddleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Middle)
}
Mutation::SetBackHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
}
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> &mut control_core::socketio::namespace::Namespace {
&mut self.namespace.namespace
}
}
@@ -0,0 +1,136 @@
use crate::machines::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent},
mock::ExtruderV2,
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
};
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
// bad performance wise, but doesnt matter its only a mock machine
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
regulation_state: self.regulation_state.clone(),
pressure_state: self.pressure_state.clone(),
screw_state: self.screw_state.clone(),
heating_states: self.heating_states.clone(),
extruder_settings_state: self.extruder_settings_state.clone(),
inverter_status_state: self.inverter_status_state.clone(),
pid_settings: self.pid_settings.clone(),
}
}
}
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let hash = hash_with_serde_model(self.inverter_status_state.clone());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.inverter_status_state.clone();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
motor_status: self.motor_status.clone(),
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
front_temperature: self.front_temperature,
back_temperature: self.back_temperature,
middle_temperature: self.middle_temperature,
nozzle_power: self.nozzle_power,
front_power: self.front_power,
back_power: self.back_power,
middle_power: self.middle_power,
combined_power: self.combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.extruder_settings_state.pressure_limit_enabled = enabled;
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.extruder_settings_state.pressure_limit = pressure;
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.back_heating_allowed = true;
self.front_heating_allowed = true;
self.nozzle_heating_allowed = true;
self.back_heating_allowed = true;
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.rotation_state.forward = forward;
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.mode_state = ModeState { mode };
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
self.regulation_state.uses_rpm = uses_rpm;
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.target_pressure = pressure;
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_state.target_rpm = rpm;
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
match heating_type {
HeatingType::Nozzle => {
self.heating_states.nozzle.target_temperature = target_temperature
}
HeatingType::Front => self.heating_states.front.target_temperature = target_temperature,
HeatingType::Back => self.heating_states.back.target_temperature = target_temperature,
HeatingType::Middle => {
self.heating_states.middle.target_temperature = target_temperature
}
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.pid_settings.pressure.ki = settings.ki;
self.pid_settings.pressure.kp = settings.kp;
self.pid_settings.pressure.kd = settings.kd;
self.emit_state();
}
}
+103
View File
@@ -0,0 +1,103 @@
#[cfg(feature = "mock-machine")]
use control_core::machines::identification::MachineIdentification;
#[cfg(feature = "mock-machine")]
use control_core_derive::Machine;
#[cfg(feature = "mock-machine")]
use std::time::Instant;
#[cfg(feature = "mock-machine")]
use crate::machines::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingStates, InverterStatusState,
ModeState, MotorStatusValues, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState,
},
},
};
// Just checking mock-machine feature here to exclude these modules from compilation entirely
#[cfg(feature = "mock-machine")]
pub mod act;
#[cfg(feature = "mock-machine")]
pub mod api;
#[cfg(feature = "mock-machine")]
pub mod mock_emit;
#[cfg(feature = "mock-machine")]
pub mod new;
#[cfg(feature = "mock-machine")]
#[derive(Debug, Machine)]
pub struct ExtruderV2 {
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
/// Energy tracking for total consumption calculation
pub total_energy_kwh: f64,
pub last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
pub target_pressure: f64,
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
pub mode_state: ModeState,
/// regulation state
pub regulation_state: RegulationState,
/// pressure state
pub pressure_state: PressureState,
/// screw state
pub screw_state: ScrewState,
/// heating states
pub heating_states: HeatingStates,
/// extruder settings state
pub extruder_settings_state: ExtruderSettingsState,
/// inverter status state
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
pub motor_status: MotorStatusValues,
/// pressure in bar
pub pressure: f64,
/// nozzle temperature in celsius
pub nozzle_temperature: f64,
/// front temperature in celsius
pub front_temperature: f64,
/// back temperature in celsius
pub back_temperature: f64,
/// middle temperature in celsius
pub middle_temperature: f64,
/// nozzle heating power in watts
pub nozzle_power: f64,
/// front heating power in watts
pub front_power: f64,
/// back heating power in watts
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
pub nozzle_heating_allowed: bool,
pub front_heating_allowed: bool,
pub back_heating_allowed: bool,
pub middle_heating_allowed: bool,
}
#[cfg(feature = "mock-machine")]
impl ExtruderV2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_EXTRUDER_V1,
};
}
+127
View File
@@ -0,0 +1,127 @@
use crate::machines::extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState,
},
mock::ExtruderV2,
};
impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let mut extruder_mock_machine = Self {
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: now,
mode: ExtruderV2Mode::Standby, // Start in standby mode
emitted_default_state: false,
last_status_hash: None,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
is_default_state: false,
rotation_state: RotationState { forward: false },
mode_state: ModeState {
mode: ExtruderV2Mode::Standby,
},
regulation_state: RegulationState { uses_rpm: true },
pressure_state: PressureState {
target_bar: 0.0,
wiring_error: false,
},
screw_state: ScrewState { target_rpm: 0.0 },
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
front: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
back: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
middle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
},
inverter_status_state: InverterStatusState {
running: false,
forward_running: true,
reverse_running: true,
up_to_frequency: false,
overload_warning: false,
no_function: false,
output_frequency_detection: false,
abc_fault: false,
fault_occurence: false,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
},
pressure: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
},
},
motor_status: MotorStatusValues {
screw_rpm: 0.0,
frequency: 0.0,
voltage: 0.0,
current: 0.0,
power: 0.0,
},
pressure: 0.0,
nozzle_temperature: 0.0,
front_temperature: 0.0,
back_temperature: 0.0,
middle_temperature: 0.0,
nozzle_power: 0.0,
front_power: 0.0,
back_power: 0.0,
middle_power: 0.0,
combined_power: 0.0,
nozzle_heating_allowed: false,
front_heating_allowed: false,
back_heating_allowed: false,
middle_heating_allowed: false,
target_pressure: 0.0,
};
extruder_mock_machine.emit_state();
Ok(extruder_mock_machine)
}
}
+47 -277
View File
@@ -1,28 +1,20 @@
use crate::machines::{MACHINE_EXTRUDER_V1, VENDOR_QITECH};
use api::{
ExtruderSettingsState, ExtruderV2Events, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, LiveValuesEvent, ModeState, PidSettings, PidSettingsStates, PressureState,
RegulationState, RotationState, ScrewState, StateEvent,
};
use control_core::helpers::hasher_serializer::check_hash_different;
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
};
use api::ExtruderV2Namespace;
use control_core::machines::identification::MachineIdentification;
use control_core_derive::Machine;
use screw_speed_controller::ScrewSpeedController;
use serde::{Deserialize, Serialize};
use std::{any::Any, time::Instant};
use std::time::Instant;
use temperature_controller::TemperatureController;
use uom::si::{
angular_velocity::{AngularVelocity, revolution_per_minute},
f64::{Pressure, ThermodynamicTemperature},
pressure::bar,
electric_current::ampere, electric_potential::volt, f64::ThermodynamicTemperature,
thermodynamic_temperature::degree_celsius,
};
pub mod act;
pub mod api;
pub mod emit;
pub mod mitsubishi_cs80;
pub mod mock;
pub mod new;
pub mod screw_speed_controller;
pub mod temperature_controller;
@@ -60,11 +52,12 @@ pub enum HeatingType {
Middle,
}
#[derive(Debug)]
#[derive(Debug, Machine)]
#[cfg(not(feature = "mock-machine"))]
pub struct ExtruderV2 {
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_state_event: Option<StateEvent>,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
screw_speed_controller: ScrewSpeedController,
temperature_controller_front: TemperatureController,
@@ -72,135 +65,23 @@ pub struct ExtruderV2 {
temperature_controller_back: TemperatureController,
temperature_controller_nozzle: TemperatureController,
/// Energy tracking for total consumption calculation
total_energy_kwh: f64,
last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
mode_state: ModeState {
mode: self.mode.clone(),
},
regulation_state: RegulationState {
uses_rpm: self.screw_speed_controller.get_uses_rpm(),
},
pressure_state: PressureState {
target_bar: self
.screw_speed_controller
.get_target_pressure()
.get::<bar>(),
wiring_error: self.screw_speed_controller.get_wiring_error(),
},
screw_state: ScrewState {
target_rpm: self
.screw_speed_controller
.get_target_rpm()
.get::<revolution_per_minute>(),
},
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: self
.temperature_controller_nozzle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_nozzle.heating.wiring_error,
},
front: HeatingState {
target_temperature: self
.temperature_controller_front
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_front.heating.wiring_error,
},
back: HeatingState {
target_temperature: self
.temperature_controller_back
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_back.heating.wiring_error,
},
middle: HeatingState {
target_temperature: self
.temperature_controller_middle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_middle.heating.wiring_error,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: self
.screw_speed_controller
.get_nozzle_pressure_limit()
.get::<bar>(),
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
forward_running: self.screw_speed_controller.inverter.status.forward_running,
reverse_running: self.screw_speed_controller.inverter.status.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.status.su,
overload_warning: self.screw_speed_controller.inverter.status.ol,
no_function: self.screw_speed_controller.inverter.status.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.status.fu,
abc_fault: self.screw_speed_controller.inverter.status.abc_,
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
ki: 0.0, // TODO: Add temperature PID settings when available
kp: 0.0,
kd: 0.0,
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
kp: self.screw_speed_controller.pid.get_kp(),
kd: self.screw_speed_controller.pid.get_kd(),
},
},
}
}
pub fn maybe_emit_state_event(&mut self) {
let new_state = self.build_state_event();
let old_state = match &self.last_state_event {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_state);
if should_emit {
self.namespace
.emit(ExtruderV2Events::State(new_state.build()));
self.last_state_event = Some(new_state);
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for ExtruderV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ExtruderV2")
}
}
impl Machine for ExtruderV2 {
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -208,73 +89,43 @@ impl ExtruderV2 {
};
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
.temperature_controller_nozzle
.heating
.temperature
.get::<degree_celsius>(),
front_temperature: self
.temperature_controller_front
.heating
.temperature
.get::<degree_celsius>(),
back_temperature: self
.temperature_controller_back
.heating
.temperature
.get::<degree_celsius>(),
middle_temperature: self
.temperature_controller_middle
.heating
.temperature
.get::<degree_celsius>(),
nozzle_power: self
.temperature_controller_nozzle
.get_heating_element_wattage(),
front_power: self
.temperature_controller_front
.get_heating_element_wattage(),
back_power: self
.temperature_controller_back
.get_heating_element_wattage(),
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
/// Calculate combined power consumption in watts
fn calculate_combined_power(&mut self) -> f64 {
let motor_power = {
let motor_status = &self.screw_speed_controller.inverter.motor_status;
let voltage = motor_status.voltage.get::<volt>();
let current = motor_status.current.get::<ampere>();
voltage * current
};
let nozzle_power = self
.temperature_controller_nozzle
.get_heating_element_wattage();
let front_power = self
.temperature_controller_front
.get_heating_element_wattage();
let back_power = self
.temperature_controller_back
.get_heating_element_wattage();
let middle_power = self
.temperature_controller_middle
.get_heating_element_wattage();
let event = live_values.build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
motor_power + nozzle_power + front_power + back_power + middle_power
}
pub fn emit_state(&mut self) {
let state = self.build_state_event();
self.last_state_event = Some(state.clone());
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
/// Update total energy consumption in kWh
fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) {
if let Some(last_time) = self.last_energy_calculation_time {
let time_delta_hours = now.duration_since(last_time).as_secs_f64() / 3600.0;
let energy_delta_kwh = (current_power_watts / 1000.0) * time_delta_hours;
self.total_energy_kwh += energy_delta_kwh;
}
self.last_energy_calculation_time = Some(now);
}
// Extruder Settings Api Impl
const fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
.set_nozzle_pressure_limit_is_enabled(enabled);
}
/// pressure is represented as bar
fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
let nozzle_pressure_limit = Pressure::new::<bar>(pressure);
self.screw_speed_controller
.set_nozzle_pressure_limit(nozzle_pressure_limit);
}
}
impl ExtruderV2 {
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
// Funktionen ohne emit_state bleiben hier
fn turn_heating_off(&mut self) {
self.temperature_controller_back.disable();
self.temperature_controller_front.disable();
@@ -282,14 +133,6 @@ impl ExtruderV2 {
self.temperature_controller_nozzle.disable();
}
const fn enable_heating(&mut self) {
self.temperature_controller_back.allow_heating();
self.temperature_controller_front.allow_heating();
self.temperature_controller_middle.allow_heating();
self.temperature_controller_nozzle.allow_heating();
}
// Turn heating OFF and do nothing
fn switch_to_standby(&mut self) {
match self.mode {
ExtruderV2Mode::Standby => (),
@@ -306,9 +149,7 @@ impl ExtruderV2 {
self.mode = ExtruderV2Mode::Standby;
}
// turn off motor if on and keep heating on
fn switch_to_heat(&mut self) {
// From what mode are we transitioning ?
match self.mode {
ExtruderV2Mode::Standby => self.enable_heating(),
ExtruderV2Mode::Heat => (),
@@ -320,7 +161,6 @@ impl ExtruderV2 {
self.mode = ExtruderV2Mode::Heat;
}
// keep heating on, and turn motor on
fn switch_to_extrude(&mut self) {
match self.mode {
ExtruderV2Mode::Standby => {
@@ -333,7 +173,7 @@ impl ExtruderV2 {
self.enable_heating();
self.screw_speed_controller.reset_pid();
}
ExtruderV2Mode::Extrude => (), // Do nothing, we are already extruding
ExtruderV2Mode::Extrude => (),
}
self.mode = ExtruderV2Mode::Extrude;
}
@@ -349,78 +189,8 @@ impl ExtruderV2 {
ExtruderV2Mode::Extrude => self.switch_to_extrude(),
}
}
}
impl ExtruderV2 {
fn set_rotation_state(&mut self, forward: bool) {
self.screw_speed_controller.set_rotation_direction(forward);
}
fn reset_inverter(&mut self) {
self.screw_speed_controller.inverter.reset_inverter();
}
fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.switch_mode(mode);
}
}
// Motor
impl ExtruderV2 {
fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller
.set_target_screw_rpm(self.screw_speed_controller.target_rpm);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
if self.screw_speed_controller.get_uses_rpm() && !uses_rpm {
self.screw_speed_controller.set_uses_rpm(uses_rpm);
self.screw_speed_controller.start_pressure_regulation();
}
}
fn set_target_pressure(&mut self, pressure: f64) {
let pressure = Pressure::new::<bar>(pressure);
self.screw_speed_controller.set_target_pressure(pressure);
}
fn set_target_rpm(&mut self, rpm: f64) {
let revolution_per_minutes = AngularVelocity::new::<revolution_per_minute>(rpm);
self.screw_speed_controller
.set_target_screw_rpm(revolution_per_minutes);
}
}
// Heating
impl ExtruderV2 {
fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(target_temperature);
match heating_type {
HeatingType::Nozzle => self
.temperature_controller_nozzle
.set_target_temperature(target_temp),
HeatingType::Front => self
.temperature_controller_front
.set_target_temperature(target_temp),
HeatingType::Back => self
.temperature_controller_back
.set_target_temperature(target_temp),
HeatingType::Middle => self
.temperature_controller_middle
.set_target_temperature(target_temp),
}
}
}
impl ExtruderV2 {
const fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
}
+5 -1
View File
@@ -1,3 +1,4 @@
#[cfg(not(feature = "mock-machine"))]
use super::{
ExtruderV2, ExtruderV2Mode, Heating, api::ExtruderV2Namespace, mitsubishi_cs80::MitsubishiCS80,
screw_speed_controller::ScrewSpeedController,
@@ -40,6 +41,7 @@ use uom::si::{
thermodynamic_temperature::degree_celsius,
};
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for ExtruderV2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
@@ -391,13 +393,15 @@ impl MachineNewTrait for ExtruderV2 {
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: Instant::now(),
mode: ExtruderV2Mode::Standby,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
temperature_controller_front,
temperature_controller_middle,
temperature_controller_back,
temperature_controller_nozzle,
screw_speed_controller,
emitted_default_state: false,
last_state_event: None,
last_status_hash: None,
};
extruder.emit_state();
Ok(extruder)
@@ -15,6 +15,8 @@ use uom::si::{
pressure::bar,
};
use crate::machines::extruder1::mitsubishi_cs80::MitsubishiCS80Status;
use super::mitsubishi_cs80::{MitsubishiCS80, MotorStatus};
#[derive(Debug)]
@@ -64,6 +66,10 @@ impl ScrewSpeedController {
}
}
pub const fn get_inverter_status(&mut self) -> MitsubishiCS80Status {
self.inverter.status
}
pub const fn get_motor_enabled(&mut self) -> bool {
self.motor_on
}
+1 -1
View File
@@ -19,9 +19,9 @@ use std::time::{Duration, Instant};
///
impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
self.update();
// more than 33ms have passed since last emit (30 "fps" target)
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;
}
+3
View File
@@ -21,6 +21,9 @@ use tracing::instrument;
pub struct LiveValuesEvent {
/// diameter measurement in mm
pub diameter: f64,
pub x_diameter: Option<f64>,
pub y_diameter: Option<f64>,
pub roundness: Option<f64>,
}
impl LiveValuesEvent {
+67 -39
View File
@@ -4,19 +4,18 @@ use crate::{
};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::{
helpers::hasher_serializer::check_hash_different,
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use smol::lock::RwLock;
use std::{any::Any, sync::Arc, time::Instant};
use std::{sync::Arc, time::Instant};
use uom::si::{f64::Length, length::millimeter};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct LaserMachine {
// drivers
laser: Arc<RwLock<Laser>>,
@@ -25,19 +24,18 @@ pub struct LaserMachine {
namespace: LaserMachineNamespace,
last_measurement_emit: Instant,
// laser values
diameter: Length,
x_diameter: Option<Length>,
y_diameter: Option<Length>,
roundness: Option<f64>,
//laser target configuration
laser_target: LaserTarget,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
last_state_event: Option<StateEvent>,
}
impl Machine for LaserMachine {
fn as_any(&self) -> &dyn Any {
self
}
}
impl LaserMachine {
@@ -50,16 +48,16 @@ impl LaserMachine {
impl LaserMachine {
///diameter in mm
pub fn emit_live_values(&mut self) {
let diameter = smol::block_on(async {
self.laser
.read()
.await
.get_data()
.await
.map(|laser_data| laser_data.diameter.get::<millimeter>())
});
let diameter = self.diameter.get::<millimeter>();
let x_diameter = self.x_diameter.map(|x| x.get::<millimeter>());
let y_diameter = self.y_diameter.map(|y| y.get::<millimeter>());
let roundness = self.roundness;
let live_values = LiveValuesEvent {
diameter: diameter.unwrap_or(0.0),
diameter,
x_diameter,
y_diameter,
roundness,
};
self.namespace
.emit(LaserEvents::LiveValues(live_values.build()));
@@ -78,24 +76,6 @@ impl LaserMachine {
}
}
pub fn maybe_emit_state_event(&mut self) {
let new_state: StateEvent = self.build_state_event();
let old_state: &StateEvent = match &self.last_state_event {
Some(old_state) => old_state,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_state);
if should_emit {
let event = &new_state.build();
self.last_state_event = Some(new_state);
self.namespace.emit(LaserEvents::State(event.clone()));
}
}
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
@@ -111,16 +91,64 @@ impl LaserMachine {
pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) {
self.laser_target.higher_tolerance = Length::new::<millimeter>(higher_tolerance);
self.emit_state();
}
pub fn set_lower_tolerance(&mut self, lower_tolerance: f64) {
self.laser_target.lower_tolerance = Length::new::<millimeter>(lower_tolerance);
self.emit_state();
}
pub fn set_target_diameter(&mut self, target_diameter: f64) {
self.laser_target.diameter = Length::new::<millimeter>(target_diameter);
self.emit_state();
}
///
/// Roundness = min(x, y) / max(x, y)
///
fn calculate_roundness(&mut self) -> Option<f64> {
match (self.x_diameter, self.y_diameter) {
(Some(x), Some(y)) => {
let x_val = x.get::<millimeter>();
let y_val = y.get::<millimeter>();
if x_val > 0.0 && y_val > 0.0 {
let roundness = f64::min(x_val, y_val) / f64::max(x_val, y_val);
Some(roundness)
} else if x_val == 0.0 && y_val == 0.0 {
Some(0.0)
} else {
None
}
}
_ => None,
}
}
pub fn update(&mut self) {
let laser_data = smol::block_on(async { self.laser.read().await.get_data().await });
self.diameter = Length::new::<millimeter>(
laser_data
.as_ref()
.map(|data| data.diameter.get::<millimeter>())
.unwrap_or(0.0),
);
self.x_diameter = laser_data
.as_ref()
.and_then(|data| data.x_axis.as_ref())
.cloned();
self.y_diameter = laser_data
.as_ref()
.and_then(|data| data.y_axis.as_ref())
.cloned();
self.roundness = self.calculate_roundness();
}
}
#[derive(Debug, Clone)]
pub struct LaserTarget {
diameter: Length,
+8 -2
View File
@@ -5,7 +5,10 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
use anyhow::Error;
use control_core::machines::new::MachineNewTrait;
use uom::si::{f64::Length, length::millimeter};
use uom::{
ConstZero,
si::{f64::Length, length::millimeter},
};
impl MachineNewTrait for LaserMachine {
fn new(
@@ -39,7 +42,10 @@ impl MachineNewTrait for LaserMachine {
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
last_state_event: None,
diameter: Length::ZERO,
x_diameter: None,
y_diameter: None,
roundness: None,
};
// Emit initial state
-1
View File
@@ -24,7 +24,6 @@ impl MachineAct for MockMachine {
// Only emit live values if machine is in Running mode
// The live values are updated approximately 30 times per second
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;
}
+8 -36
View File
@@ -2,10 +2,9 @@ use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
use api::{LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, StateEvent};
use control_core::socketio::event::BuildEvent;
use control_core::{
helpers::hasher_serializer::check_hash_different,
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use std::time::Instant;
use tracing::info;
@@ -18,7 +17,7 @@ pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct MockMachine {
// socketio
namespace: MockMachineNamespace,
@@ -39,11 +38,6 @@ pub struct MockMachine {
emitted_default_state: bool,
}
impl Machine for MockMachine {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl MockMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -103,47 +97,25 @@ impl MockMachine {
self.last_emitted_event = Some(current_state);
}
pub fn maybe_emit_state_event(&mut self) {
let new_state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
frequency1: self.frequency1.get::<millihertz>(),
frequency2: self.frequency2.get::<millihertz>(),
frequency3: self.frequency3.get::<millihertz>(),
mode_state: ModeState {
mode: self.mode.clone(),
},
};
let old_event = match &self.last_emitted_event {
Some(old_event) => old_event,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_event);
if should_emit {
self.last_emitted_event = Some(new_state.clone());
self.namespace.emit(MockEvents::State(new_state.build()));
}
}
/// Set the frequencies of the sine waves
pub fn set_frequency1(&mut self, frequency_mhz: f64) {
self.frequency1 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
pub fn set_frequency2(&mut self, frequency_mhz: f64) {
self.frequency2 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
pub fn set_frequency3(&mut self, frequency_mhz: f64) {
self.frequency3 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
/// Set the mode of the mock machine
pub const fn set_mode(&mut self, mode: Mode) {
pub fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
self.emit_state();
}
}
+2
View File
@@ -1,3 +1,4 @@
pub mod aquapath1;
pub mod buffer1;
pub mod extruder1;
pub mod laser;
@@ -11,3 +12,4 @@ 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 MACHINE_AQUAPATH_V1: u16 = 0x0009;
+9 -2
View File
@@ -1,6 +1,11 @@
#[cfg(feature = "mock-machine")]
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::ExtruderV2;
use crate::machines::{
buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine, mock::MockMachine,
winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, laser::LaserMachine, winder2::Winder2,
};
use control_core::machines::registry::MachineRegistry;
use lazy_static::lazy_static;
@@ -11,8 +16,10 @@ lazy_static! {
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc
};
}
+4 -1
View File
@@ -16,9 +16,12 @@ impl MachineAct for Winder2 {
// automatically stops or pulls after N Meters if enabled
self.stop_or_pull_spool(now);
if self.traverse_controller.did_change_state() {
self.emit_state();
}
// more than 33ms have passed since last emit (30 "fps" target)
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;
}
@@ -39,8 +39,8 @@ pub struct AdaptiveSpoolSpeedController {
filament_calc: FilamentTensionCalculator,
/// Moving window of recent speeds (in rad/s) used for dynamic acceleration limit calculation
speed_time_window: MovingTimeWindow<f64>,
/// Estimated diameter in cm
radius: Length,
/// Factor to control/calculate the feed speed based on filament tension
speed_factor: Length,
/// Timestamp of last max speed factor update, used for time-aware learning rate calculation
last_max_speed_factor_update: Option<Instant>,
@@ -87,9 +87,9 @@ impl AdaptiveSpoolSpeedController {
/// Proportional control gain for adaptive learning (negative: higher tension reduces speed)
const RADIUS_LEARNING_RATE: f64 = 0.5;
const RADIUS_MIN: f64 = 4.25;
const FACTOR_MIN: f64 = 4.25;
const RADIUS_MAX: f64 = 20.0;
const FACTOR_MAX: f64 = 20.0;
/// if the tension is the lowest, the speed can be up to 2x the puller speed
const MAX_SPEED_MULTIPLIER: f64 = 4.0;
@@ -130,7 +130,7 @@ impl AdaptiveSpoolSpeedController {
std::time::Duration::from_secs(Self::SPEED_WINDOW_DURATION_SECS),
Self::SPEED_WINDOW_MAX_SAMPLES,
),
radius: Length::new::<centimeter>(4.25),
speed_factor: Length::new::<centimeter>(4.25),
last_max_speed_factor_update: None,
tension_target: Self::TENSION_TARGET,
radius_learning_rate: Self::RADIUS_LEARNING_RATE,
@@ -162,7 +162,7 @@ impl AdaptiveSpoolSpeedController {
fn get_max_speed(&self, puller_speed_controller: &PullerSpeedController) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
(puller_speed_controller.last_speed.get::<meter_per_second>()
/ self.radius.get::<meter>())
/ self.speed_factor.get::<meter>())
* self.max_speed_multiplier,
)
}
@@ -205,7 +205,7 @@ impl AdaptiveSpoolSpeedController {
// 0.0 means minimum tension (low angle, high speed)
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
self.update_radius(filament_tension, t);
self.update_speed_factor(filament_tension, t);
// Calculate speed based on inverted tension (lower tension = higher speed)
@@ -281,7 +281,7 @@ impl AdaptiveSpoolSpeedController {
///
/// # Returns
/// Speed guaranteed to be within safe operational limits
fn clamp_speed(&mut self, speed: AngularVelocity) -> AngularVelocity {
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::SAFETY_MAX_SPEED_RPM);
@@ -297,7 +297,7 @@ impl AdaptiveSpoolSpeedController {
/// # Parameters
/// - `filament_tension`: Normalized tension value
/// - `t`: Current timestamp for time-aware learning rate
fn update_radius(&mut self, filament_tension: f64, t: Instant) {
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant) {
let delta_t = match self.last_max_speed_factor_update {
Some(last_update) => t.duration_since(last_update).as_secs_f64(),
None => {
@@ -313,13 +313,13 @@ impl AdaptiveSpoolSpeedController {
// Calculate proportional control adjustment
let proportional_gain = self.radius_learning_rate * delta_t;
let radius_change = tension_error * proportional_gain;
let factor_change = tension_error * proportional_gain;
// Update the speed factor directly
let new_radius = (self.radius.get::<centimeter>() + radius_change)
.clamp(Self::RADIUS_MIN, Self::RADIUS_MAX);
let new_factor = (self.speed_factor.get::<centimeter>() + factor_change)
.clamp(Self::FACTOR_MIN, Self::FACTOR_MAX);
self.radius = Length::new::<centimeter>(new_radius); // Convert to cm
self.speed_factor = Length::new::<centimeter>(new_factor); // Convert to cm
self.last_max_speed_factor_update = Some(t);
}
@@ -385,7 +385,7 @@ impl AdaptiveSpoolSpeedController {
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
self.radius = Length::new::<centimeter>(4.25);
self.speed_factor = Length::new::<centimeter>(4.25);
self.last_max_speed_factor_update = None;
self.tension_target = Self::TENSION_TARGET;
self.radius_learning_rate = Self::RADIUS_LEARNING_RATE;
@@ -414,8 +414,8 @@ impl AdaptiveSpoolSpeedController {
self.acceleration_controller.reset(speed);
}
pub fn get_radius(&self) -> Length {
self.radius
pub fn get_speed_factor(&self) -> Length {
self.speed_factor
}
// Getters and setters for the new configurable parameters
-2
View File
@@ -113,8 +113,6 @@ pub struct LiveValuesEvent {
pub puller_speed: f64,
/// spool rpm
pub spool_rpm: f64,
/// spool diameter in mm
pub spool_diameter: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more