Compare commits
158
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71de39e871 | ||
|
|
54ffa193c3 | ||
|
|
03164f1ceb | ||
|
|
f67a46ab4c | ||
|
|
610790a778 | ||
|
|
6eaf451ad4 | ||
|
|
022e0adae3 | ||
|
|
6e6d4b31e7 | ||
|
|
0dafad3fb5 | ||
|
|
534de55239 | ||
|
|
d9f92bcdf2 | ||
|
|
1570299e38 | ||
|
|
c95f6f3bd9 | ||
|
|
9fcde2a234 | ||
|
|
a2ffc41e05 | ||
|
|
ff13baf257 | ||
|
|
8f5df07c19 | ||
|
|
299b38515a | ||
|
|
2b073aeb76 | ||
|
|
7a6f92f8a4 | ||
|
|
2e283ea95f | ||
|
|
829bb5ea1c | ||
|
|
0be18aaed0 | ||
|
|
ffe0f09061 | ||
|
|
730d61a18d | ||
|
|
fc07525976 | ||
|
|
c48953aa6b | ||
|
|
0c948b436d | ||
|
|
1b3f92dd1c | ||
|
|
c29c549a1c | ||
|
|
17622cacde | ||
|
|
0b423454ec | ||
|
|
81ec98f670 | ||
|
|
1aed425544 | ||
|
|
da080ace68 | ||
|
|
ded8dd2d2d | ||
|
|
edd83ba559 | ||
|
|
7d1d8559a5 | ||
|
|
f0bb9bfed1 | ||
|
|
c6ea057736 | ||
|
|
90a00198a7 | ||
|
|
f8241547fe | ||
|
|
1cb5d51c13 | ||
|
|
c42cdf8db1 | ||
|
|
69014ef8c6 | ||
|
|
5faab29a2b | ||
|
|
9a8b16ca69 | ||
|
|
53f3c66214 | ||
|
|
432bf0d6ba | ||
|
|
f3990f35f1 | ||
|
|
6f17412363 | ||
|
|
68907b9750 | ||
|
|
e7007e1980 | ||
|
|
3bcd96d73c | ||
|
|
8a77c62a5e | ||
|
|
93add82ab5 | ||
|
|
690b1f7ab3 | ||
|
|
3c07b94e3a | ||
|
|
3b63b55859 | ||
|
|
f87982c465 | ||
|
|
b03cedb55c | ||
|
|
f5934efa6c | ||
|
|
186619cd48 | ||
|
|
583d4c1a4d | ||
|
|
63bee54948 | ||
|
|
1523d64ed7 | ||
|
|
48fdde61dc | ||
|
|
7bc700ac5f | ||
|
|
bdd6147f92 | ||
|
|
f7f65bdaaa | ||
|
|
232b0782ca | ||
|
|
c3c36a26b6 | ||
|
|
7f22bdfbf2 | ||
|
|
ff4c579f7b | ||
|
|
9683882775 | ||
|
|
893e01532d | ||
|
|
713f78fa4e | ||
|
|
f444347fb9 | ||
|
|
ea03c0f755 | ||
|
|
e283a0f164 | ||
|
|
6820206d78 | ||
|
|
5069ee425c | ||
|
|
f93f342508 | ||
|
|
c6d3563101 | ||
|
|
f2f893c917 | ||
|
|
bbbca4085d | ||
|
|
4fdf461520 | ||
|
|
dcfc6728ba | ||
|
|
2673a92f50 | ||
|
|
4d72185ba9 | ||
|
|
ade4f5cd2b | ||
|
|
cf1ebf2b3d | ||
|
|
6b5cb7d87e | ||
|
|
4894bbc231 | ||
|
|
721c5a99ee | ||
|
|
36fccc80cb | ||
|
|
e14fe86306 | ||
|
|
95c9f99961 | ||
|
|
546189b9c8 | ||
|
|
dfbc2a65c0 | ||
|
|
2038d79e18 | ||
|
|
188caf397b | ||
|
|
13138b1d7d | ||
|
|
5466efd8c3 | ||
|
|
61f0dc92d1 | ||
|
|
073c174203 | ||
|
|
1d0f7231e5 | ||
|
|
9401c81581 | ||
|
|
936e54f3e2 | ||
|
|
0c02fc1ce0 | ||
|
|
c723f1df00 | ||
|
|
c65a3f7fb1 | ||
|
|
a0c59f9bb6 | ||
|
|
069dcd65bb | ||
|
|
5331d39c1d | ||
|
|
2c928e6158 | ||
|
|
3ae17479cb | ||
|
|
b3589755f5 | ||
|
|
e7f97ff4d4 | ||
|
|
7b4a3b52b3 | ||
|
|
310fd99730 | ||
|
|
26473f5596 | ||
|
|
ead8a02b6a | ||
|
|
b2ca9cf792 | ||
|
|
301c254446 | ||
|
|
769091d271 | ||
|
|
1bc7907ba0 | ||
|
|
8260c606dc | ||
|
|
c0b7e563c5 | ||
|
|
9062e15712 | ||
|
|
134da6fad3 | ||
|
|
4cec6ef711 | ||
|
|
36d64c85fb | ||
|
|
8e00019023 | ||
|
|
575d4d2d26 | ||
|
|
19e9ebc20f | ||
|
|
dd4476b786 | ||
|
|
fc283a64a7 | ||
|
|
b6dc1d35c6 | ||
|
|
4f542510d6 | ||
|
|
0ca334decc | ||
|
|
239a926301 | ||
|
|
9f4c7fbedd | ||
|
|
6a9de48bb7 | ||
|
|
d9fc22399a | ||
|
|
f41b0dc0c2 | ||
|
|
14233c0aa1 | ||
|
|
58e56c4226 | ||
|
|
8ee184475e | ||
|
|
be9c5b1be5 | ||
|
|
8411f8f2b9 | ||
|
|
5441a7a126 | ||
|
|
b2425de273 | ||
|
|
82d5e73129 | ||
|
|
31189ebdef | ||
|
|
ee1720fa95 | ||
|
|
72dc0634f3 | ||
|
|
df0f86ce7e |
No files matched your search
+75
-9
@@ -1,57 +1,122 @@
|
||||
# `2.6.0`
|
||||
_03.09.2025_
|
||||
# `2.9.0`
|
||||
_06.10.2025_
|
||||
|
||||
## Extruder V2
|
||||
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
|
||||
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
|
||||
## Winder V2
|
||||
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
|
||||
|
||||
## General
|
||||
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
|
||||
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
|
||||
|
||||
|
||||
# `2.8.0`
|
||||
_30.09.2025_
|
||||
|
||||
|
||||
## Laser V1
|
||||
- [#741](https://github.com/qitechgmbh/control/pull/741) Added roundness measurement to two-axis Laser
|
||||
|
||||
|
||||
## Winder V2
|
||||
- [#745](https://github.com/qitechgmbh/control/pull/745), [#756](https://github.com/qitechgmbh/control/pull/756) improved Winder Frontend
|
||||
|
||||
## General
|
||||
- [#447](https://github.com/qitechgmbh/control/pull/447) added AquaPath Machine
|
||||
|
||||
|
||||
|
||||
|
||||
# `2.7.0`
|
||||
|
||||
_17.09.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#695](https://github.com/qitechgmbh/control/pull/695) Pressure and Rpm Regulation properly switch again
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#731](https://github.com/qitechgmbh/control/pull/731) Added Laser with two-axis measurements
|
||||
|
||||
## General
|
||||
|
||||
- [#679](https://github.com/qitechgmbh/control/pull/679),[#677](https://github.com/qitechgmbh/control/pull/677),[#698](https://github.com/qitechgmbh/control/pull/698), [#684](https://github.com/qitechgmbh/control/pull/684),[#706](https://github.com/qitechgmbh/control/pull/706) Greatly improved overall system latency and stability
|
||||
|
||||
- [#720](https://github.com/qitechgmbh/control/pull/720), [#686](https://github.com/qitechgmbh/control/pull/686) Improved User Interface
|
||||
|
||||
# `2.6.0`
|
||||
|
||||
_03.09.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
|
||||
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#642](https://github.com/qitechgmbh/control/pull/642) Improved stability of laser connection
|
||||
|
||||
## General
|
||||
|
||||
- [#657](https://github.com/qitechgmbh/control/pull/657) Time frame for graphs is now remembered
|
||||
- [#641](https://github.com/qitechgmbh/control/pull/641) Removed duplicate scrollbar from manuals
|
||||
|
||||
# `2.5.0`
|
||||
|
||||
_27.08.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#595](https://github.com/qitechgmbh/control/pull/600) Simplified power graphs
|
||||
- [#593](https://github.com/qitechgmbh/control/pull/599) Added identifiers for temperature sensors in temperature graph
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#598](https://github.com/qitechgmbh/control/pull/601) Images now load properly in the manual
|
||||
|
||||
## General
|
||||
|
||||
- [#540](https://github.com/qitechgmbh/control/pull/592) Improved readability of the presets
|
||||
- [#591](https://github.com/qitechgmbh/control/pull/626) Significantly improved user interface performance and stability
|
||||
|
||||
# `2.4.0`
|
||||
|
||||
_14.08.2025_
|
||||
|
||||
## Winder V2
|
||||
## Winder V2
|
||||
|
||||
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
|
||||
|
||||
|
||||
## General
|
||||
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
|
||||
|
||||
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
|
||||
- [#588](https://github.com/qitechgmbh/control/pull/589) Improved frontend performance
|
||||
|
||||
# `2.3.0`
|
||||
|
||||
_06.08.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
|
||||
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
|
||||
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
|
||||
|
||||
## Winder V2
|
||||
## Winder V2
|
||||
|
||||
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
|
||||
|
||||
## General
|
||||
|
||||
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
|
||||
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
|
||||
|
||||
@@ -67,6 +132,7 @@ _15.07.2025_
|
||||
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
|
||||
|
||||
## General
|
||||
|
||||
Generated
+32
-159
@@ -99,9 +99,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -284,9 +284,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
@@ -304,8 +304,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
@@ -319,9 +318,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.2"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -330,7 +329,6 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -596,6 +594,7 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"textplots",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uom",
|
||||
]
|
||||
@@ -954,11 +953,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
|
||||
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
@@ -1019,6 +1019,7 @@ dependencies = [
|
||||
"futures",
|
||||
"rand",
|
||||
"smol",
|
||||
"tracing",
|
||||
"uom",
|
||||
]
|
||||
|
||||
@@ -1677,15 +1678,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.16"
|
||||
@@ -1697,16 +1689,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@@ -1746,9 +1728,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
version = "0.2.176"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2000,17 +1982,6 @@ version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.30.0"
|
||||
@@ -2120,12 +2091,6 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pcap"
|
||||
version = "2.2.0"
|
||||
@@ -2415,9 +2380,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.2"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2427,9 +2392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2575,18 +2540,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2698,14 +2673,12 @@ dependencies = [
|
||||
"bitvec",
|
||||
"control_core",
|
||||
"control_core_derive",
|
||||
"crc",
|
||||
"dhat",
|
||||
"ethercat_hal",
|
||||
"ethercrab",
|
||||
"euclid",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"open",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry_sdk",
|
||||
@@ -2716,9 +2689,6 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"textplots",
|
||||
"thread-priority",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tower-http",
|
||||
@@ -3031,20 +3001,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||
|
||||
[[package]]
|
||||
name = "thread-priority"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
@@ -3055,37 +3011,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-ctl"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"paste",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "timerfd"
|
||||
version = "1.6.0"
|
||||
@@ -3658,28 +3583,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -3693,17 +3596,6 @@ dependencies = [
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -3732,16 +3624,6 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
@@ -3843,15 +3725,6 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -17,6 +17,7 @@ resolver = "2"
|
||||
collapsible_if = "allow"
|
||||
|
||||
# Lint groups (priority -1)
|
||||
correctness = { level = "warn", priority = -1 }
|
||||
suspicious = { level = "warn", priority = -1 }
|
||||
complexity = { level = "warn", priority = -1 }
|
||||
perf = { level = "warn", priority = -1 }
|
||||
|
||||
@@ -32,7 +32,7 @@ Other
|
||||
|
||||
## Backend
|
||||
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
|
||||
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
|
||||
|
||||
@@ -64,12 +64,12 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
|
||||
# More Docs
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Executable
+43
@@ -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,3 +29,37 @@ fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
|
||||
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Machine)]
|
||||
pub fn machine_derive(item: TokenStream) -> TokenStream {
|
||||
machine_derive2(item.into()).unwrap().into()
|
||||
}
|
||||
|
||||
fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let ast: DeriveInput = syn::parse2(item)?;
|
||||
|
||||
let ident = &ast.ident;
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
|
||||
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(expanded)
|
||||
}
|
||||
+17
-5
@@ -7,8 +7,10 @@ edition = "2024"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
axum = "0.8.4"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
axum = "0.8.6"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
ethercrab = "0.6"
|
||||
interfaces = "0.0.9"
|
||||
@@ -17,19 +19,29 @@ serde_json = "1.0.143"
|
||||
serialport = "4.7.3"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
uom = "0.36.0"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
serial = "0.4.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.6"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
|
||||
tokio = { version = "1.45.1", features = ["sync"], optional = true }
|
||||
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.175"
|
||||
libc = "0.2.176"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
textplots = "0.8"
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
video-streaming = ["dep:tokio"]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["axum","bitvec","control_core_derive"]
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::angle::radian;
|
||||
use uom::si::{angle::radian, f64::Angle};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AngleConverter {
|
||||
@@ -63,7 +63,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f32)
|
||||
pub fn degrees_encode(&self, math_angle_degrees: f32) -> f32 {
|
||||
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
@@ -85,7 +85,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f32)
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
@@ -107,14 +107,14 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle in radians from mathematical system to this system (f32)
|
||||
pub fn radians_encode(&self, math_angle_radians: f32) -> f32 {
|
||||
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
|
||||
let degrees = math_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_encode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle in radians from this system to mathematical system (f32)
|
||||
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
|
||||
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
|
||||
let degrees = system_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_decode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
@@ -179,7 +179,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f32)
|
||||
fn normalize_angle(&self, angle: f32) -> f32 {
|
||||
fn normalize_angle(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
@@ -207,30 +207,12 @@ impl AngleConverterUom {
|
||||
Self { angle_converter }
|
||||
}
|
||||
|
||||
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_encode(angle.get::<radian>()),
|
||||
)
|
||||
pub fn encode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
|
||||
}
|
||||
|
||||
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_decode(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_encode_f64(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_decode_f64(angle.get::<radian>()),
|
||||
)
|
||||
pub fn decode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,8 +279,8 @@ mod tests {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Test radians_encode
|
||||
let result = converter.radians_encode(PI / 2.0); // 90° in radians
|
||||
let expected = 270.0 * PI / 180.0; // 270° in radians
|
||||
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
|
||||
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
|
||||
assert!((result - expected).abs() < 1e-6);
|
||||
}
|
||||
|
||||
@@ -306,10 +288,10 @@ mod tests {
|
||||
fn test_roundtrip_conversion() {
|
||||
let converter = AngleConverter::new(true, false, true); // X-flip + CW
|
||||
|
||||
let original = 75.0_f32;
|
||||
let original = 75.0_f64;
|
||||
let converted = converter.degrees_encode(original);
|
||||
let roundtrip = converter.degrees_decode(converted);
|
||||
|
||||
assert!((original - roundtrip).abs() < 1e-6);
|
||||
assert!((original - roundtrip as f64).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::anyhow;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Downcast<T> {
|
||||
fn downcast(&self) -> Result<T, anyhow::Error>;
|
||||
}
|
||||
|
||||
pub trait DowncastRef<T> {
|
||||
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
|
||||
}
|
||||
|
||||
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
|
||||
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let lock = self.lock_blocking();
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
let any: &dyn Any = &*lock;
|
||||
|
||||
if any.is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned: Arc<Mutex<dyn Any>> = self.clone();
|
||||
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
|
||||
|
||||
Ok(arc)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -227,7 +228,7 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
|
||||
|
||||
/// Returns the EEPROM addresses for the machine device identification
|
||||
/// based on the subdevice's identity
|
||||
pub fn get_identification_addresses<'maindevice>(
|
||||
pub fn get_identification_addresses(
|
||||
subdevice_identity: &SubDeviceIdentity,
|
||||
subdevice_name: &str,
|
||||
) -> Result<MachineIdentificationAddresses, Error> {
|
||||
@@ -239,23 +240,25 @@ pub fn get_identification_addresses<'maindevice>(
|
||||
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2002_IDENTITY_A | EL2002_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2004_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2008_IDENTITY_A | EL2008_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
|
||||
MachineIdentificationAddresses::default()
|
||||
}
|
||||
EL2522_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3062_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
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),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
use libc;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::str;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_proc_interrupts() -> Result<String, io::Error> {
|
||||
let path = CString::new("/proc/interrupts").unwrap();
|
||||
// O_RDONLY
|
||||
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::from_raw_os_error(-fd));
|
||||
}
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut chunk = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
let n = unsafe { libc::read(fd, chunk.as_mut_ptr() as *mut _, chunk.len()) };
|
||||
if n < 0 {
|
||||
let e = io::Error::from_raw_os_error(-n as i32);
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(e);
|
||||
} else if n == 0 {
|
||||
break;
|
||||
} else {
|
||||
buf.extend_from_slice(&chunk[..n as usize]);
|
||||
}
|
||||
}
|
||||
unsafe { libc::close(fd) };
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_interface_irq(proc_content: &str, interface_name: &str) -> Option<u32> {
|
||||
for line in proc_content.lines() {
|
||||
if line.contains(interface_name) {
|
||||
if let Some(colon_pos) = line.find(':') {
|
||||
let head = &line[..colon_pos].trim();
|
||||
// try parse head as integer IRQ number
|
||||
if let Ok(irq) = head.parse::<u32>() {
|
||||
return Some(irq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Since kernel 3.0 it’s possible to use the /proc/irq/<IRQ-NUMBER>/smp_affinity_list
|
||||
/// With comma seperated values
|
||||
/// This function takes the irq identifier and writes the cpu string
|
||||
/// into /proc/irq/irq_number/smp_affinity_list
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
|
||||
let path: String = format!("/proc/irq/{}/smp_affinity_list", irq);
|
||||
let cpath = CString::new(path.clone()).unwrap();
|
||||
// We want to completely overwrite the smp affinities, so that we guarentee execution on the specified cores
|
||||
let file_descriptor = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) };
|
||||
|
||||
if file_descriptor < 0 {
|
||||
tracing::info!("Permission denied to write: {}", path);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("failed to open {}: errno {}", path, -file_descriptor),
|
||||
));
|
||||
}
|
||||
// convert to "raw bytes"
|
||||
// cpu needs to be a string because we need the raw ascii code 48 = '0' and so on as bytes
|
||||
let bytes = cpu.as_bytes();
|
||||
let mut bytes_written_total = 0usize;
|
||||
|
||||
while bytes_written_total < bytes.len() {
|
||||
let to_write = &bytes[bytes_written_total..];
|
||||
let bytes_written = unsafe {
|
||||
libc::write(
|
||||
file_descriptor,
|
||||
to_write.as_ptr() as *const _,
|
||||
to_write.len(),
|
||||
)
|
||||
};
|
||||
if bytes_written < 0 {
|
||||
let e = io::Error::from_raw_os_error(-bytes_written as i32);
|
||||
unsafe { libc::close(file_descriptor) };
|
||||
return Err(e);
|
||||
}
|
||||
bytes_written_total += bytes_written as usize;
|
||||
}
|
||||
unsafe { libc::close(file_descriptor) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is used to make sure that a given irq handler runs on the expected cpu
|
||||
/// On Realtime systems this is the make or break fix for certain usecases like ethercrab
|
||||
/// On our machines the 99.99th percentile of cycle times was very bad ~5-11 ms while ethercat is supposed to be in the microseconds
|
||||
/// After pinning the ethernet irq to the core that the ethercat code ran on 99.99th percentile went down to ~200us (microseconds)
|
||||
/// Example input: irq_name: "eno1" , cpu: 2
|
||||
/// Remember that cpu cores are counted from 0
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_irq_affinity(irq_name: &str, cpu: u32) -> Result<(), anyhow::Error> {
|
||||
let proc_contents = read_proc_interrupts()?;
|
||||
let irq = get_interface_irq(&proc_contents, irq_name);
|
||||
if irq.is_none() {
|
||||
return Err(anyhow::anyhow!("Couldnt find irq number!"));
|
||||
}
|
||||
let res = set_irq_affinity_raw(irq.unwrap(), &cpu.to_string());
|
||||
if res.is_err() {
|
||||
return Err(anyhow::anyhow!("Couldnt write affinity list!"));
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
pub mod machines;
|
||||
pub mod modbus;
|
||||
pub mod realtime;
|
||||
@@ -11,5 +13,8 @@ pub mod socketio;
|
||||
pub mod transmission;
|
||||
pub mod uom_extensions;
|
||||
|
||||
#[cfg(feature = "video-streaming")]
|
||||
pub mod video_streaming;
|
||||
|
||||
#[macro_use]
|
||||
extern crate uom;
|
||||
@@ -1,8 +1,27 @@
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::socketio::namespace::Namespace;
|
||||
|
||||
pub trait MachineApi {
|
||||
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace;
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>>;
|
||||
|
||||
/// Returns a list of available video stream identifiers for this machine
|
||||
#[cfg(feature = "video-streaming")]
|
||||
fn api_video_streams(&self) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Get a video stream receiver for the specified stream ID
|
||||
/// Returns None if the stream doesn't exist
|
||||
#[cfg(feature = "video-streaming")]
|
||||
fn api_get_video_stream(
|
||||
&mut self,
|
||||
stream_id: &str,
|
||||
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
|
||||
let _ = stream_id;
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use crate::downcast::Downcast;
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::manager::MachineManager;
|
||||
use crate::socketio::{event::GenericEvent, namespace::Namespace};
|
||||
use serde::Serialize;
|
||||
use smol::block_on;
|
||||
use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
|
||||
use super::Machine;
|
||||
use anyhow::anyhow;
|
||||
use smol::{channel::Sender, lock::Mutex};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MachineConnection<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Error(anyhow::Error),
|
||||
Disconnected,
|
||||
Connected(Arc<Mutex<M>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineSlot<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
pub machine_connection: MachineConnection<M>,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
|
||||
|
||||
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
|
||||
|
||||
impl<M: Machine> Clone for MachineConnection<M> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Connected(m) => Self::Connected(m.clone()),
|
||||
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
|
||||
Self::Disconnected => Self::Disconnected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineConnection<M> {
|
||||
pub fn to_error(&self) -> Option<anyhow::Error> {
|
||||
match self {
|
||||
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
|
||||
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
|
||||
match self {
|
||||
Self::Connected(machine) => Some(machine.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineSlot<M> {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
let namespace = Namespace::new(socket_queue_tx);
|
||||
Self {
|
||||
machine_connection: MachineConnection::Disconnected,
|
||||
namespace: Arc::new(Mutex::new(namespace)),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_connected(&self) -> bool {
|
||||
matches!(self.machine_connection, MachineConnection::Connected(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CrossConnectableMachine<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
>: Machine
|
||||
{
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineCrossConnection<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
> {
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
connected_machine: Weak<Mutex<MachineSlot<T>>>,
|
||||
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
|
||||
// need F in our type arguemnts. We are ok with
|
||||
// it being used only in a fake field. Without
|
||||
// F being fixed, we would not be anble to implement
|
||||
// reverse connecting and disconnecting properly,
|
||||
// as the type on the other side would decay
|
||||
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct MachineCrossConnectionState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<F: CrossConnectableMachine<F, T>, T: CrossConnectableMachine<T, F>>
|
||||
MachineCrossConnection<F, T>
|
||||
{
|
||||
pub fn new(
|
||||
manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) -> Self {
|
||||
Self {
|
||||
machine_manager: manager,
|
||||
connected_machine: Weak::new(),
|
||||
machine_identification_unique: machine_identification_unique.clone(),
|
||||
_make_compiler_happy: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_connected_machine(
|
||||
&mut self,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) {
|
||||
if let Some(machine_manager) = self.machine_manager.upgrade() {
|
||||
let machine_manager = block_on(machine_manager.read());
|
||||
|
||||
let slot: Arc<Mutex<MachineSlotGeneric>> =
|
||||
match machine_manager.get(&machine_identification_unique) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let slot: Arc<Mutex<MachineSlot<T>>> = match Downcast::downcast(&slot) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
self.connected_machine = Arc::downgrade(&slot);
|
||||
} else {
|
||||
panic!("The machine manager has died, cannot do anything at this point");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse_connect(&self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = &mut slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
|
||||
if !cross_connection.is_connected() {
|
||||
cross_connection.set_connected_machine(&self.machine_identification_unique);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self) {
|
||||
self.connected_machine = Weak::new();
|
||||
}
|
||||
|
||||
pub fn reverse_disconnect(&mut self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
cross_connection.disconnect();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
let arc = self.connected_machine.upgrade();
|
||||
arc.is_some()
|
||||
}
|
||||
|
||||
pub fn to_state(&self) -> MachineCrossConnectionState {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
let machine = machine.lock_blocking();
|
||||
|
||||
return MachineCrossConnectionState {
|
||||
machine_identification_unique: Some(
|
||||
machine.get_machine_identification_unique(),
|
||||
),
|
||||
is_available: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MachineCrossConnectionState {
|
||||
machine_identification_unique: None,
|
||||
is_available: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
@@ -6,6 +12,9 @@ use socketioxide::extract::SocketRef;
|
||||
|
||||
use crate::{
|
||||
machines::{
|
||||
connection::{
|
||||
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
|
||||
},
|
||||
identification::MachineIdentificationUnique,
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
},
|
||||
@@ -17,18 +26,10 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub ethercat_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
pub serial_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl Default for MachineManager {
|
||||
@@ -53,9 +54,6 @@ impl MachineManager {
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<Self>>,
|
||||
) {
|
||||
// empty ethercat machines
|
||||
self.ethercat_machines.clear();
|
||||
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
@@ -69,10 +67,18 @@ impl MachineManager {
|
||||
// iterate over all identified device groups but ignore unaffected machines
|
||||
for device_group in device_grouping_result.device_groups.iter() {
|
||||
// get the machine identification
|
||||
let machine_identification = match device_group.first() {
|
||||
Some(device_identification) => &device_identification.device_machine_identification,
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
let machine_identification_unique: MachineIdentificationUnique =
|
||||
match device_group.first() {
|
||||
Some(device_identification) => device_identification
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone(),
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
|
||||
let slot =
|
||||
self.get_or_create_slot(socket_queue_tx.clone(), machine_identification_unique);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
// create the machine
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
@@ -80,23 +86,40 @@ impl MachineManager {
|
||||
hardware: &machine_new_hardware,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
// insert the machine into the ethercat machines map
|
||||
self.ethercat_machines.insert(
|
||||
machine_identification.machine_identification_unique.clone(),
|
||||
new_machine,
|
||||
);
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_slot(
|
||||
&mut self,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_identification: MachineIdentificationUnique,
|
||||
) -> Arc<Mutex<MachineSlotGeneric>> {
|
||||
if let Some(slot) = self.get(&machine_identification) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
let slot = Arc::new(Mutex::new(MachineSlot::new(socket_queue_tx)));
|
||||
self.ethercat_machines
|
||||
.insert(machine_identification, slot.clone());
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
) -> Option<Arc<Mutex<MachineSlotGeneric>>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
.map(|x| x.clone())
|
||||
}
|
||||
|
||||
pub fn add_serial_device(
|
||||
@@ -115,21 +138,28 @@ impl MachineManager {
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
let machine_identification: MachineIdentificationUnique = device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
|
||||
let slot = self.get_or_create_slot(socket_queue_tx.clone(), machine_identification);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
machine_manager,
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
tracing::info!("Adding serial machine {:?}", new_machine);
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
|
||||
self.serial_machines.insert(
|
||||
device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
new_machine,
|
||||
);
|
||||
tracing::info!("Adding serial machine {:?}", slot);
|
||||
}
|
||||
|
||||
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
|
||||
@@ -144,53 +174,45 @@ impl MachineManager {
|
||||
device_identification_identified
|
||||
);
|
||||
|
||||
self.serial_machines.remove(
|
||||
&device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
);
|
||||
let machine_identification_unique = &device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
|
||||
if let Some(slot) = self.serial_machines.get(machine_identification_unique) {
|
||||
let mut slot = slot.lock_blocking();
|
||||
slot.machine_connection = MachineConnection::Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_machine_weak(
|
||||
pub fn get_ethercat_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.ethercat_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
Some(machine_weak)
|
||||
self.get_weak(&self.ethercat_machines, machine_identification)
|
||||
}
|
||||
|
||||
pub fn get_serial_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.serial_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
self.get_weak(&self.serial_machines, machine_identification)
|
||||
}
|
||||
|
||||
fn get_weak(
|
||||
&self,
|
||||
machines: &HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let slot = machines.get(machine_identification).clone();
|
||||
|
||||
return slot.and_then(|slot| {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
match connection {
|
||||
MachineConnectionGeneric::Error(_) => None,
|
||||
MachineConnectionGeneric::Disconnected => None,
|
||||
MachineConnectionGeneric::Connected(machine) => Some(Arc::downgrade(&machine)),
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
Some(machine_weak)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
use crate::machines::connection::MachineSlotGeneric;
|
||||
|
||||
use super::{identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
|
||||
pub enum MachineManagerIter<'a> {
|
||||
EthercatMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
@@ -24,14 +27,13 @@ pub enum MachineManagerIter<'a> {
|
||||
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
serial_machines: &'a HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
&'a Arc<Mutex<MachineSlotGeneric>>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
@@ -71,7 +73,7 @@ impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
|
||||
impl MachineManager {
|
||||
// Returns an iterator over all machines (both ethercat and serial)
|
||||
pub fn iter(&'_ self) -> MachineManagerIterator<'_> {
|
||||
pub fn iter(&self) -> MachineManagerIterator<'_> {
|
||||
MachineManagerIterator {
|
||||
iter: MachineManagerIter::EthercatMachines {
|
||||
iter: self.ethercat_machines.iter(),
|
||||
|
||||
@@ -1,80 +1,23 @@
|
||||
use anyhow::anyhow;
|
||||
use api::MachineApi;
|
||||
use new::MachineNewTrait;
|
||||
use serde::Serialize;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::new::MachineAct;
|
||||
use crate::machines::{
|
||||
api::MachineApi, identification::MachineIdentificationUnique, new::MachineAct,
|
||||
};
|
||||
pub mod api;
|
||||
pub mod connection;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
|
||||
}
|
||||
|
||||
pub trait AnyGetters: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
/// Casts a `dyn Machine` to a specific machine type
|
||||
pub async fn downcast_machine<T: Machine>(
|
||||
machine: Arc<Mutex<dyn Machine>>,
|
||||
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let read_lock = machine.lock().await;
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
if read_lock.as_any().is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned_machine = Arc::clone(&machine);
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
Ok(Arc::from_raw(
|
||||
Arc::into_raw(cloned_machine) as *const Mutex<T>
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"[{}::downcast_machine] Downcast failed",
|
||||
module_path!()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to get strong count to see if Weak has reference
|
||||
pub trait GetStrongCount {
|
||||
fn get_strong_count(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<T> GetStrongCount for Weak<Mutex<T>> {
|
||||
fn get_strong_count(&self) -> usize {
|
||||
self.strong_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectedMachine<T: GetStrongCount> {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub machine: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConnectedMachineData {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
|
||||
where
|
||||
T: GetStrongCount,
|
||||
{
|
||||
fn from(value: &ConnectedMachine<T>) -> Self {
|
||||
Self {
|
||||
machine_identification_unique: value.machine_identification_unique.clone(),
|
||||
is_available: value.machine.get_strong_count() != 0,
|
||||
}
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::{machines::manager::MachineManager, serial::SerialDevice};
|
||||
use crate::{
|
||||
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
|
||||
serial::SerialDevice,
|
||||
socketio::namespace::Namespace,
|
||||
};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
@@ -6,7 +10,10 @@ use ethercat_hal::{
|
||||
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::{channel::Sender, lock::RwLock};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
@@ -49,6 +56,19 @@ pub struct MachineNewParams<
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
|
||||
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
|
||||
@@ -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)?)))
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
@@ -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`
|
||||
Generated
+149
-28
@@ -15,7 +15,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.105.0",
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
@@ -43,6 +43,7 @@
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
@@ -77,8 +78,9 @@
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
@@ -95,7 +97,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.3.6",
|
||||
"vitest": "^3.0.6"
|
||||
}
|
||||
},
|
||||
@@ -2655,18 +2657,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz",
|
||||
"integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
|
||||
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
@@ -2689,6 +2692,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
|
||||
@@ -3964,13 +4024,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.12.tgz",
|
||||
"integrity": "sha512-7BYgOpGc1vK8MH1LIFLLBudGpH46GQy+hewnP7dNQJ4KHmkwPHv958L1IMA9jU/rs5g1ZH5n1f33BAMOBXUMYQ==",
|
||||
"version": "1.130.17",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
|
||||
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
"@tanstack/router-core": "1.130.12",
|
||||
"@tanstack/router-core": "1.130.17",
|
||||
"isbot": "^5.1.22",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"tiny-warning": "^1.0.3"
|
||||
@@ -4044,9 +4105,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.12.tgz",
|
||||
"integrity": "sha512-emq3cRU9Na1hnIToojzkfJcOZm/MG2bv9M+Kr/elUxEf83enGEwQXC1EKezTuwNgeJrOv8vPJdEhWM7IQodnHQ==",
|
||||
"version": "1.130.17",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
|
||||
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/store": "^0.7.0",
|
||||
@@ -4442,10 +4504,18 @@
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"version": "19.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
|
||||
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4459,6 +4529,17 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized": {
|
||||
"version": "9.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
|
||||
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
@@ -6295,6 +6376,16 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -9917,7 +10008,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -11301,7 +11391,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -12087,7 +12176,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -12097,8 +12185,7 @@
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
@@ -12226,6 +12313,12 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
@@ -12326,6 +12419,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized": {
|
||||
"version": "9.22.6",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
|
||||
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"clsx": "^1.0.4",
|
||||
"dom-helpers": "^5.1.3",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/read-binary-file-arch": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
||||
@@ -14552,9 +14672,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
@@ -60,7 +61,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": {
|
||||
@@ -71,7 +72,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
@@ -80,7 +81,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.105.0",
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
@@ -99,6 +100,7 @@
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
||||
@@ -58,15 +58,10 @@ export function MarkdownWithToc({
|
||||
className={`sticky ${stickyTop}`}
|
||||
style={{ maxHeight: "calc(100vh - 6rem)" }}
|
||||
>
|
||||
{/* Hide scrollbar for the TOC container across browsers */}
|
||||
<style>{`
|
||||
[data-toc-scroll-container] { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
[data-toc-scroll-container]::-webkit-scrollbar { display: none; }
|
||||
`}</style>
|
||||
{/* Table of Contents with hidden scrollbar */}
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
className="scrollbar-hidden overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 8rem)" }}
|
||||
data-toc-scroll-container
|
||||
>
|
||||
<MarkdownToc
|
||||
markdownContent={markdownContent}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import {
|
||||
List,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
ListRowRenderer,
|
||||
AutoSizer,
|
||||
} from "react-virtualized";
|
||||
|
||||
// Create cache (outside the component so it persists across renders)
|
||||
const cache = new CellMeasurerCache({
|
||||
defaultHeight: 20,
|
||||
fixedWidth: true,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
lines: string[];
|
||||
autoScroll?: boolean; // Optional prop to control if terminal should auto-scroll
|
||||
className?: string; // Optional prop to control terminal height
|
||||
autoScroll?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
exportPrefix?: string; // Optional prefix for exported log files
|
||||
exportPrefix?: string;
|
||||
};
|
||||
|
||||
const terminalStyle = cva([
|
||||
"flex flex-col overflow-hidden rounded-md border border-neutral-700 font-mono text-sm",
|
||||
]);
|
||||
|
||||
// Color mapping for ANSI color codes
|
||||
const colorMap: Record<string, string> = {
|
||||
"30": "text-gray-900",
|
||||
"31": "text-red-600",
|
||||
@@ -32,7 +44,6 @@ const colorMap: Record<string, string> = {
|
||||
"95": "text-purple-400",
|
||||
"96": "text-cyan-400",
|
||||
"97": "text-white",
|
||||
// Background colors
|
||||
"40": "bg-gray-900",
|
||||
"41": "bg-red-600",
|
||||
"42": "bg-green-600",
|
||||
@@ -43,36 +54,26 @@ const colorMap: Record<string, string> = {
|
||||
"47": "bg-gray-200",
|
||||
};
|
||||
|
||||
// Parse ANSI color codes in text
|
||||
const parseColorCodes = (text: string) => {
|
||||
// Split by ANSI escape sequences
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/* eslint-disable-next-line no-control-regex */
|
||||
const parts = text.split(/(\x1b\[\d+m)/g);
|
||||
|
||||
let currentClass = "";
|
||||
const result: { text: string; className: string }[] = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (part.startsWith("\x1b[")) {
|
||||
// This is a color code
|
||||
const code = part.slice(2, -1); // Extract the number from \x1b[XXm
|
||||
const code = part.slice(2, -1);
|
||||
currentClass = colorMap[code] || "";
|
||||
} else if (part) {
|
||||
// This is text content
|
||||
result.push({ text: part, className: currentClass });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Function to strip ANSI color codes for plain text copy
|
||||
const stripColorCodes = (text: string): string => {
|
||||
const stripColorCodes = (text: string): string =>
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return text.replace(/\x1b\[\d+m/g, "");
|
||||
};
|
||||
text.replace(/\x1b\[\d+m/g, "");
|
||||
|
||||
export function Terminal({
|
||||
lines,
|
||||
@@ -81,99 +82,88 @@ export function Terminal({
|
||||
title = "Terminal",
|
||||
exportPrefix,
|
||||
}: Props) {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
||||
const listRef = useRef<List>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [exportSuccess, setExportSuccess] = useState(false);
|
||||
|
||||
// Handle scrolling
|
||||
// Auto-scroll to bottom when new lines arrive
|
||||
useEffect(() => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal) return;
|
||||
|
||||
// If auto-scroll is enabled and user was at bottom, scroll to bottom when lines change
|
||||
if (autoScroll && isScrolledToBottom) {
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
if (autoScroll && listRef.current) {
|
||||
listRef.current.scrollToRow(lines.length - 1);
|
||||
}
|
||||
}, [lines, autoScroll, isScrolledToBottom]);
|
||||
}, [lines, autoScroll]);
|
||||
|
||||
// Handle scroll events to detect if user is at bottom
|
||||
const handleScroll = () => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal) return;
|
||||
|
||||
const isAtBottom =
|
||||
Math.abs(
|
||||
terminal.scrollHeight - terminal.clientHeight - terminal.scrollTop,
|
||||
) < 10; // Small threshold to account for rounding errors
|
||||
|
||||
setIsScrolledToBottom(isAtBottom);
|
||||
};
|
||||
|
||||
// Handle copy to clipboard
|
||||
const handleCopy = async () => {
|
||||
// Strip ANSI color codes and join lines
|
||||
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
|
||||
|
||||
const plainText = lines.map(stripColorCodes).join("\n");
|
||||
try {
|
||||
await navigator.clipboard.writeText(plainText);
|
||||
setCopySuccess(true);
|
||||
|
||||
// Reset copy success message after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopySuccess(false);
|
||||
}, 2000);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle export to file
|
||||
const handleExport = () => {
|
||||
if (!exportPrefix) return;
|
||||
|
||||
// Strip ANSI color codes and join lines
|
||||
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
|
||||
|
||||
// Create timestamp for filename
|
||||
const plainText = lines.map(stripColorCodes).join("\n");
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "-")
|
||||
.slice(0, -5);
|
||||
const filename = `${exportPrefix}_${timestamp}.log`;
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([plainText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary link element and trigger download
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Show success feedback
|
||||
setExportSuccess(true);
|
||||
setTimeout(() => {
|
||||
setExportSuccess(false);
|
||||
}, 2000);
|
||||
setTimeout(() => setExportSuccess(false), 2000);
|
||||
};
|
||||
|
||||
const rowRenderer: ListRowRenderer = ({ index, key, parent, style }) => {
|
||||
const colorParts = parseColorCodes(lines[index] || "");
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
columnIndex={0}
|
||||
rowIndex={index}
|
||||
>
|
||||
<div
|
||||
style={style}
|
||||
className="bg-black px-2 py-1 whitespace-pre-wrap text-white"
|
||||
>
|
||||
{colorParts.length > 0
|
||||
? colorParts.map((part, partIndex) => (
|
||||
<span
|
||||
key={partIndex}
|
||||
className={part.className || "text-white"}
|
||||
>
|
||||
{part.text || " "}
|
||||
</span>
|
||||
))
|
||||
: lines[index] || " "}
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={terminalStyle({ className })}>
|
||||
{/* Terminal header */}
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-neutral-700 bg-neutral-800 px-4 py-2">
|
||||
<div className="text-xs text-neutral-400">{title}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copySuccess ? (
|
||||
<>
|
||||
@@ -191,7 +181,6 @@ export function Terminal({
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
|
||||
title="Export logs to file"
|
||||
>
|
||||
{exportSuccess ? (
|
||||
<>
|
||||
@@ -209,62 +198,28 @@ export function Terminal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal content with custom scrollbar styling */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
onScroll={handleScroll}
|
||||
className={`scrollbar-thin scrollbar-thumb-neutral-600 scrollbar-track-transparent flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300`}
|
||||
style={{
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "rgb(82 82 91) transparent",
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
Add commentMore actions
|
||||
/* For Webkit browsers (Chrome, Safari) */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(82 82 91);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(82 82 91) transparent;
|
||||
}
|
||||
`}</style>
|
||||
{lines.map((line, index) => {
|
||||
const colorParts = parseColorCodes(line);
|
||||
|
||||
return (
|
||||
<div key={index} className="whitespace-pre-wrap">
|
||||
{colorParts.length > 0
|
||||
? colorParts.map((part, partIndex) => (
|
||||
<span key={partIndex} className={part.className}>
|
||||
{part.text || " "}
|
||||
</span>
|
||||
))
|
||||
: line || " "}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Virtualized List */}
|
||||
<div className="flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width}
|
||||
height={height}
|
||||
deferredMeasurementCache={cache}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowCount={lines.length}
|
||||
rowRenderer={rowRenderer}
|
||||
overscanRowCount={5}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between bg-neutral-800 px-4 py-1 text-xs text-neutral-400">
|
||||
<div>{lines.length} lines</div>
|
||||
<div>
|
||||
{isScrolledToBottom ? "At bottom" : "Scrolled up"} |
|
||||
{autoScroll ? " Auto-scroll enabled" : " Auto-scroll disabled"}
|
||||
</div>
|
||||
<div>{autoScroll ? "Auto-scroll enabled" : "Auto-scroll disabled"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,10 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:Zap";
|
||||
case "A":
|
||||
return "lu:Zap";
|
||||
case "kWh":
|
||||
return "lu:BatteryFull";
|
||||
case "l/min":
|
||||
return "lu:Waves";
|
||||
default:
|
||||
return "lu:ChartNoAxesColumn";
|
||||
}
|
||||
@@ -64,6 +68,10 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "V";
|
||||
case "A":
|
||||
return "A";
|
||||
case "kWh":
|
||||
return "kWh";
|
||||
case "l/min":
|
||||
return "l/min";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -119,6 +127,10 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "volts";
|
||||
case "A":
|
||||
return "amperes";
|
||||
case "kWh":
|
||||
return "kilowatt-hours";
|
||||
case "l/min":
|
||||
return "liters/minute";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -138,6 +150,8 @@ export const units = [
|
||||
"W",
|
||||
"V",
|
||||
"A",
|
||||
"kWh",
|
||||
"l/min",
|
||||
] as const;
|
||||
|
||||
export type Unit = (typeof units)[number];
|
||||
|
||||
@@ -145,7 +145,7 @@ async function deleteNixOSGeneration(generationId: string): Promise<{
|
||||
const process = spawn("sudo", [
|
||||
"sh",
|
||||
"-c",
|
||||
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
|
||||
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && nix store gc && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
|
||||
]);
|
||||
|
||||
let stderr = "";
|
||||
|
||||
@@ -18,17 +18,39 @@ type UpdateExecuteInvokeParams = {
|
||||
|
||||
export function exposeUpdateContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
|
||||
let currentLogListener: ((event: any, log: string) => void) | null = null;
|
||||
let currentEndListener: ((event: any, params: any) => void) | null = null;
|
||||
|
||||
contextBridge.exposeInMainWorld("update", {
|
||||
execute: (params: UpdateExecuteInvokeParams) =>
|
||||
ipcRenderer.invoke(UPDATE_EXECUTE, params),
|
||||
cancel: () => ipcRenderer.invoke(UPDATE_CANCEL),
|
||||
onLog: (callback: (log: string) => void) =>
|
||||
ipcRenderer.on(UPDATE_LOG, (_event, log: string) => {
|
||||
|
||||
onLog: (callback: (log: string) => void) => {
|
||||
if (currentLogListener) {
|
||||
ipcRenderer.removeListener(UPDATE_LOG, currentLogListener);
|
||||
}
|
||||
|
||||
currentLogListener = (_event, log: string) => {
|
||||
callback(log);
|
||||
}),
|
||||
onEnd: (callback: (params: { success: boolean; error?: string }) => void) =>
|
||||
ipcRenderer.on(UPDATE_END, (_event, params) => {
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_LOG, currentLogListener);
|
||||
},
|
||||
|
||||
onEnd: (
|
||||
callback: (params: { success: boolean; error?: string }) => void,
|
||||
) => {
|
||||
if (currentEndListener) {
|
||||
ipcRenderer.removeListener(UPDATE_END, currentEndListener);
|
||||
}
|
||||
|
||||
currentEndListener = (_event, params) => {
|
||||
callback(params);
|
||||
}),
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_END, currentEndListener);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
import type { UpdateInfo } from "@/stores/updateStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
|
||||
let currentLogListener: ((line: string) => void) | null = null;
|
||||
|
||||
export async function updateExecute(
|
||||
source: UpdateInfo,
|
||||
onLog: (log: string) => void,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
window.update.onLog(onLog);
|
||||
// Remove previous listener if exists
|
||||
if (currentLogListener) {
|
||||
window.update.onLog(() => {}); // clear previous
|
||||
currentLogListener = null;
|
||||
}
|
||||
|
||||
currentLogListener = onLog;
|
||||
window.update.onLog(currentLogListener);
|
||||
|
||||
window.update.execute(source);
|
||||
|
||||
window.update.onEnd((params) => {
|
||||
window.update.onLog(() => {});
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {});
|
||||
currentLogListener = null;
|
||||
resolve(params);
|
||||
});
|
||||
});
|
||||
@@ -40,7 +52,10 @@ export async function updateCancel(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
return window.update.cancel();
|
||||
const result = await window.update.cancel();
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {}); // remove listener
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enhanced helper that automatically manages store state
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
|
||||
export function Aquapath1ControlPage() {
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
setFrontFlow,
|
||||
setBackFlow,
|
||||
} = useAquapath1();
|
||||
const frontTargetTemperature =
|
||||
state?.temperature_states?.front.target_temperature ?? 0;
|
||||
const backTargetTemperature =
|
||||
state?.temperature_states?.back.target_temperature ?? 0;
|
||||
|
||||
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
|
||||
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Flow Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Flow"
|
||||
unit="l/min"
|
||||
timeseries={front_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Flow"
|
||||
unit="l/min"
|
||||
timeseries={back_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Label label="Front Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={frontTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setFrontFlow(true);
|
||||
} else {
|
||||
setFrontFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Back Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={backTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setBackFlow(true);
|
||||
} else {
|
||||
setBackFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Temperature Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front in"
|
||||
unit="C"
|
||||
timeseries={front_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Reservoir"
|
||||
unit="C"
|
||||
timeseries={front_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Front Target Temperature">
|
||||
<EditValue
|
||||
title="Set Front Target Temperature"
|
||||
value={frontTargetTemperature}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontTemperature(val);
|
||||
}}
|
||||
defaultValue={
|
||||
defaultState?.temperature_states.front.target_temperature
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back in"
|
||||
unit="C"
|
||||
timeseries={back_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Reservoir"
|
||||
unit="C"
|
||||
timeseries={back_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Back Target Temperature">
|
||||
<EditValue
|
||||
title="Set Back Target Temperature"
|
||||
value={backTargetTemperature}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackTemperature(val);
|
||||
}}
|
||||
defaultValue={
|
||||
defaultState?.temperature_states.back.target_temperature
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Mode">
|
||||
<SelectionGroup<"Standby" | "Auto">
|
||||
value={state?.mode_state.mode}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Auto: {
|
||||
children: "Auto",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setAquapathMode(value)}
|
||||
/>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import {
|
||||
AutoSyncedBigGraph,
|
||||
SyncedFloatingControlPanel,
|
||||
useGraphSync,
|
||||
type GraphConfig,
|
||||
} from "@/components/graph";
|
||||
|
||||
import React from "react";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import { TimeSeries } from "@/lib/timeseries";
|
||||
|
||||
export function Aquapath1GraphPage() {
|
||||
const {
|
||||
state,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
} = useAquapath1();
|
||||
|
||||
const syncHook = useGraphSync("aquapath-group");
|
||||
|
||||
const front_temp_target =
|
||||
state?.temperature_states?.front.target_temperature ?? 0;
|
||||
const back_temp_target =
|
||||
state?.temperature_states?.back.target_temperature ?? 0;
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
<FlowGraph
|
||||
syncHook={syncHook}
|
||||
flow={front_flow}
|
||||
name={"Front Flow"}
|
||||
id={"front_flow"}
|
||||
/>
|
||||
<FlowGraph
|
||||
syncHook={syncHook}
|
||||
flow={back_flow}
|
||||
name={"Back Flow"}
|
||||
id={"back_flow"}
|
||||
/>
|
||||
<TemperatureGraph
|
||||
syncHook={syncHook}
|
||||
temp_in={front_temperature}
|
||||
temp_out={front_temp_reservoir}
|
||||
targetTemp={front_temp_target}
|
||||
name={"Front Temperature"}
|
||||
id={"front_temp"}
|
||||
/>
|
||||
<TemperatureGraph
|
||||
syncHook={syncHook}
|
||||
temp_in={back_temperature}
|
||||
temp_out={back_temp_reservoir}
|
||||
targetTemp={back_temp_target}
|
||||
name={"Back Temperature"}
|
||||
id={"back_temp"}
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlowGraph({
|
||||
syncHook,
|
||||
flow,
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
flow: TimeSeries | null;
|
||||
name: string;
|
||||
id: string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: name,
|
||||
icon: "lu:RotateCcw",
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "flow_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: flow,
|
||||
color: "#3b82f6",
|
||||
}}
|
||||
unit="l/min"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
config={config}
|
||||
graphId={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function TemperatureGraph({
|
||||
syncHook,
|
||||
temp_in,
|
||||
temp_out,
|
||||
targetTemp,
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
temp_in: TimeSeries | null;
|
||||
temp_out: TimeSeries | null;
|
||||
targetTemp: number;
|
||||
name: string;
|
||||
id: string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: name,
|
||||
icon: "lu:RotateCcw",
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "temperature_data",
|
||||
showLegend: true,
|
||||
lines: [],
|
||||
};
|
||||
|
||||
const combinedData = [
|
||||
...(temp_in
|
||||
? [
|
||||
{
|
||||
newData: temp_in,
|
||||
title: "Temperature In",
|
||||
color: "#3b82f6",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetTemp,
|
||||
label: "Target Temperature",
|
||||
color: "#3b82f6",
|
||||
show: true,
|
||||
width: 2,
|
||||
dash: [5, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(temp_out
|
||||
? [
|
||||
{
|
||||
newData: temp_out,
|
||||
title: "Temperature Out",
|
||||
color: "#f87171",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetTemp,
|
||||
label: "Target Temperature",
|
||||
color: "#f87171",
|
||||
show: true,
|
||||
width: 2,
|
||||
dash: [5, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={combinedData}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
config={config}
|
||||
graphId={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { aquapath1SerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function Aquapath1Page() {
|
||||
const { serial } = aquapath1SerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/aquapath1/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
{
|
||||
link: "graph",
|
||||
title: "Graph",
|
||||
icon: "lu:ChartSpline",
|
||||
activeLink: "graphs",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @file Aquapath1Namespace.ts
|
||||
* @description TypeScript implementation of Aquapath1 namespace with Zod schema validation.
|
||||
*/
|
||||
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
|
||||
|
||||
// ========== Event Schema Definitions ==========
|
||||
/**
|
||||
* Mode enum for Aquapath Machine
|
||||
*/
|
||||
export const modeSchema = z.enum(["Standby", "Auto"]);
|
||||
|
||||
/**
|
||||
* Mode state schema
|
||||
*/
|
||||
export const modeStateSchema = z.object({
|
||||
mode: modeSchema,
|
||||
});
|
||||
export const tempStateSchema = z.object({
|
||||
temperature: z.number(),
|
||||
target_temperature: z.number(),
|
||||
});
|
||||
/**
|
||||
* Cooling states schema
|
||||
*/
|
||||
export const tempStatesSchema = z.object({
|
||||
front: tempStateSchema,
|
||||
back: tempStateSchema,
|
||||
});
|
||||
|
||||
export const flowStateSchema = z.object({
|
||||
flow: z.number(),
|
||||
should_flow: z.boolean(),
|
||||
});
|
||||
export const flowStatesSchema = z.object({
|
||||
front: flowStateSchema,
|
||||
back: flowStateSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Live values event schema (time-series data)
|
||||
*/
|
||||
export const liveValuesEventDataSchema = z.object({
|
||||
front_flow: z.number(),
|
||||
back_flow: z.number(),
|
||||
front_temperature: z.number(),
|
||||
back_temperature: z.number(),
|
||||
front_temp_reservoir: z.number(),
|
||||
back_temp_reservoir: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* State event schema (consolidated state)
|
||||
*/
|
||||
export const stateEventDataSchema = z.object({
|
||||
is_default_state: z.boolean(),
|
||||
mode_state: modeStateSchema,
|
||||
flow_states: flowStatesSchema,
|
||||
temperature_states: tempStatesSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
// ========== Type Inferences ==========
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
export type ModeState = z.infer<typeof modeStateSchema>;
|
||||
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
|
||||
export type StateEvent = z.infer<typeof stateEventSchema>;
|
||||
|
||||
export type Aquapath1NamespaceStore = {
|
||||
// Single state event from server
|
||||
state: StateEvent | null;
|
||||
defaultState: StateEvent | null;
|
||||
|
||||
front_flow: TimeSeries;
|
||||
back_flow: TimeSeries;
|
||||
|
||||
front_temperature: TimeSeries;
|
||||
back_temperature: TimeSeries;
|
||||
|
||||
front_temp_reservoir: TimeSeries;
|
||||
back_temp_reservoir: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: back_temperature, insert: addTemperature2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: front_temp_reservoir, insert: addTempReserv1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
/**
|
||||
* Factory function to create a new Aquapath namespace store
|
||||
* @returns A new Zustand store instance for Aquapath namespace
|
||||
*/
|
||||
export const createAquapath1NamespaceStore =
|
||||
(): StoreApi<Aquapath1NamespaceStore> => {
|
||||
return create<Aquapath1NamespaceStore>(() => {
|
||||
return {
|
||||
state: null,
|
||||
defaultState: null,
|
||||
front_temperature: front_temperature,
|
||||
back_temperature: back_temperature,
|
||||
front_flow: front_flow,
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a message handler for Mock1 namespace events with validation and appropriate caching strategies
|
||||
* @param store The store to update when messages are received
|
||||
* @param throttledUpdater Throttled updater for batching updates at 60 FPS
|
||||
* @returns A message handler function
|
||||
*/
|
||||
export function aquapath1MessageHandler(
|
||||
store: StoreApi<Aquapath1NamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<Aquapath1NamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const eventName = event.name;
|
||||
|
||||
// Helper function to update store through buffer
|
||||
const updateStore = (
|
||||
updater: (state: Aquapath1NamespaceStore) => Aquapath1NamespaceStore,
|
||||
) => {
|
||||
throttledUpdater.updateWith(updater);
|
||||
};
|
||||
|
||||
try {
|
||||
// State events (latest only)
|
||||
if (eventName === "StateEvent") {
|
||||
const stateEvent = stateEventSchema.parse(event);
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
state: stateEvent,
|
||||
// only set default state if is_default_state is true
|
||||
defaultState: stateEvent.data.is_default_state
|
||||
? stateEvent
|
||||
: state.defaultState,
|
||||
}));
|
||||
}
|
||||
// Live values events (time-series data)
|
||||
else if (eventName === "LiveValuesEvent") {
|
||||
const liveValuesEvent = liveValuesEventSchema.parse(event);
|
||||
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
front_temperature: addTemperature1(state.front_temperature, {
|
||||
value: liveValuesEvent.data.front_temperature,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_temperature: addTemperature2(state.back_temperature, {
|
||||
value: liveValuesEvent.data.back_temperature,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_flow: addFlow1(state.front_flow, {
|
||||
value: liveValuesEvent.data.front_flow,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_flow: addFlow2(state.back_flow, {
|
||||
value: liveValuesEvent.data.back_flow,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_temp_reservoir: addTempReserv1(state.front_temp_reservoir, {
|
||||
value: liveValuesEvent.data.front_temp_reservoir,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_temp_reservoir: addTempReserv2(state.back_temp_reservoir, {
|
||||
value: liveValuesEvent.data.back_temp_reservoir,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
}));
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error processing ${eventName} event:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Aquapath1 namespace implementation
|
||||
*/
|
||||
const useAquapath1NamespaceImplementation =
|
||||
createNamespaceHookImplementation<Aquapath1NamespaceStore>({
|
||||
createStore: createAquapath1NamespaceStore,
|
||||
createEventHandler: aquapath1MessageHandler,
|
||||
});
|
||||
|
||||
export function useAquapath1Namespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): Aquapath1NamespaceStore {
|
||||
// Generate namespace ID from validated machine ID
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
// Use the implementation with validated namespace ID
|
||||
return useAquapath1NamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { aquapath1 } from "@/machines/properties";
|
||||
import { aquapath1SerialRoute } from "@/routes/routes";
|
||||
import { Mode, StateEvent, useAquapath1Namespace } from "./aquapath1Namespace";
|
||||
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { produce } from "immer";
|
||||
import { z } from "zod";
|
||||
|
||||
export function useAquapath1() {
|
||||
const { serial: serialString } = aquapath1SerialRoute.useParams();
|
||||
|
||||
// Memoize the machine identification to keep it stable between renders
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
|
||||
return {
|
||||
machine_identification: {
|
||||
vendor: 0,
|
||||
machine: 0,
|
||||
},
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
machine_identification: aquapath1.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Get consolidated state and live values from namespace
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
front_temperature: front_temperature,
|
||||
back_temperature: back_temperature,
|
||||
front_flow: front_flow,
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
} = useAquapath1Namespace(machineIdentification);
|
||||
|
||||
// Single optimistic state for all state management
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
// Update optimistic state when real state changes
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
stateOptimistic.setReal(state);
|
||||
}
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
const setAquapathMode = (mode: Mode) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.mode_state.mode = mode;
|
||||
},
|
||||
() =>
|
||||
requestAquapathMode({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetAquaPathMode: mode },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontTemperature = (temperature: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.temperature_states.front.target_temperature = temperature;
|
||||
},
|
||||
() =>
|
||||
requestFrontTemperature({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontTemperature: temperature },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setBackTemperature = (temperature: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.temperature_states.back.target_temperature = temperature;
|
||||
},
|
||||
() =>
|
||||
requestBackTemperature({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackTemperature: temperature },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontFlow = (flow: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.flow_states.front.should_flow = flow;
|
||||
},
|
||||
() =>
|
||||
requestFrontFlow({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontFlow: flow },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setBackFlow = (flow: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.flow_states.back.should_flow = flow;
|
||||
},
|
||||
() =>
|
||||
requestBackFlow({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackFlow: flow },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Mutation hooks
|
||||
const { request: requestAquapathMode } = useMachineMutation(
|
||||
z.object({ SetAquaPathMode: z.enum(["Standby", "Auto"]) }),
|
||||
);
|
||||
const { request: requestFrontTemperature } = useMachineMutation(
|
||||
z.object({ SetFrontTemperature: z.number() }),
|
||||
);
|
||||
const { request: requestBackTemperature } = useMachineMutation(
|
||||
z.object({ SetBackTemperature: z.number() }),
|
||||
);
|
||||
const { request: requestFrontFlow } = useMachineMutation(
|
||||
z.object({ SetFrontFlow: z.boolean() }),
|
||||
);
|
||||
const { request: requestBackFlow } = useMachineMutation(
|
||||
z.object({ SetBackFlow: z.boolean() }),
|
||||
);
|
||||
// Helper function for optimistic updates using produce
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
}
|
||||
serverRequest();
|
||||
};
|
||||
|
||||
return {
|
||||
// Consolidated state
|
||||
state: stateOptimistic.value?.data,
|
||||
|
||||
// Default state for initial values
|
||||
defaultState: defaultState?.data,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
setFrontFlow,
|
||||
setBackFlow,
|
||||
};
|
||||
}
|
||||
@@ -50,12 +50,11 @@ export function useBuffer1() {
|
||||
// Single optimistic state for all state management
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
// Update optimistic state when real state changes
|
||||
useEffect(() => {
|
||||
if (state && !stateOptimistic.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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export function Winder2SettingPage() {
|
||||
unit="mm"
|
||||
step={0.05}
|
||||
min={0.1}
|
||||
max={10}
|
||||
max={75}
|
||||
defaultValue={defaultState?.traverse_state?.step_size}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) => setTraverseStepSize(value)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,15 +11,16 @@ import { toast } from "sonner";
|
||||
export function TroubleshootPage() {
|
||||
const [isRebootLoading, setIsRebootLoading] = useState(false);
|
||||
const [isRestartLoading, setIsRestartLoading] = useState(false);
|
||||
const { getLogsBySource, clearLogs } = useLogsStore();
|
||||
|
||||
const { getLogsBySource } = useLogsStore();
|
||||
|
||||
// Perhaps we just need to clear the logs ?
|
||||
// Get backend logs for display
|
||||
const backendLogs = getLogsBySource("qitech-control-server");
|
||||
const logLines = backendLogs.map((log) => log.raw);
|
||||
|
||||
console.log(backendLogs);
|
||||
const handleRebootHmi = async () => {
|
||||
setIsRebootLoading(true);
|
||||
|
||||
try {
|
||||
const result = await rebootHmi();
|
||||
if (result.success) {
|
||||
|
||||
@@ -22,6 +22,8 @@ export function UpdateExecutePage() {
|
||||
startUpdate,
|
||||
stopUpdate,
|
||||
addTerminalLine,
|
||||
clearTerminalLines,
|
||||
resetUpdateState,
|
||||
} = useUpdateStore();
|
||||
|
||||
// Set update info from search params when component mounts or search changes
|
||||
@@ -49,6 +51,7 @@ export function UpdateExecutePage() {
|
||||
};
|
||||
|
||||
startUpdate();
|
||||
// Perhaps we just need to clear the logs ?
|
||||
const res = await updateExecute(updateInfo, addTerminalLine);
|
||||
stopUpdate();
|
||||
|
||||
@@ -66,12 +69,14 @@ export function UpdateExecutePage() {
|
||||
const res = await updateCancelWithStore();
|
||||
if (res.success) {
|
||||
toast.info("Update cancelled successfully");
|
||||
clearTerminalLines();
|
||||
} else {
|
||||
toast.error("Failed to cancel update: " + res.error);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("Failed to cancel update: " + error.message);
|
||||
}
|
||||
resetUpdateState();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,6 @@ const initialState: LogsState = {
|
||||
|
||||
export const useLogsStore = create<LogsStore>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
addLogEntry: (entry) =>
|
||||
set(
|
||||
produce((state: LogsState) => {
|
||||
@@ -69,6 +68,7 @@ export const useLogsStore = create<LogsStore>((set, get) => ({
|
||||
),
|
||||
|
||||
getLogsBySource: (source) => {
|
||||
console.log(source);
|
||||
const state = get();
|
||||
return state.entries.filter((entry) => entry.source === source);
|
||||
},
|
||||
|
||||
@@ -78,11 +78,16 @@ export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
addTerminalLine: (line) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.terminalLines.push(line);
|
||||
console.log(state.terminalLines);
|
||||
|
||||
// Keep only last 10000 lines to prevent memory issues
|
||||
if (state.terminalLines.length > 10000) {
|
||||
state.terminalLines.splice(0, state.terminalLines.length - 10000);
|
||||
const lastLine = state.terminalLines[state.terminalLines.length - 1];
|
||||
if (line !== lastLine) {
|
||||
state.terminalLines.push(line);
|
||||
|
||||
// Keep only last 10000 lines to prevent memory issues
|
||||
if (state.terminalLines.length > 10000) {
|
||||
state.terminalLines.splice(0, state.terminalLines.length - 10000);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -177,3 +177,107 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* macOS-style scrollbars */
|
||||
@layer base {
|
||||
/* WebKit browsers (Safari, Chrome, newer Edge) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
html,
|
||||
body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.dark html,
|
||||
.dark body {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Auto-hide behavior for overlay scrollbars */
|
||||
@media (pointer: fine) {
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:hover {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Container hover shows scrollbars */
|
||||
.scrollbar-visible:hover::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for scrollbar customization */
|
||||
.scrollbar-hidden {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-always-visible::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
@media (pointer: fine) {
|
||||
.scrollbar-always-visible::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,8 @@ tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
control_core = { path = "../control-core" }
|
||||
futures = "0.3.31"
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["interfaces"]
|
||||
Binary file not shown.
@@ -7,14 +7,15 @@ edition = "2024"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
ethercrab = "0.6"
|
||||
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
smol = "2.0.2"
|
||||
uom = "0.36.0"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64","si"] }
|
||||
rand = "0.9.2"
|
||||
futures = "0.3.31"
|
||||
tracing = "0.1.41"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
@@ -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!(),
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
+35
-29
@@ -70,36 +70,42 @@ 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
|
||||
CapabilityBoundingSet = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
|
||||
AmbientCapabilities = "CAP_NET_RAW CAP_IPC_LOCK 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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
+10
-9
@@ -16,9 +16,8 @@ control_core_derive = { path = "../control-core-derive" }
|
||||
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
serde = "1.0.217"
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
open = "5.3.2"
|
||||
bitvec = "1.0.1"
|
||||
lazy_static = "1.5.0"
|
||||
dhat = { version = "0.3.3", optional = true }
|
||||
@@ -27,18 +26,16 @@ 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"
|
||||
regex = "1.11.3"
|
||||
futures = "0.3.31"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
socketioxide = { version = "0.17.2", features = ["msgpack"] }
|
||||
tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum = { version = "0.8.6", features = ["macros"] }
|
||||
|
||||
# serial
|
||||
crc = "3.3.0"
|
||||
serialport = "4.7.3"
|
||||
|
||||
#tracing
|
||||
@@ -58,9 +55,9 @@ opentelemetry-otlp = { version = "0.30.0", features = [
|
||||
], optional = true }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
#[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
#tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
|
||||
#tikv-jemalloc-ctl = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
@@ -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"]
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::ethercat::config::{MAX_SUBDEVICES, PDI_LEN};
|
||||
use crate::performance_metrics::EthercatPerformanceMetrics;
|
||||
use crate::serial::registry::SERIAL_DEVICE_REGISTRY;
|
||||
use crate::socketio::main_namespace::machines_event::MachineObj;
|
||||
use crate::socketio::namespaces::Namespaces;
|
||||
use control_core::machines::Machine;
|
||||
use control_core::machines::identification::{DeviceIdentification, MachineIdentificationUnique};
|
||||
@@ -90,4 +91,22 @@ impl AppState {
|
||||
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_machine_objs(&self) -> Vec<MachineObj> {
|
||||
let machines = self.machines.read_blocking();
|
||||
machines
|
||||
.iter()
|
||||
.map(|machine| {
|
||||
let error = {
|
||||
let slot = machine.1.lock_blocking();
|
||||
slot.machine_connection.to_error().map(|e| e.to_string())
|
||||
};
|
||||
|
||||
MachineObj {
|
||||
machine_identification_unique: machine.0.clone(),
|
||||
error,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ use crate::{
|
||||
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
|
||||
};
|
||||
use control_core::ethercat::eeprom_identification::read_device_identifications;
|
||||
#[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,
|
||||
};
|
||||
@@ -21,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,
|
||||
@@ -45,6 +50,12 @@ pub async fn setup_loop(
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx_clone);
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
// Set core affinity to 4th core
|
||||
let _ = set_core_affinity(3);
|
||||
|
||||
@@ -82,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
|
||||
@@ -149,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): (
|
||||
@@ -190,7 +199,6 @@ pub async fn setup_loop(
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
// construct machines
|
||||
{
|
||||
let mut machines_guard = app_state.machines.write().await;
|
||||
@@ -212,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 {
|
||||
@@ -221,7 +235,7 @@ pub async fn setup_loop(
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = MachinesEventBuilder().build(app_state_clone.clone()).await;
|
||||
let event = MachinesEventBuilder().build(app_state_clone.clone());
|
||||
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
pub fn init_jemalloc_stats() {
|
||||
std::thread::Builder::new()
|
||||
.name("jemalloc-stats".to_string())
|
||||
.spawn(|| {
|
||||
let mut last_allocated = 0usize;
|
||||
let mut last_time = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
smol::block_on(async {
|
||||
sleep(Duration::from_secs(10));
|
||||
// Advance epoch and get allocated bytes
|
||||
let _ = tikv_jemalloc_ctl::epoch::advance();
|
||||
|
||||
if let Ok(allocated_bytes) =
|
||||
unsafe { tikv_jemalloc_ctl::raw::read::<usize>(b"stats.allocated\0") }
|
||||
{
|
||||
let current_time = std::time::Instant::now();
|
||||
let time_diff = current_time.duration_since(last_time).as_secs_f64();
|
||||
|
||||
let change = allocated_bytes as i64 - last_allocated as i64;
|
||||
let change_per_sec = if time_diff > 0.0 {
|
||||
(change as f64 / time_diff) as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let change_str = if change_per_sec >= 0 {
|
||||
format!("+{} KB/s", format_bytes(change_per_sec))
|
||||
} else {
|
||||
format!("-{} KB/s", format_bytes(-change_per_sec))
|
||||
};
|
||||
|
||||
let formatted = format_bytes(allocated_bytes as i64);
|
||||
|
||||
tracing::debug!("Memory: {} KB ({})", formatted, change_str);
|
||||
last_allocated = allocated_bytes;
|
||||
last_time = current_time;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn jemalloc-stats thread");
|
||||
}
|
||||
|
||||
fn format_bytes(bytes: i64) -> String {
|
||||
let kb = bytes / 1024;
|
||||
format!("{}", kb)
|
||||
.chars()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(|(i, c)| {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
format!("_{}", c)
|
||||
} else {
|
||||
c.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,7 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use bitvec::prelude::*;
|
||||
use control_core::machines::connection::MachineConnection;
|
||||
use control_core::realtime::{set_core_affinity, set_realtime_priority};
|
||||
use smol::channel::Sender;
|
||||
use std::sync::Arc;
|
||||
@@ -127,7 +128,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;
|
||||
}
|
||||
|
||||
@@ -140,7 +145,8 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
for machine in machine_guard.iter() {
|
||||
if let Ok(machine) = machine.1 {
|
||||
let connection = &machine.1.lock_blocking().machine_connection;
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
// if the machine is currenlty locked (likely processing API call)
|
||||
// we skip the machine
|
||||
if let Some(mut machine_guard) = machine.try_lock() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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::lock::Mutex;
|
||||
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: Arc<Mutex<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();
|
||||
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
use control_core::{
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
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 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::machines::{
|
||||
aquapath1::{Flow, Temperature, api::AquaPathV1Namespace, controller::Controller},
|
||||
get_ethercat_device,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_subdevice_by_index,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
use ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::{
|
||||
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,
|
||||
},
|
||||
},
|
||||
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 ¶ms.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 _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
|
||||
|
||||
// Role 1 - EL2008 Digital Output Module
|
||||
let el2008 =
|
||||
get_ethercat_device::<EL2008>(hardware, params, 1, [EL2008_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// Role 2 - EL4002 Analog Output Module
|
||||
let el4002 =
|
||||
get_ethercat_device::<EL4002>(hardware, params, 2, [EL4002_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let el3204 = get_ethercat_device::<EL3204>(
|
||||
hardware,
|
||||
params,
|
||||
3,
|
||||
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let el5152 =
|
||||
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let config = EL5152Configuration {
|
||||
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
|
||||
..Default::default()
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
|
||||
el5152
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &config)
|
||||
.await?;
|
||||
|
||||
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 {
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: AquaPathV1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
mode: AquaPathV1Mode::Standby,
|
||||
last_measurement_emit: Instant::now(),
|
||||
front_controller,
|
||||
back_controller,
|
||||
};
|
||||
water_cooling.emit_state();
|
||||
|
||||
Ok(water_cooling)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use super::BufferV1;
|
||||
use control_core::machines::new::MachineAct;
|
||||
|
||||
impl MachineAct for BufferV1 {
|
||||
fn act(&mut self, now: Instant) {
|
||||
// if last measurement is older than 1 second, emit a new measurement
|
||||
|
||||
@@ -2,7 +2,10 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use super::{BufferV1, BufferV1Mode};
|
||||
use control_core::{
|
||||
machines::{api::MachineApi, identification::MachineIdentificationUnique},
|
||||
machines::{
|
||||
api::MachineApi, connection::MachineCrossConnectionState,
|
||||
identification::MachineIdentificationUnique,
|
||||
},
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
@@ -13,8 +16,7 @@ use control_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
@@ -31,7 +33,7 @@ pub struct StateEvent {
|
||||
/// mode state
|
||||
pub mode_state: ModeState,
|
||||
/// connected machine state
|
||||
pub connected_machine_state: ConnectedMachineState,
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
@@ -78,7 +80,7 @@ enum Mutation {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Buffer1Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
|
||||
@@ -86,15 +88,9 @@ impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
|
||||
fn emit(&mut self, events: BufferV1Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer1Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ impl MachineApi for BufferV1 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,22 @@ pub mod new;
|
||||
|
||||
use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEvent};
|
||||
use buffer_tower_controller::BufferTowerController;
|
||||
use control_core::machines::connection::{CrossConnectableMachine, MachineCrossConnection};
|
||||
use control_core::{
|
||||
machines::{
|
||||
ConnectedMachine, ConnectedMachineData, Machine, downcast_machine,
|
||||
identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
manager::MachineManager,
|
||||
},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use futures::executor::block_on;
|
||||
use control_core_derive::Machine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
use smol::lock::RwLock;
|
||||
use std::{sync::Weak, time::Instant};
|
||||
|
||||
use crate::machines::{
|
||||
MACHINE_BUFFER_V1, VENDOR_QITECH, buffer1::api::ConnectedMachineState, winder2::Winder2,
|
||||
};
|
||||
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct BufferV1 {
|
||||
// controllers
|
||||
pub buffer_tower_controller: BufferTowerController,
|
||||
@@ -40,32 +34,29 @@ pub struct BufferV1 {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// connected machines
|
||||
pub connected_winder: Option<ConnectedMachine<Weak<Mutex<Winder2>>>>,
|
||||
pub connected_winder: MachineCrossConnection<BufferV1, Winder2>,
|
||||
|
||||
// mode
|
||||
mode: BufferV1Mode,
|
||||
}
|
||||
|
||||
impl CrossConnectableMachine<BufferV1, Winder2> for BufferV1 {
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<BufferV1, Winder2> {
|
||||
&mut self.connected_winder
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BufferV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BufferV1")
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for BufferV1 {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_BUFFER_V1,
|
||||
};
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let live_values = LiveValuesEvent {};
|
||||
|
||||
@@ -78,28 +69,13 @@ impl BufferV1 {
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone(),
|
||||
},
|
||||
connected_machine_state: ConnectedMachineState {
|
||||
machine_identification_unique: self.connected_winder.as_ref().map(
|
||||
|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).machine_identification_unique
|
||||
},
|
||||
),
|
||||
is_available: self
|
||||
.connected_winder
|
||||
.as_ref()
|
||||
.map(|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).is_available
|
||||
})
|
||||
.unwrap_or(false),
|
||||
},
|
||||
connected_machine_state: self.connected_winder.to_state(),
|
||||
};
|
||||
|
||||
let event = state.build();
|
||||
self.namespace.emit(BufferV1Events::State(event));
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
// To be implemented
|
||||
fn fill_buffer(&mut self) {
|
||||
todo!();
|
||||
@@ -152,17 +128,13 @@ impl BufferV1 {
|
||||
BufferV1Mode::EmptyingBuffer => self.switch_to_emptying(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
fn set_mode_state(&mut self, mode: BufferV1Mode) {
|
||||
self.switch_mode(mode);
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Connecting/Disconnecting machine
|
||||
impl BufferV1 {
|
||||
/// Connecting/Disconnecting machine
|
||||
/// set connected winder
|
||||
pub fn set_connected_winder(
|
||||
&mut self,
|
||||
@@ -174,34 +146,13 @@ impl BufferV1 {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let machine_manager_arc = match self.machine_manager.upgrade() {
|
||||
Some(machine_manager_arc) => machine_manager_arc,
|
||||
None => return,
|
||||
};
|
||||
let machine_manager_guard = block_on(machine_manager_arc.read());
|
||||
let winder2_weak = machine_manager_guard.get_machine_weak(&machine_identification_unique);
|
||||
let winder2_weak = match winder2_weak {
|
||||
Some(winder2_weak) => winder2_weak,
|
||||
None => return,
|
||||
};
|
||||
let winder2_strong = match winder2_weak.upgrade() {
|
||||
Some(winder2_strong) => winder2_strong,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let winder2: Arc<Mutex<Winder2>> = block_on(downcast_machine::<Winder2>(winder2_strong))
|
||||
.expect("failed downcasting machine");
|
||||
|
||||
let machine = Arc::downgrade(&winder2);
|
||||
|
||||
self.connected_winder = Some(ConnectedMachine {
|
||||
machine_identification_unique,
|
||||
machine,
|
||||
});
|
||||
self.connected_winder
|
||||
.set_connected_machine(&machine_identification_unique);
|
||||
|
||||
self.emit_state();
|
||||
|
||||
self.reverse_connect();
|
||||
self.connected_winder.reverse_connect();
|
||||
}
|
||||
|
||||
/// disconnect winder
|
||||
@@ -215,36 +166,9 @@ impl BufferV1 {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(connected) = &self.connected_winder {
|
||||
if let Some(winder2_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut winder2 = winder2_arc.lock().await;
|
||||
if winder2.connected_buffer.is_some() {
|
||||
winder2.connected_buffer = None;
|
||||
winder2.emit_state();
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
}
|
||||
self.connected_winder = None;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// initiate connection from winder to buffer
|
||||
pub fn reverse_connect(&mut self) {
|
||||
let machine_identification_unique = self.machine_identification_unique.clone();
|
||||
if let Some(connected) = &self.connected_winder {
|
||||
if let Some(winder2_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut winder2 = winder2_arc.lock().await;
|
||||
if winder2.connected_buffer.is_none() {
|
||||
winder2.set_connected_buffer(machine_identification_unique);
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
}
|
||||
self.connected_winder.disconnect();
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+106
-190
@@ -1,208 +1,125 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Error;
|
||||
use control_core::machines::identification::DeviceHardwareIdentification;
|
||||
use control_core::machines::connection::MachineCrossConnection;
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
|
||||
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
use ethercat_hal::coe::ConfigurableDevice;
|
||||
use ethercat_hal::devices::ek1100::EK1100;
|
||||
use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
|
||||
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
|
||||
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
|
||||
use ethercat_hal::devices::el7031_0030::{self, EL7031_0030, EL7031_0030_IDENTITY_A};
|
||||
use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
|
||||
use ethercat_hal::devices::el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port};
|
||||
use ethercat_hal::devices::{EthercatDeviceUsed, downcast_device, subdevice_identity_to_tuple};
|
||||
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
use ethercat_hal::shared_config;
|
||||
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
|
||||
use ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::el7031_0030::coe::EL7031_0030Configuration,
|
||||
devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment,
|
||||
devices::el7041_0052::coe::EL7041_0052Configuration,
|
||||
devices::{
|
||||
EthercatDeviceUsed,
|
||||
ek1100::{EK1100, EK1100_IDENTITY_A},
|
||||
el7031_0030::{EL7031_0030, EL7031_0030_IDENTITY_A},
|
||||
el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port},
|
||||
},
|
||||
io::stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
shared_config,
|
||||
shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration},
|
||||
};
|
||||
|
||||
use crate::machines::buffer1::BufferV1Mode;
|
||||
use crate::machines::buffer1::buffer_tower_controller::BufferTowerController;
|
||||
use crate::machines::get_ethercat_device;
|
||||
|
||||
use super::{BufferV1, api::Buffer1Namespace};
|
||||
|
||||
impl MachineNewTrait for BufferV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
fn new<'maindevice>(
|
||||
params: &MachineNewParams<'maindevice, '_, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params.device_group.to_vec();
|
||||
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let hardware: &&control_core::machines::new::MachineNewHardwareEthercat<'_, '_, '_> =
|
||||
match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
smol::block_on(async {
|
||||
// Role 0
|
||||
// Buscoupler
|
||||
// EK1100
|
||||
// Role 0 - Buscoupler EK1100
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// Role 1 - Stepper Buffer EL7041-0052
|
||||
let (el7041, subdevice) = get_ethercat_device::<EL7041_0052>(
|
||||
hardware,
|
||||
params,
|
||||
1,
|
||||
[EL7041_0052_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7041_config = EL7041_0052Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 6000,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
el7041
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &el7041_config)
|
||||
.await?;
|
||||
{
|
||||
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/Buffer::new] Device with role 0 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) {
|
||||
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/Buffer::new] Device with role 0 is not an EK1100",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
let mut device_guard = el7041.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
|
||||
// Role 1
|
||||
// 1x Stepper Buffer
|
||||
// EL7041-0052
|
||||
let (el7041, _el7041_config) = {
|
||||
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/Buffer::new] Device with role 1 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) {
|
||||
EL7041_0052_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7041_0052>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 1 is not an EL7041-0052",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7041_0052Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 6000,
|
||||
..Default::default()
|
||||
},
|
||||
// Role 2 - Stepper Puller EL7031-0030
|
||||
let (el7031, subdevice) = get_ethercat_device::<EL7031_0030>(
|
||||
hardware,
|
||||
params,
|
||||
2,
|
||||
[EL7031_0030_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7031_config = EL7031_0030Configuration {
|
||||
stm_features: ethercat_hal::devices::el7031_0030::coe::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 8cm diameter = approx 75 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 1500,
|
||||
..Default::default()
|
||||
},
|
||||
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Role 2
|
||||
// 1x Stepper Puller
|
||||
// EL7031
|
||||
let (_el7031_0030, _el7031_0030_config) = {
|
||||
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/Buffer::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) {
|
||||
EL7031_0030_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7031_0030>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer1::new] Device with role 2 is not an EL7031-0030",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7031_0030Configuration {
|
||||
stm_features: el7031_0030::coe::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 8cm diameter = approx 75 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 1500,
|
||||
..Default::default()
|
||||
},
|
||||
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
};
|
||||
el7031
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &el7031_config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = el7031.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
|
||||
// Controller
|
||||
let buffer_tower_controller = BufferTowerController::new(StepperVelocityEL70x1::new(
|
||||
@@ -210,23 +127,22 @@ impl MachineNewTrait for BufferV1 {
|
||||
EL7041_0052Port::STM1,
|
||||
));
|
||||
|
||||
let machine_id = params
|
||||
.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
let machine_identification_unique = params.get_machine_identification_unique();
|
||||
|
||||
// create buffer instance
|
||||
let mut buffer: Self = Self {
|
||||
namespace: Buffer1Namespace::new(params.socket_queue_tx.clone()),
|
||||
let mut buffer: BufferV1 = Self {
|
||||
namespace: Buffer1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: Instant::now(),
|
||||
mode: BufferV1Mode::Standby,
|
||||
buffer_tower_controller,
|
||||
machine_manager: params.machine_manager.clone(),
|
||||
machine_identification_unique: machine_id,
|
||||
connected_winder: None,
|
||||
machine_identification_unique: machine_identification_unique.clone(),
|
||||
connected_winder: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
&machine_identification_unique,
|
||||
),
|
||||
};
|
||||
buffer.emit_state();
|
||||
Ok(buffer)
|
||||
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user