Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andTheBest6337 97dca1d56d Fix tension arm multi-sensor data corruption bug
Co-authored-by: TheBest6337 <80114847+TheBest6337@users.noreply.github.com>
2025-11-24 13:43:01 +00:00
copilot-swe-agent[bot] 6f1e34e8dc Initial plan 2025-11-24 13:35:55 +00:00
Christian 2d366d1ac7 Refactor PID tuning parameters for temperature controllers in Gluetex machine 2025-11-24 14:35:32 +01:00
Christian bf5c584594 Add heating control functions for Gluetex machine and update state handling 2025-11-24 14:33:06 +01:00
Christian ca6f7185e3 Add tension arm monitoring functionality to Gluetex machine 2025-11-24 13:38:19 +01:00
Christian f51b14a382 Update navigation in MachinesPage to use dynamic serial parameter 2025-11-24 13:27:20 +01:00
Christian 5469a4e993 Add addon tension arm functionality to Gluetex machine 2025-11-24 13:24:34 +01:00
Christian dc56396c89 Refactor tension arm handling in GluetexAddonsPage to use slave tension arm variables 2025-11-24 13:13:34 +01:00
Christian ca66187dd0 Add heating control functionality to Gluetex machine
- Introduced heating enabled state management in the Gluetex API and emit modules.
- Updated the Gluetex state schema to include heating states.
- Implemented helper functions to derive heating mode from backend state.
- Enhanced useGluetex hook to manage heating state updates.
2025-11-24 13:09:15 +01:00
Christian 88f4ebbee9 Update Gluetex routing and machine identification handling to support dynamic serial numbers 2025-11-23 18:17:54 +01:00
Christian 59796e8970 Update Gluetex machine configuration to use EL7031-0030 for addon motors 3 and 4 2025-11-23 18:08:10 +01:00
Christian 17138e06ee Update Gluetex machine identity vectors to include EL7031_0030_IDENTITY_A 2025-11-23 17:54:41 +01:00
Christian 9ab498cbd4 Add additional Gluetex machine configurations and update error messages for identity mismatches 2025-11-23 17:41:41 +01:00
Christian 88890288cd Add vendor ID to Gluetex machine properties and update role assignment logic in DeviceEepromDialogContent 2025-11-23 17:25:09 +01:00
Christian 10eb564c7f Fix device role assignment in DeviceEepromDialogContent to use actual role number 2025-11-23 17:08:14 +01:00
Christian b8a4eda728 Update Gluetex machine roles and device configurations for EL2008 2025-11-23 16:49:32 +01:00
Christian 93b6f26344 Add Gluetex machine properties and device roles 2025-11-23 16:32:43 +01:00
Christian 051fbc7642 Add heating PID settings and controls for multiple zones in Gluetex 2025-11-20 18:18:41 +01:00
Christian 4ffbbc61e4 Format LinearStepConverter initialization for better readability in tests 2025-11-20 17:41:54 +01:00
Christian acbc9e3cf7 Update LinearStepConverter initialization in tests to use from_circumference method 2025-11-20 17:40:11 +01:00
Christian ad3d0b620f Remove unused import and update function parameter name in SlavePullerSpeedController 2025-11-20 17:37:07 +01:00
Christian 35f2ead11b Implement slave puller functionality with state management and control in Gluetex 2025-11-20 17:29:23 +01:00
Christian a13e757a98 Add addon motor support with state management and control functions, remove cutting unit 2025-11-20 16:57:27 +01:00
Christian cb7fa3cff8 Merge pull request #912 from qitechgmbh/master
sync to master
2025-11-20 16:01:28 +01:00
Robin Krämer 5e86988ac4 Merge pull request #907 from qitechgmbh/882-add-nozzle-docs-for-extruder
882 add nozzle docs for extruder
2025-11-19 11:38:07 +01:00
snacj 2d28c18b1a Added Heading for new Mechanical Diagram 2025-11-18 14:04:32 +01:00
snacj b19764281a Added Nozzle docs 2025-11-18 13:58:51 +01:00
snacj c25d63f781 Added Nozzle docs 2025-11-18 13:58:51 +01:00
Robin Krämer d79efc412f Merge pull request #904 from qitechgmbh/898-mock-machine-doesnt-work
898 mock machine doesnt work
2025-11-18 12:53:24 +01:00
Christian fde8a7f01b Implement heating control features for Gluetex machine, including temperature controllers and power management for heating zones. 2025-11-17 18:40:36 +01:00
Christian 448e8c5d68 Add temperature sensor integration to Gluetex machine and update UI components 2025-11-17 18:18:34 +01:00
rob 7ad74cf089 fixed formatting 2025-11-17 18:00:43 +01:00
rob 6073d37892 fixed remaining compile errors 2025-11-17 18:00:43 +01:00
rob c04937553f machines show up but dont get subscribed to 2025-11-17 18:00:43 +01:00
rob e08bdfa3c7 removed unnecessary and outdated docs 2025-11-17 18:00:43 +01:00
rob d282678b19 fixed compile errors for mock-machines 2025-11-17 18:00:43 +01:00
Robin Krämer 9ff4243350 Merge pull request #902 from qitechgmbh/2.12-releasenotes
updated Changelog
2025-11-17 16:38:48 +01:00
rob 6c79b33752 updated Changelog 2025-11-17 16:36:41 +01:00
rob 861b5da7a3 updated Changelog 2025-11-17 16:35:28 +01:00
Christian 771284c23d Implement rust backend for default winding operations 2025-11-16 13:50:09 +01:00
Christian 7d230defea Add Gluetex Heaters page and integrate simulated temperature and power data 2025-11-15 16:30:53 +01:00
Christian caf4ae47a0 Implement Gluetex Addons page and integrate motor ratio controls 2025-11-15 16:22:33 +01:00
Christian 8513201080 Add temperature control features to Gluetex machine interface 2025-11-15 14:19:24 +01:00
Christian b080414bf2 Add default winder to gluetex device with dummy data 2025-11-15 13:56:32 +01:00
Robin Krämer 7dd3d50653 Merge pull request #899 from qitechgmbh/add-debugging-utils-folder
added debugging utils and fixed memory leak
2025-11-14 23:30:55 +01:00
kraemr ba8e04c641 removed unused imports 2025-11-14 23:28:05 +01:00
kraemr cece245a55 removed one_hour caching on backend for apis 2025-11-14 21:26:38 +01:00
kraemr fab4d2810e fixed memory leak and added debugging utilities 2025-11-14 21:18:51 +01:00
Robin Krämer 5bff3ca929 Merge pull request #897 from qitechgmbh/fix-async-yield-timeout
Fix async yield timeout
2025-11-14 16:45:59 +01:00
kraemr afe1d3b80e fixed display of time taken on failing loop 2025-11-14 14:06:32 +01:00
kraemr 6c8005dba9 log cycle time on failure 2025-11-14 13:36:50 +01:00
kraemr 9f399ca6f3 fixed compile error 2025-11-14 13:25:44 +01:00
kraemr a7574dd348 better error handling for copy ethercat outputs and inputs 2025-11-14 13:22:27 +01:00
kraemr b39f3597c8 fixed loop 2025-11-14 12:20:53 +01:00
79 changed files with 12066 additions and 569 deletions

No files matched your search

+16
View File
@@ -1,3 +1,19 @@
# `2.12.0`
_17.11.2025_
## General
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
## Laser
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
## Extruder V2
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
# `2.11.0`
_21.10.2025_
Generated
+145
View File
@@ -356,6 +356,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.1"
@@ -654,6 +663,17 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctrlc"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790"
dependencies = [
"dispatch2",
"nix 0.30.1",
"windows-sys 0.61.2",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
@@ -712,6 +732,22 @@ dependencies = [
"syn 2.0.105",
]
[[package]]
name = "dhat"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827"
dependencies = [
"backtrace",
"lazy_static",
"mintex",
"parking_lot",
"rustc-hash",
"serde",
"serde_json",
"thousands",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -722,6 +758,18 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
]
[[package]]
name = "document-features"
version = "0.2.11"
@@ -1532,6 +1580,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
@@ -1636,6 +1693,12 @@ dependencies = [
"adler2",
]
[[package]]
name = "mintex"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536"
[[package]]
name = "mio"
version = "1.0.3"
@@ -1688,6 +1751,18 @@ dependencies = [
"memoffset 0.9.1",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "no-std-net"
version = "0.6.0"
@@ -1722,6 +1797,21 @@ dependencies = [
"libc",
]
[[package]]
name = "objc2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "object"
version = "0.36.7"
@@ -1749,6 +1839,29 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link 0.2.1",
]
[[package]]
name = "paste"
version = "1.0.15"
@@ -2010,6 +2123,15 @@ dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "regex"
version = "1.11.3"
@@ -2076,6 +2198,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.44"
@@ -2219,6 +2347,8 @@ dependencies = [
"axum",
"bitvec",
"control_core",
"ctrlc",
"dhat",
"ethercat_hal",
"ethercrab",
"futures",
@@ -2238,6 +2368,7 @@ dependencies = [
"tracing-journald",
"tracing-subscriber",
"units",
"utils",
]
[[package]]
@@ -2515,6 +2646,12 @@ dependencies = [
"syn 2.0.105",
]
[[package]]
name = "thousands"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread_local"
version = "1.1.8"
@@ -2879,6 +3016,14 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utils"
version = "0.1.0"
dependencies = [
"backtrace",
"dhat",
]
[[package]]
name = "valuable"
version = "0.1.1"
+1
View File
@@ -8,6 +8,7 @@ members = [
#"ethercat-eeprom-dump",
"control-core-derive",
"units",
"utils",
]
exclude = []
resolver = "2"
Binary file not shown.
+12 -36
View File
@@ -175,7 +175,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -639,7 +638,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -662,7 +660,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1148,7 +1145,6 @@
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.1.1",
"fs-extra": "^9.0.1",
@@ -4095,7 +4091,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
@@ -4177,7 +4172,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4411,7 +4405,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4568,7 +4563,6 @@
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -4585,7 +4579,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4595,7 +4588,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4678,7 +4670,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@@ -5033,7 +5024,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5481,7 +5471,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -6174,8 +6163,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -6448,7 +6436,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7448,7 +7437,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7509,7 +7497,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8816,7 +8803,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -8879,7 +8865,6 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -9554,7 +9539,6 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10120,6 +10104,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12064,7 +12049,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12178,6 +12162,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12192,6 +12177,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12320,7 +12306,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12329,7 +12314,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -12341,7 +12325,6 @@
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -12382,7 +12365,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -13133,7 +13117,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -13923,8 +13906,7 @@
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"peer": true
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -14026,8 +14008,7 @@
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"peer": true
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@@ -14078,7 +14059,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14390,7 +14370,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14752,7 +14731,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14874,7 +14852,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15418,7 +15395,6 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -720,3 +720,7 @@ The following persons must not work on the machine:
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
## Mechanical Diagrams
![QiTech Pro Extruder Nozzle 2025](images/manuals/extruder/Nozzle.png)
-1
View File
@@ -353,7 +353,6 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
namespaceId.machine_identification_unique.serial,
);
//console.log(machineExists);
socket.connect();
if (machineExists && !socket.connected) {
socket.connect();
clearInterval(intervalId); // stop polling
@@ -0,0 +1,427 @@
import React, { useState, useCallback, useEffect } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Separator } from "@/components/ui/separator";
type RatioInputProps = {
master?: number;
slave?: number;
onRatioChange?: (master: number, slave: number) => void;
title?: string;
min?: number;
max?: number;
step?: number;
};
const ratioSchema = z.object({
master: z.number().min(0.1).max(1000),
slave: z.number().min(0.1).max(1000),
});
type RatioFormData = z.infer<typeof ratioSchema>;
export function RatioInput({
master = 1,
slave = 1,
onRatioChange,
title = "Motor Ratio",
min = 0.1,
max = 1000,
step = 0.1,
}: RatioInputProps) {
const [isOpen, setIsOpen] = useState(false);
const [activeField, setActiveField] = useState<"master" | "slave" | null>(
null,
);
const [inputValue, setInputValue] = useState<string>("");
const getInitialValues = useCallback(() => {
return {
master: master ?? 1,
slave: slave ?? 1,
};
}, [master, slave]);
const form = useForm<RatioFormData>({
resolver: zodResolver(ratioSchema),
defaultValues: getInitialValues(),
mode: "all",
});
// Initialize form with current values when dialog opens
useEffect(() => {
if (isOpen) {
const initialValues = getInitialValues();
form.reset(initialValues);
setActiveField(null);
setInputValue("");
}
}, [isOpen, getInitialValues, form]);
const formatRatioDisplay = useCallback(() => {
const m = master ?? 1;
const s = slave ?? 1;
return `${m.toFixed(1)} : ${s.toFixed(1)}`;
}, [master, slave]);
// Increment/decrement functions with bounds checking
const adjustRatioValue = useCallback(
(field: "master" | "slave", increment: boolean) => {
const currentValue = form.getValues(field);
let newValue = increment ? currentValue + step : currentValue - step;
// Round to avoid floating point precision issues
newValue = Math.round(newValue * 10) / 10;
// Clamp to min/max bounds
if (newValue < min) {
newValue = min;
} else if (newValue > max) {
newValue = max;
}
form.setValue(field, newValue, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
},
[form, step, min, max],
);
const handleNumpadClick = (digit: string) => {
if (activeField === null) return;
const newValue = inputValue + digit;
setInputValue(newValue);
};
const handleDecimal = () => {
if (activeField === null) return;
if (!inputValue.includes(".")) {
setInputValue(inputValue === "" ? "0." : inputValue + ".");
}
};
const handleBackspace = () => {
setInputValue(inputValue.slice(0, -1));
};
const handleClear = () => {
setInputValue("");
};
const handleNumpadConfirm = () => {
if (activeField === null || inputValue === "") return;
let numValue = parseFloat(inputValue);
// Clamp to min/max bounds
if (numValue < min) {
numValue = min;
} else if (numValue > max) {
numValue = max;
}
form.setValue(activeField, numValue, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
setInputValue("");
setActiveField(null);
};
const handleFieldClick = (field: "master" | "slave") => {
if (activeField === field) {
// If same field, submit and close
handleNumpadConfirm();
} else {
// Switch to new field
if (activeField !== null && inputValue !== "") {
handleNumpadConfirm();
}
setActiveField(field);
setInputValue("");
}
};
const handleSubmit = () => {
if (activeField !== null && inputValue !== "") {
handleNumpadConfirm();
return;
}
form.handleSubmit((data) => {
onRatioChange?.(data.master, data.slave);
setIsOpen(false);
})();
};
const handleAbort = () => {
const initialValues = getInitialValues();
form.reset(initialValues);
setActiveField(null);
setInputValue("");
setIsOpen(false);
};
const handleReset = () => {
form.setValue("master", 1, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
form.setValue("slave", 1, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
setActiveField(null);
setInputValue("");
};
// Watch individual form fields for real-time updates
const masterValue = useWatch({
control: form.control,
name: "master",
defaultValue: getInitialValues().master,
});
const slaveValue = useWatch({
control: form.control,
name: "slave",
defaultValue: getInitialValues().slave,
});
// Get current form values with fallback
const currentValues = {
master: masterValue ?? 1,
slave: slaveValue ?? 1,
};
// RatioValueInput component for individual ratio fields
const RatioValueInput = ({
field,
label,
value,
}: {
field: "master" | "slave";
label: string;
value: number;
}) => {
const isActive = activeField === field;
const displayValue =
isActive && inputValue !== "" ? inputValue : value.toFixed(1);
return (
<div className="flex flex-col items-center gap-3">
<label className="text-sm font-medium text-gray-700">{label}</label>
<div className="flex flex-col items-center gap-2">
<TouchButton
variant="outline"
size="sm"
className="h-10 w-20 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustRatioValue(field, true);
}}
disabled={value >= max}
>
<Icon name="lu:ChevronUp" className="size-4" />
</TouchButton>
<div
onClick={() => handleFieldClick(field)}
className={`flex h-16 w-24 cursor-pointer items-center justify-center rounded border-2 font-mono text-3xl transition-colors ${
isActive
? "border-blue-500 bg-blue-50"
: "border-gray-300 bg-white"
}`}
>
{displayValue}
</div>
<TouchButton
variant="outline"
size="sm"
className="h-10 w-20 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustRatioValue(field, false);
}}
disabled={value <= min}
>
<Icon name="lu:ChevronDown" className="size-4" />
</TouchButton>
</div>
</div>
);
};
// Numpad component
const Numpad = () => (
<div className="flex flex-col gap-3">
{/* Display showing input value */}
<div className="flex h-12 items-center justify-center rounded border-2 border-gray-300 bg-gray-50 font-mono text-xl">
{inputValue === "" ? "0" : inputValue}
</div>
{/* Numpad grid */}
<div className="grid grid-cols-3 gap-2">
{[
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
[".", "0", "DEL"],
].map((row, i) => (
<div key={i} className="contents">
{row.map((key) => (
<TouchButton
key={key}
variant="outline"
className="h-12 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (key === "DEL") {
handleBackspace();
} else if (key === ".") {
handleDecimal();
} else {
handleNumpadClick(key);
}
}}
>
{key === "DEL" ? (
<Icon name="lu:Delete" className="size-4" />
) : (
key
)}
</TouchButton>
))}
</div>
))}
</div>
{/* Control buttons */}
<div className="flex gap-2">
<TouchButton
variant="outline"
className="h-10 flex-1"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleClear();
}}
>
C
</TouchButton>
<TouchButton
className="h-10 flex-1 bg-green-600 text-white hover:bg-green-700"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleNumpadConfirm();
}}
disabled={activeField === null || inputValue === ""}
>
<Icon name="lu:Check" className="size-4" />
</TouchButton>
</div>
</div>
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
>
<Icon name="lu:Settings" className="mr-2 size-4" />
{formatRatioDisplay()}
</TouchButton>
</DialogTrigger>
<DialogContent className="max-w-4xl p-8">
<div className="flex flex-col gap-8">
{/* Header */}
<div className="flex min-h-[2.5rem] items-center justify-between">
<div className="flex items-center gap-3">
<Icon name="lu:Settings" className="size-6" />
<h3 className="text-xl font-semibold">{title}</h3>
</div>
<TouchButton
variant="ghost"
size="sm"
onClick={handleReset}
className="text-blue-600 hover:text-blue-700"
>
<Icon name="lu:RotateCcw" className="mr-2 size-4" />
Reset to 1:1
</TouchButton>
</div>
{/* Description */}
<div className="-mt-4 text-base text-gray-600">
Set the gear ratio. For every {currentValues.master.toFixed(1)}{" "}
rotation(s) of the master puller, the motor will make{" "}
{currentValues.slave.toFixed(1)} rotation(s).
</div>
<Separator />
{/* Main content: Ratio inputs and Numpad */}
<div className="flex gap-8">
{/* Ratio inputs */}
<div className="flex flex-1 items-center justify-center gap-8">
<RatioValueInput
field="master"
label="Master"
value={currentValues.master}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<RatioValueInput
field="slave"
label="Slave"
value={currentValues.slave}
/>
</div>
{/* Numpad */}
<div className="w-40">
<Numpad />
</div>
</div>
<Separator />
{/* Action buttons */}
<div className="flex gap-4">
<TouchButton
variant="outline"
className="h-14 flex-1"
onClick={handleAbort}
>
<Icon name="lu:X" className="mr-2 size-4" />
Cancel
</TouchButton>
<TouchButton
className="h-14 flex-1 bg-blue-600 text-white hover:bg-blue-700"
onClick={handleSubmit}
>
<Icon name="lu:Check" className="mr-2 size-4" />
Apply
</TouchButton>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -110,8 +110,7 @@ export function buffer1MessageHandler(
state: stateEvent,
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timestamp = event.ts;
liveValuesEventSchema.parse(event);
updateStore((state) => ({
...state,
}));
+218 -3
View File
@@ -90,13 +90,18 @@ export const winder2: MachineProperties = {
],
},
{
role: 4,
role_label: "1x Stepper Puller",
role: 10,
role_label: "Slave Puller",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x10001e,
revision: 0x1a0000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x190000,
},
],
},
@@ -340,6 +345,215 @@ export const aquapath1: MachineProperties = {
],
};
export const gluetex: MachineProperties = {
name: "Gluetex",
version: "V1",
slug: "gluetex",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x000a,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "2x Digital Output",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d23052,
revision: 0x110000,
},
{
vendor_id: 2,
product_id: 0x7d23052,
revision: 0x120000,
},
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x110000,
},
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x120000,
},
],
},
{
role: 2,
role_label: "1x Stepper Spool EL7041-0052",
allowed_devices: [
{
vendor_id: 2,
product_id: 461451346,
revision: 0x100034,
},
],
},
{
role: 3,
role_label: "1x Stepper Traverse",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x1a0000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x190000,
},
],
},
{
role: 4,
role_label: "1x Stepper Puller",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
{
role: 5,
role_label: "Temperature Sensors 1-4",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x160000,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x150000,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x1a0000,
},
],
},
{
role: 6,
role_label: "Temperature Sensors 5-6",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x160000,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x150000,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x1a0000,
},
],
},
{
role: 7,
role_label: "Digital Outputs for Heater SSRs",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x110000,
},
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x120000,
},
],
},
{
role: 8,
role_label: "Addon Motor 3",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x1a0000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x190000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
{
role: 9,
role_label: "Addon Motor 4",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x1a0000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x190000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
{
role: 10,
role_label: "Slave Puller",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x1a0000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x190000,
},
{
vendor_id: 2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder2,
@@ -347,6 +561,7 @@ export const machineProperties: MachineProperties[] = [
mock1,
buffer1,
aquapath1,
gluetex,
];
export const getMachineProperties = (
@@ -0,0 +1,65 @@
import { cn } from "@/utils/tailwind";
import React from "react";
type Props = {
min: number;
max: number;
minLimit: number;
maxLimit: number;
current: number;
className?: string;
};
export function TemperatureBar({
min,
max,
minLimit,
maxLimit,
current,
className,
}: Props) {
// Calculate percentages for positioning
const range = max - min;
const minLimitPercent = ((minLimit - min) / range) * 100;
const maxLimitPercent = ((maxLimit - min) / range) * 100;
const currentPercent = ((current - min) / range) * 100;
// Determine if current value is within limits
const isInRange = current >= minLimit && current <= maxLimit;
return (
<div className={cn("w-full space-y-1 pt-6", className)}>
<div className="relative h-4 w-full">
{/* Base bar with overflow hidden to clip inner bar */}
<div className="absolute h-full w-full overflow-hidden rounded-full bg-gray-200">
{/* Valid range (green section) */}
<div
className="absolute h-full bg-green-600"
style={{
left: `${minLimitPercent}%`,
width: `${maxLimitPercent - minLimitPercent}%`,
}}
/>
</div>
{/* Current temperature indicator - outside the overflow container so it's not clipped */}
<div
className={cn(
"absolute -top-5 z-10 h-14 w-1.5 rounded-full",
isInRange ? "bg-green-600" : "bg-red-600",
)}
style={{
left: `${currentPercent}%`,
transform: "translateX(-50%)",
}}
/>
</div>
{/* Labels */}
<div className="flex justify-between text-xs text-gray-400">
<span>MIN</span>
<span>MAX</span>
</div>
</div>
);
}
@@ -0,0 +1,349 @@
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 { TemperatureBar } from "../TemperatureBar";
import { SelectionGroup } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { TouchButton } from "@/components/touch/TouchButton";
import { StatusBadge } from "@/control/StatusBadge";
import { useGluetex } from "./useGluetex";
import {
StepperMode,
HeatingMode,
getGearRatioMultiplier,
} from "./gluetexNamespace";
import { roundToDecimals, roundDegreesToDecimals } from "@/lib/decimal";
import { TensionArm } from "../TensionArm";
import { RatioInput } from "@/components/ratio/RatioInput";
export function GluetexAddonsPage() {
const {
state,
defaultState,
isLoading,
isDisabled,
setStepper3Mode,
setStepper4Mode,
setHeatingMode,
temperature1,
temperature2,
setTemperature1Min,
setTemperature1Max,
setTemperature2Min,
setTemperature2Max,
slavePullerSpeed,
slaveTensionArmAngle,
addonTensionArmAngle,
zeroSlaveTensionArm,
zeroAddonTensionArm,
setStepper3Master,
setStepper3Slave,
setStepper4Master,
setStepper4Slave,
} = useGluetex();
// Calculate max speed based on gear ratio (same as main puller)
const gearRatioMultiplier = getGearRatioMultiplier(
state?.puller_state?.gear_ratio,
);
return (
<Page>
<ControlGrid>
<ControlCard className="bg-red" title="Motors">
<Label label="Stepper 3">
<SelectionGroup<StepperMode>
value={state?.stepper_state?.stepper3_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setStepper3Mode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:Power",
isActiveClassName: "bg-green-600",
},
Run: {
children: "Run",
icon: "lu:Play",
isActiveClassName: "bg-green-600",
},
}}
/>
</Label>
<Label label="Stepper 4">
<SelectionGroup<StepperMode>
value={state?.stepper_state?.stepper4_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setStepper4Mode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:Power",
isActiveClassName: "bg-green-600",
},
Run: {
children: "Run",
icon: "lu:Play",
isActiveClassName: "bg-green-600",
},
}}
/>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Heating Mode">
<SelectionGroup<HeatingMode>
value={state?.heating_state?.heating_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setHeatingMode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:Power",
isActiveClassName: "bg-green-600",
},
Heating: {
children: "Heating",
icon: "lu:Flame",
isActiveClassName: "bg-green-600",
},
}}
/>
</ControlCard>
<ControlCard className="bg-red" height={2} title="Quality Control">
<div className="space-y-6">
{/* Temperature 1 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<TimeSeriesValueNumeric
label="Temperature 1"
unit="C"
timeseries={temperature1}
renderValue={(value) => roundToDecimals(value, 1)}
/>
</div>
<TemperatureBar
min={0}
max={150}
minLimit={
state?.quality_control_state?.temperature1.min_temperature ??
0
}
maxLimit={
state?.quality_control_state?.temperature1.max_temperature ??
150
}
current={temperature1.current?.value ?? 0}
/>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Min Temperature">
<EditValue
value={
state?.quality_control_state?.temperature1.min_temperature
}
unit="C"
title="Min Temperature 1"
defaultValue={
defaultState?.quality_control_state?.temperature1
.min_temperature
}
min={0}
max={Math.min(
149,
(state?.quality_control_state?.temperature1
.max_temperature ?? 150) - 1,
)}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setTemperature1Min}
/>
</Label>
<Label label="Max Temperature">
<EditValue
value={
state?.quality_control_state?.temperature1.max_temperature
}
unit="C"
title="Max Temperature 1"
defaultValue={
defaultState?.quality_control_state?.temperature1
.max_temperature
}
min={Math.max(
1,
(state?.quality_control_state?.temperature1
.min_temperature ?? 0) + 1,
)}
max={150}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setTemperature1Max}
/>
</Label>
</div>
</div>
{/* Temperature 2 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<TimeSeriesValueNumeric
label="Temperature 2"
unit="C"
timeseries={temperature2}
renderValue={(value) => roundToDecimals(value, 1)}
/>
</div>
<TemperatureBar
min={0}
max={200}
minLimit={
state?.quality_control_state?.temperature2.min_temperature ??
0
}
maxLimit={
state?.quality_control_state?.temperature2.max_temperature ??
200
}
current={temperature2.current?.value ?? 0}
/>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Min Temperature">
<EditValue
value={
state?.quality_control_state?.temperature2.min_temperature
}
unit="C"
title="Min Temperature 2"
defaultValue={
defaultState?.quality_control_state?.temperature2
.min_temperature
}
min={0}
max={Math.min(
199,
(state?.quality_control_state?.temperature2
.max_temperature ?? 200) - 1,
)}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setTemperature2Min}
/>
</Label>
<Label label="Max Temperature">
<EditValue
value={
state?.quality_control_state?.temperature2.max_temperature
}
unit="C"
title="Max Temperature 2"
defaultValue={
defaultState?.quality_control_state?.temperature2
.max_temperature
}
min={Math.max(
1,
(state?.quality_control_state?.temperature2
.min_temperature ?? 0) + 1,
)}
max={200}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setTemperature2Max}
/>
</Label>
</div>
</div>
</div>
</ControlCard>
<ControlCard title="Tension Arm">
<TensionArm degrees={slaveTensionArmAngle.current?.value} />
<TimeSeriesValueNumeric
label="Tension Arm"
unit="deg"
timeseries={slaveTensionArmAngle}
renderValue={(value) => roundDegreesToDecimals(value, 0)}
/>
<TimeSeriesValueNumeric
label="Slave Puller Speed"
unit="m/min"
timeseries={slavePullerSpeed}
renderValue={(value) => roundToDecimals(value, 1)}
/>
<TouchButton
variant="outline"
icon="lu:House"
onClick={zeroSlaveTensionArm}
disabled={isDisabled}
isLoading={isLoading}
>
Set Zero Point
</TouchButton>
{!state?.slave_puller_state?.tension_arm?.zeroed && (
<StatusBadge variant="error">Not Zeroed</StatusBadge>
)}
</ControlCard>
<ControlCard className="bg-red" title="Motor Ratios">
<Label label="Stepper 3 Ratio">
<RatioInput
master={state?.motor_ratios_state?.stepper3_master}
slave={state?.motor_ratios_state?.stepper3_slave}
title="Stepper 3 Motor Ratio"
onRatioChange={(master, slave) => {
setStepper3Master(master);
setStepper3Slave(slave);
}}
/>
</Label>
<Label label="Stepper 4 Ratio">
<RatioInput
master={state?.motor_ratios_state?.stepper4_master}
slave={state?.motor_ratios_state?.stepper4_slave}
title="Stepper 4 Motor Ratio"
onRatioChange={(master, slave) => {
setStepper4Master(master);
setStepper4Slave(slave);
}}
/>
</Label>
</ControlCard>
<ControlCard title="Addon Tension Arm">
<TensionArm degrees={addonTensionArmAngle.current?.value} />
<TimeSeriesValueNumeric
label="Angle"
unit="deg"
timeseries={addonTensionArmAngle}
renderValue={(value) => roundDegreesToDecimals(value, 0)}
/>
<TouchButton
variant="outline"
icon="lu:House"
onClick={zeroAddonTensionArm}
disabled={isDisabled}
isLoading={isLoading}
>
Set Zero Point
</TouchButton>
{!state?.addon_tension_arm_state?.zeroed && (
<StatusBadge variant="error">Not Zeroed</StatusBadge>
)}
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,375 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React, { useState } from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { TraverseBar } from "../TraverseBar";
import {
SelectionGroup,
SelectionGroupBoolean,
} from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { TouchButton } from "@/components/touch/TouchButton";
import { StatusBadge } from "@/control/StatusBadge";
import { useGluetex } from "./useGluetex";
import {
Mode,
SpoolAutomaticActionMode,
getGearRatioMultiplier,
} from "./gluetexNamespace";
import { TensionArm } from "../TensionArm";
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
import { Spool } from "../Spool";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { getGluetexTraverseMax } from "./gluetexConfig";
export function GluetexControlPage() {
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
const traverseMax = getGluetexTraverseMax();
// use optimistic state
const {
state,
defaultState,
enableTraverseLaserpointer,
tensionArmAngle,
zeroTensionArmAngle,
spoolRpm,
setMode,
pullerSpeed,
spoolProgress,
setPullerTargetSpeed,
traversePosition,
resetSpoolProgress,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
isLoading,
isDisabled,
} = useGluetex();
// Calculate max speed based on gear ratio
const gearRatioMultiplier = getGearRatioMultiplier(
state?.puller_state?.gear_ratio,
);
const maxMotorSpeed = 75; // Maximum motor speed in m/min
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
const handleResetProgress = () => {
// Check if the machine is currently in Wind mode
if (state?.mode_state?.mode === "Wind") {
setShowResetConfirmDialog(true);
} else {
resetSpoolProgress();
}
};
const confirmResetProgress = () => {
resetSpoolProgress();
setShowResetConfirmDialog(false);
};
return (
<Page>
<ControlGrid>
<ControlCard title="Spool">
<Spool rpm={spoolRpm.current?.value} />
<TimeSeriesValueNumeric
label="Spool Speed"
unit="rpm"
timeseries={spoolRpm}
renderValue={(value) => roundToDecimals(value, 0)}
/>
</ControlCard>
<ControlCard className="bg-red" height={2} title="Traverse">
<TimeSeriesValueNumeric
label="Position"
unit="mm"
timeseries={traversePosition}
renderValue={(value) => roundToDecimals(value, 1)}
/>
{state?.traverse_state && (
<TraverseBar
inside={0}
outside={traverseMax}
min={state?.traverse_state.limit_inner}
max={state?.traverse_state.limit_outer}
current={traversePosition.current?.value ?? 0}
/>
)}
<div className="flex flex-row flex-wrap gap-4">
<Label label="Outer Limit">
<EditValue
value={state?.traverse_state?.limit_outer}
unit="mm"
title="Outer Limit"
defaultValue={defaultState?.traverse_state?.limit_outer}
// Traverse limit validation: Outer limit must be at least 0.9mm greater than inner limit
// We use 1mm buffer to ensure the backend validation (which requires >0.9mm) will pass
// Formula: min_outer = inner_limit + 1mm
min={Math.max(0, (state?.traverse_state?.limit_inner ?? 0) + 1)}
minLabel="IN"
maxLabel="OUT"
max={traverseMax}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setTraverseLimitOuter}
/>
<TouchButton
variant="outline"
icon="lu:ArrowLeftToLine"
onClick={gotoTraverseLimitOuter}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Outer Limit
</TouchButton>
</Label>
<Label label="Inner Limit">
<EditValue
value={state?.traverse_state?.limit_inner}
unit="mm"
title="Inner Limit"
min={0}
// Traverse limit validation: Inner limit must be at least 0.9mm smaller than outer limit
// We use 1mm buffer to ensure the backend validation (which requires outer > inner + 0.9mm) will pass
// Formula: max_inner = outer_limit - 1mm
max={Math.min(
traverseMax,
(state?.traverse_state?.limit_outer ?? traverseMax) - 1,
)}
defaultValue={defaultState?.traverse_state?.limit_inner}
minLabel="IN"
maxLabel="OUT"
renderValue={(value) => roundToDecimals(value, 0)}
inverted
onChange={setTraverseLimitInner}
/>
<TouchButton
variant="outline"
icon="lu:ArrowRightToLine"
onClick={gotoTraverseLimitInner}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Inner Limit
</TouchButton>
</Label>
</div>
<Label label="Laserpointer">
<SelectionGroupBoolean
value={state?.traverse_state.laserpointer}
disabled={isLoading || isDisabled}
loading={isLoading}
optionFalse={{ children: "Off", icon: "lu:LightbulbOff" }}
optionTrue={{ children: "On", icon: "lu:Lightbulb" }}
onChange={enableTraverseLaserpointer}
/>
</Label>
<Label label="Home">
<TouchButton
variant="outline"
icon="lu:House"
onClick={() => gotoTraverseHome()}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Home
</TouchButton>
{state?.traverse_state?.is_homed !== true ? (
<StatusBadge variant={"error"}>{"Not Homed"}</StatusBadge>
) : null}
</Label>
</ControlCard>
<ControlCard title="Tension Arm">
<TensionArm degrees={tensionArmAngle.current?.value} />
<TimeSeriesValueNumeric
label="Tension Arm"
unit="deg"
timeseries={tensionArmAngle}
renderValue={(value) => roundDegreesToDecimals(value, 0)}
/>
<TouchButton
variant="outline"
icon="lu:House"
onClick={zeroTensionArmAngle}
disabled={isDisabled}
isLoading={isLoading}
>
Set Zero Point
</TouchButton>
{!state?.tension_arm_state?.zeroed && (
<StatusBadge variant="error">Not Zeroed</StatusBadge>
)}
</ControlCard>
<ControlCard className="bg-red" title="Winder Mode">
<SelectionGroup<Mode>
value={state?.mode_state.mode}
disabled={isDisabled}
loading={isLoading}
onChange={setMode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:Power",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Hold: {
children: "Hold",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Pull: {
children: "Pull",
icon: "lu:ChevronsLeft",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Wind: {
children: "Wind",
icon: "lu:RefreshCcw",
isActiveClassName: "bg-green-600",
disabled: !state?.mode_state?.can_wind,
className: "h-full",
},
}}
/>
</ControlCard>
<ControlCard className="bg-red" title="Puller">
<TimeSeriesValueNumeric
label="Speed"
unit="m/min"
timeseries={pullerSpeed}
renderValue={(value) => roundToDecimals(value, 1)}
/>
<Label label="Target Speed">
<EditValue
value={state?.puller_state?.target_speed}
unit="m/min"
title="Target Speed"
defaultValue={defaultState?.puller_state?.target_speed}
min={0}
max={maxTargetSpeed}
step={0.1}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setPullerTargetSpeed}
/>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Spool Autostop">
<TimeSeriesValueNumeric
label="Pulled Distance"
renderValue={(value) => roundToDecimals(value, 2)}
unit="m"
timeseries={spoolProgress}
/>
<Label label="Target Length">
<EditValue
value={state?.spool_automatic_action_state.spool_required_meters}
unit="m"
title="Expected Meters"
defaultValue={250}
min={10}
max={10000}
step={10}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setSpoolAutomaticRequiredMeters}
/>
</Label>
<TouchButton
variant="outline"
onClick={handleResetProgress}
disabled={isDisabled}
isLoading={isLoading || state?.traverse_state?.is_going_out}
>
Reset Progress
</TouchButton>
<Label label="After Target Length Reached">
<SelectionGroup<SpoolAutomaticActionMode>
value={
state?.spool_automatic_action_state.spool_automatic_action_mode
}
disabled={isDisabled}
loading={isLoading}
onChange={setSpoolAutomaticAction}
orientation="vertical"
options={{
Hold: {
children: "Hold",
icon: "lu:CirclePause",
className: "h-full",
},
Pull: {
children: "Pull",
icon: "lu:ChevronsLeft",
className: "h-full",
},
NoAction: {
children: "No Action",
icon: "lu:RefreshCcw",
className: "h-full",
},
}}
/>
</Label>
</ControlCard>
</ControlGrid>
{/* Reset Progress Confirmation Dialog */}
<Dialog
open={showResetConfirmDialog}
onOpenChange={setShowResetConfirmDialog}
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Reset Spool Progress?</DialogTitle>
<DialogDescription>
The machine is currently in Wind mode. Are you sure you want to
reset the spool progress?
</DialogDescription>
</DialogHeader>
<div className="mt-4 flex flex-col gap-2">
<TouchButton
variant="destructive"
onClick={confirmResetProgress}
disabled={isLoading}
isLoading={isLoading}
>
Yes, Reset Progress
</TouchButton>
<TouchButton
variant="outline"
onClick={() => setShowResetConfirmDialog(false)}
>
Cancel
</TouchButton>
</div>
</DialogContent>
</Dialog>
</Page>
);
}
@@ -0,0 +1,290 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
type GraphLine,
} from "@/components/graph";
import React from "react";
import { useGluetex } from "./useGluetex";
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
import { TimeSeries } from "@/lib/timeseries";
import { Unit } from "@/control/units";
export function GluetexGraphsPage() {
const {
state,
spoolRpm,
traversePosition,
tensionArmAngle,
pullerSpeed,
spoolProgress,
} = useGluetex();
const syncHook = useGraphSync("gluetex-group");
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<div className="grid gap-4">
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
/>
<TensionArmAngleGraph
syncHook={syncHook}
newData={tensionArmAngle}
unit="deg"
renderValue={(value) => roundDegreesToDecimals(value, 0)}
/>
<SpoolRpmGraph
syncHook={syncHook}
newData={spoolRpm}
unit="rpm"
renderValue={(value) => roundToDecimals(value, 0)}
/>
<TraversePositionGraph
syncHook={syncHook}
newData={traversePosition}
limitInner={state?.traverse_state?.limit_inner}
limitOuter={state?.traverse_state?.limit_outer}
unit="mm"
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolProgressGraph
syncHook={syncHook}
newData={spoolProgress}
unit="m"
renderValue={(value) => roundToDecimals(value, 2)}
/>
</div>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
export function SpoolRpmGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Spool Speed",
icon: "lu:RotateCcw",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
background: "#ffffff",
},
exportFilename: "spool_rpm_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#10b981",
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="spool-rpm"
/>
);
}
export function TraversePositionGraph({
syncHook,
newData,
limitInner,
limitOuter,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
limitInner?: number;
limitOuter?: number;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const lines: GraphLine[] = [];
if (limitInner !== undefined) {
lines.push({
type: "threshold",
value: limitInner,
color: "#8b5cf6",
dash: [5, 5],
});
}
if (limitOuter !== undefined) {
lines.push({
type: "threshold",
value: limitOuter,
color: "#8b5cf6",
dash: [5, 5],
});
}
const config: GraphConfig = {
title: "Traverse Position",
icon: "lu:Move",
colors: {
primary: "#8b5cf6",
},
exportFilename: "traverse_position_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#8b5cf6",
lines,
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="traverse-position"
/>
);
}
export function TensionArmAngleGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Tension Arm Angle",
icon: "lu:RotateCw",
colors: {
primary: "#f59e0b",
},
exportFilename: "tension_arm_angle_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#f59e0b",
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="tension-arm-angle"
/>
);
}
export function SpoolProgressGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Spool Progress",
icon: "lu:RotateCw",
colors: {
primary: "#f59e0b",
},
exportFilename: "spool_progress",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#f59e0b",
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="spool-progress"
/>
);
}
export function PullerSpeedGraph({
syncHook,
newData,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const lines: GraphLine[] = [];
if (targetSpeed !== undefined) {
lines.push({
type: "target",
value: targetSpeed,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
}
const config: GraphConfig = {
title: "Puller Speed",
icon: "lu:Gauge",
colors: {
primary: "#06b6d4",
},
exportFilename: "puller_speed_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#06b6d4",
lines,
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="puller-speed"
/>
);
}
@@ -0,0 +1,90 @@
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { HeatingZone } from "@/machines/extruder/HeatingZone";
import { useGluetex } from "./useGluetex";
export function GluetexHeatersPage() {
const {
state,
temperature1,
temperature2,
temperature3,
temperature4,
temperature5,
temperature6,
heater1Power,
heater2Power,
heater3Power,
heater4Power,
heater5Power,
heater6Power,
setHeatingZone1Temperature,
setHeatingZone2Temperature,
setHeatingZone3Temperature,
setHeatingZone4Temperature,
setHeatingZone5Temperature,
setHeatingZone6Temperature,
} = useGluetex();
return (
<Page>
<ControlGrid>
<HeatingZone
title={"Temperature 1"}
heatingState={state?.heating_states.zone_1}
heatingTimeSeries={temperature1}
heatingPower={heater1Power}
onChangeTargetTemp={setHeatingZone1Temperature}
min={0}
max={300}
/>
<HeatingZone
title={"Temperature 2"}
heatingState={state?.heating_states.zone_2}
heatingTimeSeries={temperature2}
heatingPower={heater2Power}
onChangeTargetTemp={setHeatingZone2Temperature}
min={0}
max={300}
/>
<HeatingZone
title={"Temperature 3"}
heatingState={state?.heating_states.zone_3}
heatingTimeSeries={temperature3}
heatingPower={heater3Power}
onChangeTargetTemp={setHeatingZone3Temperature}
min={0}
max={300}
/>
<HeatingZone
title={"Temperature 4"}
heatingState={state?.heating_states.zone_4}
heatingTimeSeries={temperature4}
heatingPower={heater4Power}
onChangeTargetTemp={setHeatingZone4Temperature}
min={0}
max={300}
/>
<HeatingZone
title={"Temperature 5"}
heatingState={state?.heating_states.zone_5}
heatingTimeSeries={temperature5}
heatingPower={heater5Power}
onChangeTargetTemp={setHeatingZone5Temperature}
min={0}
max={300}
/>
<HeatingZone
title={"Temperature 6"}
heatingState={state?.heating_states.zone_6}
heatingTimeSeries={temperature6}
heatingPower={heater6Power}
onChangeTargetTemp={setHeatingZone6Temperature}
min={0}
max={300}
/>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,12 @@
import { Page } from "@/components/Page";
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
import React from "react";
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
export function GluetexManualPage() {
return (
<Page>
<MarkdownWithToc markdownContent={winderManualContent} />
</Page>
);
}
@@ -0,0 +1,56 @@
import { Topbar } from "@/components/Topbar";
import { gluetexRoute } from "@/routes/routes";
import React from "react";
export function GluetexPage() {
const { serial } = gluetexRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/gluetex/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
{
link: "addons",
activeLink: "addons",
title: "Addons",
icon: "lu:Puzzle",
},
{
link: "heaters",
activeLink: "heaters",
title: "Heaters",
icon: "lu:Flame",
},
{
link: "graphs",
activeLink: "graphs",
title: "Graphs",
icon: "lu:ChartSpline",
},
{
link: "settings",
activeLink: "settings",
title: "Config",
icon: "lu:Settings",
},
{
link: "manual",
activeLink: "manual",
title: "Manual",
icon: "lu:BookOpen",
},
{
link: "presets",
activeLink: "presets",
title: "Presets",
icon: "lu:Save",
},
]}
/>
);
}
@@ -0,0 +1,16 @@
import React from "react";
import { Page } from "@/components/Page";
export function GluetexPresetsPage() {
return (
<Page>
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Gluetex Presets</h1>
<p className="text-gray-600">
Preset functionality is not available in this test version. This is a
hardcoded frontend for testing purposes only.
</p>
</div>
</Page>
);
}
@@ -0,0 +1,957 @@
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import { EditValue } from "@/control/EditValue";
import React, { useState } from "react";
import { useGluetex } from "./useGluetex";
import { roundToDecimals } from "@/lib/decimal";
import { Label } from "@/control/Label";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { SelectionGroup } from "@/control/SelectionGroup";
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import {
getGluetexXLMode,
setGluetexXLMode,
GLUETEX_TRAVERSE_MAX_STANDARD,
GLUETEX_TRAVERSE_MAX_XL,
} from "./gluetexConfig";
export function GluetexSettingPage() {
const [xlMode, setXlMode] = useState(getGluetexXLMode());
const {
state,
defaultState,
setTraverseStepSize,
setTraversePadding,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseHome,
setPullerForward,
setPullerGearRatio,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
isLoading,
isDisabled,
selectedMachine,
filteredMachines,
setConnectedMachine,
disconnectMachine,
setSlavePullerEnabled,
setSlavePullerForward,
setSlavePullerMinAngle,
setSlavePullerMaxAngle,
setSlavePullerMinSpeedFactor,
setSlavePullerMaxSpeedFactor,
zeroSlaveTensionArm,
setHeatingPid,
setTensionArmMonitorEnabled,
setTensionArmMonitorMinAngle,
setTensionArmMonitorMaxAngle,
} = useGluetex();
const handleXlModeChange = (enabled: boolean) => {
setGluetexXLMode(enabled);
setXlMode(enabled);
// When switching from XL to normal mode, reset traverse limits to default values
if (!enabled && defaultState) {
// Only reset if current values exceed the standard max
const currentOuter = state?.traverse_state?.limit_outer ?? 0;
const currentInner = state?.traverse_state?.limit_inner ?? 0;
const defaultOuter = defaultState.traverse_state?.limit_outer;
const defaultInner = defaultState.traverse_state?.limit_inner;
if (
currentOuter > GLUETEX_TRAVERSE_MAX_STANDARD &&
defaultOuter !== undefined
) {
setTraverseLimitOuter(defaultOuter);
setTraverseLimitInner(defaultInner);
}
if (
currentInner > GLUETEX_TRAVERSE_MAX_STANDARD &&
defaultInner !== undefined
) {
setTraverseLimitOuter(defaultOuter);
setTraverseLimitInner(defaultInner);
}
// Home the traverse when switching from XL to normal mode
gotoTraverseHome();
}
};
return (
<Page>
<ControlGrid>
<ControlCard title="Traverse">
<Label label="Traverse Size">
<SelectionGroupBoolean
value={xlMode}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: `Standard (${GLUETEX_TRAVERSE_MAX_STANDARD}mm)`,
icon: "lu:Settings",
}}
optionTrue={{
children: `XL (${GLUETEX_TRAVERSE_MAX_XL}mm)`,
icon: "lu:Maximize2",
}}
onChange={handleXlModeChange}
/>
</Label>
<Label label="Step Size">
<EditValue
value={state?.traverse_state?.step_size}
title={"Step Size"}
unit="mm"
step={0.05}
min={0.1}
max={75}
defaultValue={defaultState?.traverse_state?.step_size}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setTraverseStepSize(value)}
/>
</Label>
<Label label="Padding">
<EditValue
value={state?.traverse_state?.padding}
title={"Padding"}
unit="mm"
step={0.01}
min={0}
max={5}
defaultValue={defaultState?.traverse_state?.padding}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setTraversePadding(value)}
/>
</Label>
</ControlCard>
<ControlCard title="Spool">
<Label label="Speed Algorithm">
<SelectionGroup
value={state?.spool_speed_controller_state?.regulation_mode}
disabled={isDisabled}
loading={isLoading}
options={{
MinMax: {
children: "Min/Max",
icon: "lu:ArrowUpDown",
},
Adaptive: {
children: "Adaptive",
icon: "lu:Brain",
},
}}
onChange={(value) =>
setSpoolRegulationMode(value as "Adaptive" | "MinMax")
}
/>
</Label>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.spool_speed_controller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Reverse",
icon: "lu:RotateCcw",
}}
optionTrue={{
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setSpoolForward(value)}
/>
</Label>
{state?.spool_speed_controller_state?.regulation_mode ===
"MinMax" && (
<>
<Label label="Minimum Speed">
<EditValue
value={state?.spool_speed_controller_state?.minmax_min_speed}
title={"Minimum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_min_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMinSpeed(value)}
/>
</Label>
<Label label="Maximum Speed">
<EditValue
value={state?.spool_speed_controller_state?.minmax_max_speed}
title={"Maximum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_max_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMaxSpeed(value)}
/>
</Label>
</>
)}
{state?.spool_speed_controller_state?.regulation_mode ===
"Adaptive" && (
<div className="flex flex-row flex-wrap gap-4">
<Label label="Tension Target">
<EditValue
value={
state?.spool_speed_controller_state?.adaptive_tension_target
}
title={"Tension Target"}
unit={undefined}
step={0.01}
min={0}
max={1}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_tension_target
}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setSpoolAdaptiveTensionTarget(value)}
/>
</Label>
<Label label="Learning Rate">
<EditValue
value={
state?.spool_speed_controller_state
?.adaptive_radius_learning_rate
}
title={"Radius Learning Rate"}
unit={undefined}
step={0.001}
min={0}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_radius_learning_rate
}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSpoolAdaptiveRadiusLearningRate(value)
}
/>
</Label>
<Label label="Max Speed Multiplier">
<EditValue
value={
state?.spool_speed_controller_state
?.adaptive_max_speed_multiplier
}
title={"Max Speed Multiplier"}
unit={undefined}
step={0.1}
min={0.1}
max={10}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_max_speed_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) =>
setSpoolAdaptiveMaxSpeedMultiplier(value)
}
/>
</Label>
<Label label="Acceleration Factor">
<EditValue
value={
state?.spool_speed_controller_state
?.adaptive_acceleration_factor
}
title={"Acceleration Factor"}
unit={undefined}
step={0.01}
min={0.01}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_acceleration_factor
}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSpoolAdaptiveAccelerationFactor(value)
}
/>
</Label>
<Label label="Deaccel. Urgency">
<EditValue
value={
state?.spool_speed_controller_state
?.adaptive_deacceleration_urgency_multiplier
}
title={"Deacceleration Urgency Multiplier"}
unit={undefined}
step={0.5}
min={1}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_deacceleration_urgency_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) =>
setSpoolAdaptiveDeaccelerationUrgencyMultiplier(value)
}
/>
</Label>
</div>
)}
</ControlCard>
<ControlCard title="Puller">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.puller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Reverse",
icon: "lu:RotateCcw",
}}
optionTrue={{
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setPullerForward(value)}
/>
</Label>
<Label label="Gear Ratio">
<SelectionGroup
value={state?.puller_state?.gear_ratio}
disabled={isDisabled}
loading={isLoading}
options={{
OneToOne: {
children: "1:1",
icon: "lu:Circle",
},
OneToFive: {
children: "1:5",
icon: "lu:Cog",
},
OneToTen: {
children: "1:10",
icon: "lu:Cog",
},
}}
onChange={(value) =>
setPullerGearRatio(
value as "OneToOne" | "OneToFive" | "OneToTen",
)
}
/>
</Label>
</ControlCard>
<ControlCard title="Slave Puller">
<Label label="Enable Slave Puller">
<SelectionGroupBoolean
value={state?.slave_puller_state?.enabled}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Disabled",
icon: "lu:Circle",
}}
optionTrue={{
children: "Enabled",
icon: "lu:Circle",
}}
onChange={(value) => setSlavePullerEnabled(value)}
/>
</Label>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.slave_puller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Reverse",
icon: "lu:RotateCcw",
}}
optionTrue={{
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setSlavePullerForward(value)}
/>
</Label>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Min Angle (Detection Zone)">
<EditValue
value={state?.slave_puller_state?.min_angle}
title={"Minimum Angle"}
unit="deg"
step={1}
min={0}
max={
state?.slave_puller_state?.max_angle
? state.slave_puller_state.max_angle - 5
: 85
}
defaultValue={20}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSlavePullerMinAngle(value)}
/>
</Label>
<Label label="Max Angle (Detection Zone)">
<EditValue
value={state?.slave_puller_state?.max_angle}
title={"Maximum Angle"}
unit="deg"
step={1}
min={
state?.slave_puller_state?.min_angle
? state.slave_puller_state.min_angle + 5
: 25
}
max={180}
defaultValue={90}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSlavePullerMaxAngle(value)}
/>
</Label>
<Label label="Min Speed Factor (Optional)">
<EditValue
value={state?.slave_puller_state?.min_speed_factor ?? 0}
title={"Minimum Speed Factor"}
unit={undefined}
step={0.05}
min={0.1}
max={
state?.slave_puller_state?.max_speed_factor
? state.slave_puller_state.max_speed_factor - 0.1
: 2
}
defaultValue={0}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSlavePullerMinSpeedFactor(value === 0 ? null : value)
}
/>
</Label>
<Label label="Max Speed Factor (Optional)">
<EditValue
value={state?.slave_puller_state?.max_speed_factor ?? 0}
title={"Maximum Speed Factor"}
unit={undefined}
step={0.05}
min={
state?.slave_puller_state?.min_speed_factor
? state.slave_puller_state.min_speed_factor + 0.1
: 0.2
}
max={3}
defaultValue={0}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSlavePullerMaxSpeedFactor(value === 0 ? null : value)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 1">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_1?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_1?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_1",
value,
state?.heating_pid_settings?.zone_1?.ki ?? 0,
state?.heating_pid_settings?.zone_1?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_1?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_1?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_1",
state?.heating_pid_settings?.zone_1?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_1?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_1?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_1?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_1",
state?.heating_pid_settings?.zone_1?.kp ?? 0,
state?.heating_pid_settings?.zone_1?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 2">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_2?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_2?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_2",
value,
state?.heating_pid_settings?.zone_2?.ki ?? 0,
state?.heating_pid_settings?.zone_2?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_2?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_2?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_2",
state?.heating_pid_settings?.zone_2?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_2?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_2?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_2?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_2",
state?.heating_pid_settings?.zone_2?.kp ?? 0,
state?.heating_pid_settings?.zone_2?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 3">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_3?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_3?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_3",
value,
state?.heating_pid_settings?.zone_3?.ki ?? 0,
state?.heating_pid_settings?.zone_3?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_3?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_3?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_3",
state?.heating_pid_settings?.zone_3?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_3?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_3?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_3?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_3",
state?.heating_pid_settings?.zone_3?.kp ?? 0,
state?.heating_pid_settings?.zone_3?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 4">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_4?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_4?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_4",
value,
state?.heating_pid_settings?.zone_4?.ki ?? 0,
state?.heating_pid_settings?.zone_4?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_4?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_4?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_4",
state?.heating_pid_settings?.zone_4?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_4?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_4?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_4?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_4",
state?.heating_pid_settings?.zone_4?.kp ?? 0,
state?.heating_pid_settings?.zone_4?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 5">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_5?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_5?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_5",
value,
state?.heating_pid_settings?.zone_5?.ki ?? 0,
state?.heating_pid_settings?.zone_5?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_5?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_5?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_5",
state?.heating_pid_settings?.zone_5?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_5?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_5?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_5?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_5",
state?.heating_pid_settings?.zone_5?.kp ?? 0,
state?.heating_pid_settings?.zone_5?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Heating Zone 6">
<div className="flex flex-row flex-wrap gap-4">
<Label label="Proportional Gain (Kp)">
<EditValue
value={state?.heating_pid_settings?.zone_6?.kp}
title={"Kp"}
unit={undefined}
step={0.01}
min={0}
max={10}
defaultValue={defaultState?.heating_pid_settings?.zone_6?.kp}
renderValue={(value) => roundToDecimals(value, 3)}
onChange={(value) =>
setHeatingPid(
"zone_6",
value,
state?.heating_pid_settings?.zone_6?.ki ?? 0,
state?.heating_pid_settings?.zone_6?.kd ?? 0,
)
}
/>
</Label>
<Label label="Integral Gain (Ki)">
<EditValue
value={state?.heating_pid_settings?.zone_6?.ki}
title={"Ki"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_6?.ki}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_6",
state?.heating_pid_settings?.zone_6?.kp ?? 0,
value,
state?.heating_pid_settings?.zone_6?.kd ?? 0,
)
}
/>
</Label>
<Label label="Derivative Gain (Kd)">
<EditValue
value={state?.heating_pid_settings?.zone_6?.kd}
title={"Kd"}
unit={undefined}
step={0.001}
min={0}
max={1}
defaultValue={defaultState?.heating_pid_settings?.zone_6?.kd}
renderValue={(value) => roundToDecimals(value, 4)}
onChange={(value) =>
setHeatingPid(
"zone_6",
state?.heating_pid_settings?.zone_6?.kp ?? 0,
state?.heating_pid_settings?.zone_6?.ki ?? 0,
value,
)
}
/>
</Label>
</div>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
<ControlCard title="Tension Arm Monitor">
<Label label="Enable Monitoring">
<SelectionGroupBoolean
value={state?.tension_arm_monitor_state?.enabled}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Disabled",
icon: "lu:CircleOff",
}}
optionTrue={{
children: "Enabled",
icon: "lu:Shield",
}}
onChange={(value) => setTensionArmMonitorEnabled(value)}
/>
</Label>
{state?.tension_arm_monitor_state?.triggered && (
<div className="rounded-md bg-red-500/10 p-3 text-red-600 dark:text-red-400">
<div className="flex items-center gap-2">
<span className="text-lg">⚠️</span>
<span className="font-semibold">
Tension Arm Limit Exceeded - Machine Stopped
</span>
</div>
</div>
)}
<Label label="Minimum Angle">
<EditValue
value={state?.tension_arm_monitor_state?.min_angle}
title={"Minimum Angle"}
unit="deg"
step={1}
min={0}
max={180}
defaultValue={defaultState?.tension_arm_monitor_state?.min_angle}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setTensionArmMonitorMinAngle(value)}
/>
</Label>
<Label label="Maximum Angle">
<EditValue
value={state?.tension_arm_monitor_state?.max_angle}
title={"Maximum Angle"}
unit="deg"
step={1}
min={0}
max={180}
defaultValue={defaultState?.tension_arm_monitor_state?.max_angle}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setTensionArmMonitorMaxAngle(value)}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,35 @@
/**
* Configuration helper for Gluetex machine settings
*/
const GLUETEX_XL_MODE_KEY = "gluetex_xl_mode";
export const GLUETEX_TRAVERSE_MAX_STANDARD = 180; // mm
export const GLUETEX_TRAVERSE_MAX_XL = 385; // mm
/**
* Get the XL mode setting from localStorage
* @returns true if XL mode is enabled, false otherwise
*/
export function getGluetexXLMode(): boolean {
const stored = localStorage.getItem(GLUETEX_XL_MODE_KEY);
return stored === "true";
}
/**
* Set the XL mode setting in localStorage
* @param enabled - true to enable XL mode, false to disable
*/
export function setGluetexXLMode(enabled: boolean): void {
localStorage.setItem(GLUETEX_XL_MODE_KEY, enabled.toString());
}
/**
* Get the maximum traverse limit based on XL mode
* @returns the maximum traverse limit in mm
*/
export function getGluetexTraverseMax(): number {
return getGluetexXLMode()
? GLUETEX_TRAVERSE_MAX_XL
: GLUETEX_TRAVERSE_MAX_STANDARD;
}
@@ -0,0 +1,932 @@
/**
* @file gluetexNamespace.ts
* @description TypeScript implementation of Gluetex namespace with real backend connection
* Standard winder features connect to backend, addon features use local state
*/
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 { useMemo } from "react";
import {
createTimeSeries,
TimeSeries,
TimeSeriesValue,
} from "@/lib/timeseries";
// ========== Event Schema Definitions (Backend) ==========
/**
* Consolidated live values event schema (30FPS data)
*/
export const liveValuesEventDataSchema = z.object({
traverse_position: z.number().nullable(),
puller_speed: z.number(),
spool_rpm: z.number(),
tension_arm_angle: z.number(),
spool_progress: z.number(),
temperature_1: z.number(),
temperature_2: z.number(),
temperature_3: z.number(),
temperature_4: z.number(),
temperature_5: z.number(),
temperature_6: z.number(),
heater_1_power: z.number(),
heater_2_power: z.number(),
heater_3_power: z.number(),
heater_4_power: z.number(),
heater_5_power: z.number(),
heater_6_power: z.number(),
slave_puller_speed: z.number(),
slave_tension_arm_angle: z.number(),
addon_tension_arm_angle: z.number(),
});
/**
* Puller regulation type enum
*/
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "OneToFive":
return 5.0;
case "OneToTen":
return 10.0;
default:
return 1.0;
}
}
/**
* Machine operation mode enum
*/
export const modeSchema = z.enum(["Standby", "Hold", "Pull", "Wind"]);
export type Mode = z.infer<typeof modeSchema>;
/**
* Machine operation mode enum
*/
export const spoolAutomaticActionModeSchema = z.enum([
"NoAction",
"Pull",
"Hold",
]);
export type SpoolAutomaticActionMode = z.infer<
typeof spoolAutomaticActionModeSchema
>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolRegulationModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolRegulationMode = z.infer<typeof spoolRegulationModeSchema>;
/**
* Heating zone enum to match backend
*/
export type HeatingZone =
| "Zone1"
| "Zone2"
| "Zone3"
| "Zone4"
| "Zone5"
| "Zone6";
export const spoolAutomaticActionStateSchema = z.object({
spool_required_meters: z.number(),
spool_automatic_action_mode: spoolAutomaticActionModeSchema,
});
export type SpoolAutomaticActionState = z.infer<
typeof spoolAutomaticActionStateSchema
>;
/**
* Traverse state schema
*/
export const traverseStateSchema = z.object({
limit_inner: z.number(),
limit_outer: z.number(),
position_in: z.number(),
position_out: z.number(),
is_going_in: z.boolean(),
is_going_out: z.boolean(),
is_homed: z.boolean(),
is_going_home: z.boolean(),
is_traversing: z.boolean(),
laserpointer: z.boolean(),
step_size: z.number(),
padding: z.number(),
can_go_in: z.boolean(),
can_go_out: z.boolean(),
can_go_home: z.boolean(),
});
/**
* Puller state schema
*/
export const pullerStateSchema = z.object({
regulation: pullerRegulationSchema,
target_speed: z.number(),
target_diameter: z.number(),
forward: z.boolean(),
gear_ratio: gearRatioSchema,
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
can_wind: z.boolean(),
});
/**
* Connected machine state scheme
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
machine: z.number(),
});
export const machineIdentificationUniqueSchema = z.object({
machine_identification: machineIdentificationSchema,
serial: z.number(),
});
export const connectedMachineStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Tension arm state schema
*/
export const tensionArmStateSchema = z.object({
zeroed: z.boolean(),
});
/**
* Spool speed controller state schema
*/
export const spoolSpeedControllerStateSchema = z.object({
regulation_mode: spoolRegulationModeSchema,
minmax_min_speed: z.number(),
minmax_max_speed: z.number(),
adaptive_tension_target: z.number(),
adaptive_radius_learning_rate: z.number(),
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
forward: z.boolean(),
});
/**
* Heating state schema
*/
export const heatingStateSchema = z.object({
target_temperature: z.number(),
wiring_error: z.boolean(),
});
/**
* Heating PID settings schema
*/
export const heatingPidSettingsSchema = z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
});
/**
* Heating PID states schema
*/
export const heatingPidStatesSchema = z.object({
zone_1: heatingPidSettingsSchema,
zone_2: heatingPidSettingsSchema,
zone_3: heatingPidSettingsSchema,
zone_4: heatingPidSettingsSchema,
zone_5: heatingPidSettingsSchema,
zone_6: heatingPidSettingsSchema,
});
/**
* Heating states schema
*/
export const heatingStatesSchema = z.object({
enabled: z.boolean(),
zone_1: heatingStateSchema,
zone_2: heatingStateSchema,
zone_3: heatingStateSchema,
zone_4: heatingStateSchema,
zone_5: heatingStateSchema,
zone_6: heatingStateSchema,
});
/**
* Addon motor state schema (from backend)
*/
export const addonMotorStateSchema = z.object({
enabled: z.boolean(),
master_ratio: z.number(),
slave_ratio: z.number(),
});
/**
* Tension arm monitor state schema (from backend)
*/
export const tensionArmMonitorStateSchema = z.object({
enabled: z.boolean(),
min_angle: z.number(),
max_angle: z.number(),
triggered: z.boolean(),
});
/**
* Consolidated state event schema (state changes only) - from backend
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
traverse_state: traverseStateSchema,
puller_state: pullerStateSchema,
mode_state: modeStateSchema,
tension_arm_state: tensionArmStateSchema,
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
heating_states: heatingStatesSchema,
heating_pid_settings: heatingPidStatesSchema,
connected_machine_state: connectedMachineStateSchema,
addon_motor_3_state: addonMotorStateSchema,
addon_motor_4_state: addonMotorStateSchema,
addon_tension_arm_state: tensionArmStateSchema,
tension_arm_monitor_state: tensionArmMonitorStateSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventSchema>;
export type StateEventData = z.infer<typeof stateEventDataSchema>;
// ========== Addon Types (Local State Only) ==========
export type StepperMode = "Standby" | "Run";
export type HeatingMode = "Standby" | "Heating";
export type SlavePullerState = {
enabled: boolean;
forward: boolean;
min_angle: number;
max_angle: number;
min_speed_factor: number | null;
max_speed_factor: number | null;
tension_arm: {
zeroed: boolean;
};
};
export type MotorRatiosState = {
stepper3_master: number;
stepper3_slave: number;
stepper4_master: number;
stepper4_slave: number;
};
export type StepperState = {
stepper3_mode: StepperMode;
stepper4_mode: StepperMode;
};
/**
* Helper to convert backend addon motor state to frontend MotorRatiosState
*/
function getMotorRatiosFromBackend(
motor3: z.infer<typeof addonMotorStateSchema>,
motor4: z.infer<typeof addonMotorStateSchema>,
): MotorRatiosState {
return {
stepper3_master: motor3.master_ratio,
stepper3_slave: motor3.slave_ratio,
stepper4_master: motor4.master_ratio,
stepper4_slave: motor4.slave_ratio,
};
}
/**
* Helper to determine stepper3 mode from backend enabled state
*/
function getStepper3ModeFromBackend(
motor3: z.infer<typeof addonMotorStateSchema>,
): StepperMode {
return motor3.enabled ? "Run" : "Standby";
}
/**
* Helper to determine stepper4 mode from backend enabled state
*/
function getStepper4ModeFromBackend(
motor4: z.infer<typeof addonMotorStateSchema>,
): StepperMode {
return motor4.enabled ? "Run" : "Standby";
}
/**
* Helper to determine heating mode from backend enabled state
*/
function getHeatingModeFromBackend(
heatingStates: z.infer<typeof heatingStatesSchema>,
): HeatingMode {
return heatingStates.enabled ? "Heating" : "Standby";
}
export type HeatingState = {
heating_mode: HeatingMode;
};
export type TemperatureState = {
current_temperature: number;
min_temperature: number;
max_temperature: number;
};
export type QualityControlState = {
temperature1: TemperatureState;
temperature2: TemperatureState;
};
/**
* Extended state event data with addon fields
*/
export type ExtendedStateEventData = StateEventData & {
slave_puller_state: SlavePullerState;
motor_ratios_state: MotorRatiosState;
stepper_state: StepperState;
heating_state: HeatingState;
quality_control_state: QualityControlState;
};
/**
* Extended state event with addon fields
*/
export type ExtendedStateEvent = {
name: string;
ts: number;
data: ExtendedStateEventData;
};
// ========== Store Definition ==========
export type GluetexNamespaceStore = {
// State event from server (extended with addon fields)
state: ExtendedStateEvent | null;
defaultState: ExtendedStateEvent | null;
// Time series data for live values (from backend)
traversePosition: TimeSeries;
pullerSpeed: TimeSeries;
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
temperature1: TimeSeries;
temperature2: TimeSeries;
temperature3: TimeSeries;
temperature4: TimeSeries;
temperature5: TimeSeries;
temperature6: TimeSeries;
heater1Power: TimeSeries;
heater2Power: TimeSeries;
heater3Power: TimeSeries;
heater4Power: TimeSeries;
heater5Power: TimeSeries;
heater6Power: TimeSeries;
// Time series data for addons (local)
slavePullerSpeed: TimeSeries;
slaveTensionArmAngle: TimeSeries;
addonTensionArmAngle: 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;
// Create time series for backend values
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
// Create time series for temperature values (from backend)
const { initialTimeSeries: temperature1, insert: addTemperature1 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: temperature2, insert: addTemperature2 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: temperature3, insert: addTemperature3 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: temperature4, insert: addTemperature4 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: temperature5, insert: addTemperature5 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: temperature6, insert: addTemperature6 } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater1Power, insert: addHeater1Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater2Power, insert: addHeater2Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater3Power, insert: addHeater3Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater4Power, insert: addHeater4Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater5Power, insert: addHeater5Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: heater6Power, insert: addHeater6Power } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
// Create time series for addon values (local)
const { initialTimeSeries: slavePullerSpeed, insert: addSlavePullerSpeed } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const {
initialTimeSeries: slaveTensionArmAngle,
insert: addSlaveTensionArmAngle,
} = createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const {
initialTimeSeries: addonTensionArmAngle,
insert: addAddonTensionArmAngle,
} = createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
// Default addon state
const DEFAULT_ADDON_STATE = {
slave_puller_state: {
enabled: false,
forward: true,
min_angle: 20.0,
max_angle: 90.0,
min_speed_factor: null,
max_speed_factor: null,
tension_arm: {
zeroed: false,
},
},
motor_ratios_state: {
stepper3_master: 1.0,
stepper3_slave: 1.0,
stepper4_master: 1.0,
stepper4_slave: 1.0,
},
stepper_state: {
stepper3_mode: "Standby" as StepperMode,
stepper4_mode: "Standby" as StepperMode,
},
heating_state: {
heating_mode: "Standby" as HeatingMode,
},
quality_control_state: {
temperature1: {
current_temperature: 85.0,
min_temperature: 75.0,
max_temperature: 95.0,
},
temperature2: {
current_temperature: 125.0,
min_temperature: 115.0,
max_temperature: 135.0,
},
},
};
/**
* Factory function to create a new Gluetex namespace store
* @returns A new Zustand store instance for Gluetex namespace
*/
export const createGluetexNamespaceStore =
(): StoreApi<GluetexNamespaceStore> =>
create<GluetexNamespaceStore>(() => {
return {
// State event from server (will be extended with addon state)
state: null,
defaultState: null,
// Time series data for live values
traversePosition,
pullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
temperature1,
temperature2,
temperature3,
temperature4,
temperature5,
temperature6,
heater1Power,
heater2Power,
heater3Power,
heater4Power,
heater5Power,
heater6Power,
// Time series data for addons
slavePullerSpeed,
slaveTensionArmAngle,
addonTensionArmAngle,
};
});
/**
* Creates a message handler for Gluetex 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 30 FPS
* @returns A message handler function
*/
export function gluetexMessageHandler(
store: StoreApi<GluetexNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<GluetexNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: GluetexNamespaceStore) => GluetexNamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
if (eventName === "StateEvent") {
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => {
// Derive motor ratios from backend addon motor state
const motorRatiosState = getMotorRatiosFromBackend(
stateEvent.data.addon_motor_3_state,
stateEvent.data.addon_motor_4_state,
);
// Derive stepper modes from backend enabled state
const stepper3Mode = getStepper3ModeFromBackend(
stateEvent.data.addon_motor_3_state,
);
const stepper4Mode = getStepper4ModeFromBackend(
stateEvent.data.addon_motor_4_state,
);
// Derive heating mode from backend enabled state
const heatingMode = getHeatingModeFromBackend(
stateEvent.data.heating_states,
);
// Extend backend state with addon state (some local, some derived from backend)
const extendedData: ExtendedStateEventData = {
...stateEvent.data,
// Preserve existing local-only addon state
slave_puller_state:
state.state?.data.slave_puller_state ||
DEFAULT_ADDON_STATE.slave_puller_state,
// Derive from backend addon motor state
motor_ratios_state: motorRatiosState,
stepper_state: {
stepper3_mode: stepper3Mode,
stepper4_mode: stepper4Mode,
},
heating_state: {
heating_mode: heatingMode,
},
quality_control_state:
state.state?.data.quality_control_state ||
DEFAULT_ADDON_STATE.quality_control_state,
};
const extendedState: ExtendedStateEvent = {
name: stateEvent.name,
ts: stateEvent.ts,
data: extendedData,
};
return {
...state,
state: extendedState,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? extendedState
: state.defaultState,
};
});
} else if (eventName === "LiveValuesEvent") {
// Parse and validate the live values event
const liveValuesEvent = liveValuesEventSchema.parse(event);
// Extract values and add to time series
const {
traverse_position,
puller_speed,
spool_rpm,
tension_arm_angle,
spool_progress,
temperature_1,
temperature_2,
temperature_3,
temperature_4,
temperature_5,
temperature_6,
heater_1_power,
heater_2_power,
heater_3_power,
heater_4_power,
heater_5_power,
heater_6_power,
slave_puller_speed,
slave_tension_arm_angle,
addon_tension_arm_angle,
} = liveValuesEvent.data;
const timestamp = liveValuesEvent.ts;
updateStore((state) => {
const newState = { ...state };
// Add traverse position if not null
if (traverse_position !== null) {
const timeseriesValue: TimeSeriesValue = {
value: traverse_position,
timestamp,
};
newState.traversePosition = addTraversePosition(
state.traversePosition,
timeseriesValue,
);
}
if (spool_progress !== null) {
const timeseriesValue: TimeSeriesValue = {
value: spool_progress,
timestamp,
};
newState.spoolProgress = addSpoolProgress(
state.spoolProgress,
timeseriesValue,
);
}
// Add puller speed
const pullerSpeedValue: TimeSeriesValue = {
value: puller_speed,
timestamp,
};
newState.pullerSpeed = addPullerSpeed(
state.pullerSpeed,
pullerSpeedValue,
);
// Add spool RPM
const spoolRpmValue: TimeSeriesValue = {
value: spool_rpm,
timestamp,
};
newState.spoolRpm = addSpoolRpm(state.spoolRpm, spoolRpmValue);
// Add tension arm angle
const tensionArmAngleValue: TimeSeriesValue = {
value: tension_arm_angle,
timestamp,
};
newState.tensionArmAngle = addTensionArmAngle(
state.tensionArmAngle,
tensionArmAngleValue,
);
// Add temperature readings from backend
const temp1Value: TimeSeriesValue = {
value: temperature_1,
timestamp,
};
newState.temperature1 = addTemperature1(
state.temperature1,
temp1Value,
);
const temp2Value: TimeSeriesValue = {
value: temperature_2,
timestamp,
};
newState.temperature2 = addTemperature2(
state.temperature2,
temp2Value,
);
const temp3Value: TimeSeriesValue = {
value: temperature_3,
timestamp,
};
newState.temperature3 = addTemperature3(
state.temperature3,
temp3Value,
);
const temp4Value: TimeSeriesValue = {
value: temperature_4,
timestamp,
};
newState.temperature4 = addTemperature4(
state.temperature4,
temp4Value,
);
const temp5Value: TimeSeriesValue = {
value: temperature_5,
timestamp,
};
newState.temperature5 = addTemperature5(
state.temperature5,
temp5Value,
);
const temp6Value: TimeSeriesValue = {
value: temperature_6,
timestamp,
};
newState.temperature6 = addTemperature6(
state.temperature6,
temp6Value,
);
// Add heater power values
const heater1Value: TimeSeriesValue = {
value: heater_1_power,
timestamp,
};
newState.heater1Power = addHeater1Power(
state.heater1Power,
heater1Value,
);
const heater2Value: TimeSeriesValue = {
value: heater_2_power,
timestamp,
};
newState.heater2Power = addHeater2Power(
state.heater2Power,
heater2Value,
);
const heater3Value: TimeSeriesValue = {
value: heater_3_power,
timestamp,
};
newState.heater3Power = addHeater3Power(
state.heater3Power,
heater3Value,
);
const heater4Value: TimeSeriesValue = {
value: heater_4_power,
timestamp,
};
newState.heater4Power = addHeater4Power(
state.heater4Power,
heater4Value,
);
const heater5Value: TimeSeriesValue = {
value: heater_5_power,
timestamp,
};
newState.heater5Power = addHeater5Power(
state.heater5Power,
heater5Value,
);
const heater6Value: TimeSeriesValue = {
value: heater_6_power,
timestamp,
};
newState.heater6Power = addHeater6Power(
state.heater6Power,
heater6Value,
);
// Update slave puller data from backend
const slavePullerValue: TimeSeriesValue = {
value: slave_puller_speed,
timestamp,
};
newState.slavePullerSpeed = addSlavePullerSpeed(
state.slavePullerSpeed,
slavePullerValue,
);
const slaveTensionArmValue: TimeSeriesValue = {
value: slave_tension_arm_angle,
timestamp,
};
newState.slaveTensionArmAngle = addSlaveTensionArmAngle(
state.slaveTensionArmAngle,
slaveTensionArmValue,
);
const addonTensionArmValue: TimeSeriesValue = {
value: addon_tension_arm_angle,
timestamp,
};
newState.addonTensionArmAngle = addAddonTensionArmAngle(
state.addonTensionArmAngle,
addonTensionArmValue,
);
return newState;
});
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(`Unexpected error processing ${eventName} event:`, error);
throw error;
}
};
}
/**
* Create the Gluetex namespace implementation
*/
const useGluetexNamespaceImplementation =
createNamespaceHookImplementation<GluetexNamespaceStore>({
createStore: createGluetexNamespaceStore,
createEventHandler: gluetexMessageHandler,
});
/**
* Hook for a machine-specific Gluetex namespace
*
* @example
* ```tsx
* function GluetexStatus({ machine }) {
* const { state, traversePosition, pullerSpeed } = useGluetexNamespace(machine);
*
* return (
* <div>
* {state && (
* <div>Mode: {state.data.mode_state.mode}</div>
* )}
* </div>
* );
* }
* ```
*/
export function useGluetexNamespace(
machine_identification_unique: MachineIdentificationUnique,
): GluetexNamespaceStore {
// Generate namespace ID from validated machine ID
const namespaceId = useMemo<NamespaceId>(
() => ({
type: "machine",
machine_identification_unique,
}),
[machine_identification_unique],
);
// Use the implementation with validated namespace ID
return useGluetexNamespaceImplementation(namespaceId);
}
File diff suppressed because it is too large. Load diff
+66
View File
@@ -10,6 +10,14 @@ import { Winder2ControlPage } from "@/machines/winder/winder2/Winder2ControlPage
import { Winder2ManualPage } from "@/machines/winder/winder2/Winder2Manual";
import { Winder2SettingPage } from "@/machines/winder/winder2/Winder2Settings";
import { Winder2GraphsPage } from "@/machines/winder/winder2/Winder2Graphs";
import { GluetexPage } from "@/machines/winder/gluetex/GluetexPage";
import { GluetexControlPage } from "@/machines/winder/gluetex/GluetexControlPage";
import { GluetexManualPage } from "@/machines/winder/gluetex/GluetexManual";
import { GluetexSettingPage } from "@/machines/winder/gluetex/GluetexSettings";
import { GluetexGraphsPage } from "@/machines/winder/gluetex/GluetexGraphs";
import { GluetexPresetsPage } from "@/machines/winder/gluetex/GluetexPresetsPage";
import { GluetexAddonsPage } from "@/machines/winder/gluetex/GluetexAddonsPage";
import { GluetexHeatersPage } from "@/machines/winder/gluetex/GluetexHeatersPage";
import { Extruder2Page } from "@/machines/extruder/extruder2/Extruder2Page";
import { Extruder2ControlPage } from "@/machines/extruder/extruder2/Extruder2ControlPage";
import { Extruder2SettingsPage } from "@/machines/extruder/extruder2/Extruder2Settings";
@@ -132,6 +140,54 @@ export const winder2PresetsRoute = createRoute({
component: () => <Winder2PresetsPage />,
});
export const gluetexRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "gluetex/$serial",
component: () => <GluetexPage />,
});
export const gluetexControlRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "control",
component: () => <GluetexControlPage />,
});
export const gluetexManualRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "manual",
component: () => <GluetexManualPage />,
});
export const gluetexSettingsRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "settings",
component: () => <GluetexSettingPage />,
});
export const gluetexGraphsRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "graphs",
component: () => <GluetexGraphsPage />,
});
export const gluetexPresetsRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "presets",
component: () => <GluetexPresetsPage />,
});
export const gluetexAddonsRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "addons",
component: () => <GluetexAddonsPage />,
});
export const gluetexHeatersRoute = createRoute({
getParentRoute: () => gluetexRoute,
path: "heaters",
component: () => <GluetexHeatersPage />,
});
export const laser1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "laser1/$serial",
@@ -327,6 +383,16 @@ export const rootTree = RootRoute.addChildren([
winder2PresetsRoute,
]),
gluetexRoute.addChildren([
gluetexControlRoute,
gluetexManualRoute,
gluetexSettingsRoute,
gluetexGraphsRoute,
gluetexPresetsRoute,
gluetexAddonsRoute,
gluetexHeatersRoute,
]),
extruder2Route.addChildren([
extruder2ControlRoute,
extruder2SettingsRoute,
+10 -5
View File
@@ -147,7 +147,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
device.revision,
machinePreset?.device_roles,
),
[device.product_id, device.revision, machinePreset],
[device.vendor_id, device.product_id, device.revision, machinePreset],
);
// if there is only one allowed role, set the role to that immediately
@@ -161,10 +161,15 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
const index = filteredAllowedDevices.findIndex(
(isAllowed) => isAllowed === true,
);
// set the device role of index
form.setValue("role", index.toString());
// set the device role using the actual role number, not the index
if (machinePreset && machinePreset.device_roles[index]) {
form.setValue(
"role",
machinePreset.device_roles[index].role.toString(),
);
}
}
}, [filteredAllowedDevices]);
}, [filteredAllowedDevices, machinePreset, form]);
return (
<DialogContent>
@@ -234,7 +239,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
<FormItem>
<FormLabel>Device Role</FormLabel>
<FormControl>
<Select {...field}>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="w-min">
<SelectValue placeholder="Device Role" />
</SelectTrigger>
+22
View File
@@ -15,6 +15,8 @@ import {
} from "@/machines/properties";
import { IconText } from "@/components/IconText";
import { MachinesEventData, useMainNamespace } from "@/client/mainNamespace";
import { TouchButton } from "@/components/touch/TouchButton";
import { useNavigate } from "@tanstack/react-router";
export const columns: ColumnDef<
NonNullable<MachinesEventData>["machines"][number]
@@ -76,6 +78,7 @@ export const columns: ColumnDef<
export function MachinesPage() {
const { machines } = useMainNamespace();
const navigate = useNavigate();
const data = useMemo(() => {
return machines?.data?.machines || [];
@@ -93,6 +96,25 @@ export function MachinesPage() {
<RefreshIndicator ts={machines?.ts} />
</SectionTitle>
<MyTable table={table} key={data.toString()} />
<div className="mt-8 border-t border-gray-200 p-4">
<h3 className="mb-2 text-lg font-semibold">Test Machines</h3>
<p className="mb-4 text-sm text-gray-600">
Hardcoded frontend machines for testing (no backend required)
</p>
<TouchButton
onClick={() =>
navigate({
to: "/_sidebar/machines/gluetex/$serial/control",
params: { serial: "999" },
})
}
icon="lu:TestTube"
variant="outline"
>
Open Gluetex Test Winder
</TouchButton>
</div>
</Page>
);
}
+5 -6
View File
@@ -35,18 +35,17 @@ pub struct AiStandard {
impl TxPdoObject for AiStandard {
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
// only read other values if txpdo_toggle is true
self.txpdo_toggle = bits[8 + 7];
if !self.txpdo_toggle {
return;
}
// Read all values regardless of txpdo_toggle state.
// The toggle bit is still read for informational purposes, but we always
// update the value to prevent stale data when multiple analog inputs
// are active on the same device.
self.undervoltage = bits[0];
self.overvoltage = bits[1];
self.limit1 = bits[2..4].load_le::<u8>().into();
self.limit2 = bits[4..6].load_le::<u8>().into();
self.error = bits[7];
self.txpdo_state = bits[8 + 6];
self.txpdo_toggle = bits[8 + 7];
self.value = bits[16..16 + 16].load_le::<u16>();
}
}
+3
View File
@@ -46,5 +46,8 @@ io-uring = []
development-build = []
[package.metadata.cargo-machete]
ignored = ["tonic"]
+3 -5
View File
@@ -3,14 +3,13 @@ use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -114,11 +113,10 @@ impl CacheableEvents<AquaPathV1Events> for AquaPathV1Events {
}
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::LiveValues(_) => cache_first_and_last,
AquaPathV1Events::State(_) => cache_first_and_last,
}
}
+4 -10
View File
@@ -1,18 +1,13 @@
use std::{sync::Arc, time::Duration};
use crate::{MachineApi, MachineMessage, machine_identification::MachineIdentificationUnique};
use super::{BufferV1, BufferV1Mode};
use crate::{MachineApi, MachineMessage, machine_identification::MachineIdentificationUnique};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_one_event,
},
namespace::{CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_one_event},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -99,11 +94,10 @@ impl CacheableEvents<Self> for BufferV1Events {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_one = cache_one_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_one,
Self::State(_) => cache_one,
}
}
+3 -6
View File
@@ -11,8 +11,7 @@ use crate::MachineApi;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
@@ -21,7 +20,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use tracing::instrument;
use units::angular_velocity::revolution_per_minute;
use units::electric_current::ampere;
@@ -267,11 +266,9 @@ impl CacheableEvents<Self> for ExtruderV2Events {
}
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 {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
+32 -2
View File
@@ -1,9 +1,17 @@
use crate::machines::extruder1::mock::ExtruderV2;
use control_core::machines::new::MachineAct;
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
@@ -12,4 +20,26 @@ impl MachineAct for ExtruderV2 {
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
+12 -6
View File
@@ -1,8 +1,8 @@
use crate::machines::extruder1::{HeatingType, api::Mutation, mock::ExtruderV2};
use control_core::machines::api::MachineApi;
use control_core::socketio::namespace::Namespace;
use smol::lock::Mutex;
use std::sync::Arc;
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
};
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
@@ -45,7 +45,13 @@ impl MachineApi for ExtruderV2 {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
+2 -1
View File
@@ -1,8 +1,9 @@
use crate::machines::extruder1::{
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
mock::ExtruderV2,
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
+28 -5
View File
@@ -1,14 +1,21 @@
#[cfg(feature = "mock-machine")]
use control_core::machines::identification::MachineIdentification;
use smol::channel::Receiver;
#[cfg(feature = "mock-machine")]
use control_core::machines::identification::MachineIdentificationUnique;
use smol::channel::Sender;
#[cfg(feature = "mock-machine")]
use control_core_derive::Machine;
use crate::{AsyncThreadMessage, Machine};
#[cfg(feature = "mock-machine")]
use crate::{
MachineIdentificationUnique, MachineMessage, machine_identification::MachineIdentification,
};
#[cfg(feature = "mock-machine")]
use std::time::Instant;
#[cfg(feature = "mock-machine")]
use crate::machines::{
use crate::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
extruder1::{
ExtruderV2Mode,
@@ -31,9 +38,25 @@ pub mod mock_emit;
pub mod new;
#[cfg(feature = "mock-machine")]
#[derive(Debug, Machine)]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(feature = "mock-machine")]
#[derive(Debug)]
pub struct ExtruderV2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
+19 -14
View File
@@ -1,18 +1,19 @@
use crate::machines::extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
},
mock::ExtruderV2,
},
mock::ExtruderV2,
};
impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
impl MachineNewTrait for ExtruderV2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
@@ -20,19 +21,23 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut extruder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: params.namespace.clone(),
+8 -2
View File
@@ -2,17 +2,22 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use serde::{Deserialize, Serialize};
use smol::channel::Receiver;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use units::electric_current::ampere;
#[cfg(not(feature = "mock-machine"))]
use units::electric_potential::volt;
use crate::{AsyncThreadMessage, Machine};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
use crate::{AsyncThreadMessage, Machine};
#[cfg(not(feature = "mock-machine"))]
use crate::{
MACHINE_EXTRUDER_V1, MachineMessage, VENDOR_QITECH,
@@ -94,6 +99,7 @@ pub struct ExtruderV2 {
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
+80
View File
@@ -0,0 +1,80 @@
use super::Gluetex;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for Gluetex {
fn act(&mut self, now: Instant) {
let machine_message = self.api_receiver.try_recv();
match machine_message {
Ok(machine_message) => self.act_machine_message(machine_message),
Err(_e) => (),
};
// sync the spool speed
self.sync_spool_speed(now);
// sync the puller speed
self.sync_puller_speed(now);
// sync the slave puller speed
self.sync_slave_puller_speed(now);
// sync addon motor speeds
self.sync_addon_motor_3_speed(now);
self.sync_addon_motor_4_speed(now);
// sync the traverse speed
self.sync_traverse_speed();
// automatically stops or pulls after N Meters if enabled
self.stop_or_pull_spool(now);
// check tension arm positions and trigger emergency stop if needed
self.check_tension_arm_monitor();
// update all temperature controllers
self.temperature_controller_1.update(now);
self.temperature_controller_2.update(now);
self.temperature_controller_3.update(now);
self.temperature_controller_4.update(now);
self.temperature_controller_5.update(now);
self.temperature_controller_6.update(now);
if self.traverse_controller.did_change_state() {
self.emit_state();
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(machine_connection) => {
if self.connected_machines.len() >= self.max_connected_machines {
tracing::debug!(
"Refusing to add Machine Connection {:?}, since self.connected_machines would be over the limit of {:?}",
machine_connection,
self.max_connected_machines
);
return;
}
self.connected_machines.push(machine_connection);
}
MachineMessage::DisconnectMachine(_machine_connection) => {
self.connected_machines.clear();
}
}
}
}
@@ -0,0 +1,460 @@
use crate::gluetex::{
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
puller_speed_controller::PullerSpeedController,
};
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
use control_core::{
controllers::first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
helpers::{interpolation::scale, moving_time_window::MovingTimeWindow},
};
use core::f64;
use std::time::Instant;
use units::ConstZero;
use units::angle::degree;
use units::angular_acceleration::radian_per_second_squared;
use units::angular_velocity::{radian_per_second, revolution_per_minute};
use units::f64::*;
use units::length::{centimeter, meter};
use units::velocity::meter_per_second;
/// Adaptive spool speed controller that automatically adjusts to maintain optimal filament tension.
///
/// This controller monitors filament tension via the tension arm and learns the appropriate
/// maximum speed based on puller speed and tension feedback. It uses closed-loop control
/// to minimize tension error and applies smooth acceleration to prevent sudden motor commands.
#[derive(Debug)]
pub struct AdaptiveSpoolSpeedController {
/// Last commanded angular velocity sent to the spool motor
last_speed: AngularVelocity,
/// Whether the speed controller is enabled (false = always returns zero speed)
enabled: bool,
/// Acceleration controller to smooth speed transitions and prevent sudden changes
acceleration_controller: AngularAccelerationSpeedController,
/// Calculator for converting tension arm angle to normalized filament tension
filament_calc: FilamentTensionCalculator,
/// Moving window of recent speeds (in rad/s) used for dynamic acceleration limit calculation
speed_time_window: MovingTimeWindow<f64>,
/// Factor to control/calculate the feed speed based on filament tension
speed_factor: Length,
/// Timestamp of last max speed factor update, used for time-aware learning rate calculation
last_max_speed_factor_update: Option<Instant>,
/// Target normalized tension value (0.0-1.0) that the controller tries to maintain
tension_target: f64,
/// Proportional control gain for adaptive learning (negative: higher tension reduces speed)
radius_learning_rate: f64,
/// Speed multiplier when tension is at minimum (max speed factor)
max_speed_multiplier: f64,
/// Base acceleration as a fraction of max possible speed (per second)
acceleration_factor: f64,
/// Urgency multiplier for near-zero target speeds
deacceleration_urgency_multiplier: f64,
}
impl Default for AdaptiveSpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveSpoolSpeedController {
/// Maximum speed limit (in RPM) used to initialize the acceleration controller
const INITIAL_MAX_SPEED_RPM: f64 = 150.0;
/// Absolute safety limit (in RPM) that the spool speed can never exceed to protect hardware
const SAFETY_MAX_SPEED_RPM: f64 = 600.0;
/// Time window duration (in seconds) for tracking recent speed history in acceleration calculations
const SPEED_WINDOW_DURATION_SECS: u64 = 5;
/// Maximum number of speed samples stored in the moving time window
const SPEED_WINDOW_MAX_SAMPLES: usize = 10;
/// Maximum angle (in degrees) of the tension arm, representing loosest filament tension
const TENSION_ARM_MAX_ANGLE_DEG: f64 = 90.0;
/// Minimum angle (in degrees) of the tension arm, representing tightest filament tension
const TENSION_ARM_MIN_ANGLE_DEG: f64 = 20.0;
/// Target normalized tension value (0.0-1.0) that the controller tries to maintain
const TENSION_TARGET: f64 = 0.7;
/// Proportional control gain for adaptive learning (negative: higher tension reduces speed)
const RADIUS_LEARNING_RATE: f64 = 0.5;
const FACTOR_MIN: f64 = 4.25;
const FACTOR_MAX: f64 = 20.0;
/// if the tension is the lowest, the speed can be up to 2x the puller speed
const MAX_SPEED_MULTIPLIER: f64 = 4.0;
/// Base acceleration as a fraction of max possible speed (per second)
const ACCELERATION_FACTOR: f64 = 0.2; // 20% of max speed per second
/// Urgency multiplier for near-zero target speeds
const DEACCELERATION_URGENCY_MULTIPLIER: f64 = 15.0;
/// Minimum acceleration limit to prevent completely frozen motion
const MIN_ACCELERATION_LIMIT: f64 = 0.5; // rad/s²
/// Creates a new adaptive spool speed controller with default settings.
///
/// Initializes all internal components and sets baseline parameters.
///
/// # Returns
/// A new `AdaptiveSpoolSpeedController` instance ready for use.
pub fn new() -> Self {
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::INITIAL_MAX_SPEED_RPM);
Self {
last_speed: AngularVelocity::ZERO,
enabled: false,
acceleration_controller: AngularAccelerationSpeedController::new(
Some(AngularVelocity::ZERO),
Some(max_speed),
-AngularAcceleration::ZERO, // Will be dynamically adjusted
AngularAcceleration::ZERO, // Will be dynamically adjusted
AngularVelocity::ZERO,
),
filament_calc: FilamentTensionCalculator::new(
Angle::new::<degree>(Self::TENSION_ARM_MAX_ANGLE_DEG),
Angle::new::<degree>(Self::TENSION_ARM_MIN_ANGLE_DEG),
),
speed_time_window: MovingTimeWindow::new(
std::time::Duration::from_secs(Self::SPEED_WINDOW_DURATION_SECS),
Self::SPEED_WINDOW_MAX_SAMPLES,
),
speed_factor: Length::new::<centimeter>(4.25),
last_max_speed_factor_update: None,
tension_target: Self::TENSION_TARGET,
radius_learning_rate: Self::RADIUS_LEARNING_RATE,
max_speed_multiplier: Self::MAX_SPEED_MULTIPLIER,
acceleration_factor: Self::ACCELERATION_FACTOR,
deacceleration_urgency_multiplier: Self::DEACCELERATION_URGENCY_MULTIPLIER,
}
}
/// Calculates the current maximum speed based on puller speed and learned factor.
///
/// Uses the fundamental relationship between linear and angular velocity:
/// ω = (v / r) × multiplier
///
/// Where:
/// - ω (omega) = angular velocity in radians per second
/// - v = linear velocity in meters per second
/// - r = radius in meters
/// - multiplier = MAX_SPEED_MULTIPLIER for speed scaling
///
/// This ensures the spool's rim speed is properly scaled relative to the puller's
/// linear speed, with the multiplier allowing for tension control adjustments.
///
/// # Parameters
/// - `puller_speed_controller`: Reference to puller for baseline speed calculation
///
/// # Returns
/// Current maximum angular velocity for the spool in radians per second
fn get_max_speed(&self, puller_speed_controller: &PullerSpeedController) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
(puller_speed_controller.last_speed.get::<meter_per_second>()
/ self.speed_factor.get::<meter>())
* self.max_speed_multiplier,
)
}
/// Calculates the desired spool speed based on filament tension feedback.
///
/// Reads the tension arm angle, converts it to normalized tension, and calculates
/// the appropriate speed. Also updates the learned maximum speed based on tension error.
///
/// # Parameters
/// - `t`: Current timestamp for learning updates
/// - `tension_arm`: Reference to tension arm for angle measurements
/// - `puller_speed_controller`: Reference to puller for baseline speed calculations
///
/// # Returns
/// The calculated target angular velocity for the spool motor
fn calculate_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = self.get_max_speed(puller_speed_controller).abs();
// Calculate filament tension from arm angle
let tension_arm_angle = tension_arm.get_angle();
let (clamped_angle, clamping_state) = clamp_revolution_uom(
tension_arm_angle,
self.filament_calc.get_max_angle(), // Inverted because min angle = max tension
self.filament_calc.get_min_angle(),
);
// Return minimum speed if tension arm is at limits
if matches!(clamping_state, Clamping::Min | Clamping::Max) {
return min_speed;
}
// 1.0 means maximum tension (high angle, low speed)
// 0.0 means minimum tension (low angle, high speed)
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
self.update_speed_factor(filament_tension, t);
// Calculate speed based on inverted tension (lower tension = higher speed)
AngularVelocity::new::<radian_per_second>(scale(
1.0 - filament_tension,
min_speed.get::<radian_per_second>(),
max_speed.get::<radian_per_second>(),
))
}
/// Simplified urgency-weighted acceleration that adapts to current operating conditions.
///
/// This approach uses the current max speed (calculated from puller speed and learned diameter)
/// as the basis for acceleration limits, ensuring smooth operation across all speed ranges
/// while providing instant deceleration capability when approaching zero.
///
/// # Parameters
/// - `target_speed`: Target speed to accelerate towards
/// - `puller_speed_controller`: Reference to puller for current max speed calculation
/// - `t`: Current timestamp for the acceleration controller
///
/// # Returns
/// Smoothly accelerated speed with urgency-weighted limits
fn accelerate_speed(
&mut self,
target_speed: AngularVelocity,
puller_speed_controller: &PullerSpeedController,
t: Instant,
) -> AngularVelocity {
let target_speed_rad_s = target_speed.get::<radian_per_second>();
let current_max_speed = self.get_max_speed(puller_speed_controller);
let max_speed_rad_s = current_max_speed.get::<radian_per_second>();
// Base acceleration proportional to current max operating speed
let base_acceleration = max_speed_rad_s * self.acceleration_factor;
// Simple urgency factor - dramatically increases near zero
let urgency_factor = if target_speed_rad_s.abs() < 0.1 {
self.deacceleration_urgency_multiplier * (1.0 / (target_speed_rad_s.abs() + 0.01))
} else {
1.0
};
// Final acceleration limit
let acceleration_limit =
(base_acceleration * urgency_factor).max(Self::MIN_ACCELERATION_LIMIT);
let acceleration =
AngularAcceleration::new::<radian_per_second_squared>(acceleration_limit);
// Update acceleration controller limits
self.acceleration_controller
.set_max_acceleration(acceleration);
self.acceleration_controller
.set_min_acceleration(-acceleration);
let new_speed = self.acceleration_controller.update(target_speed, t);
// Record for diagnostics
self.speed_time_window
.update(new_speed.get::<radian_per_second>(), t);
new_speed
}
/// Safety function that enforces absolute speed limits.
///
/// Ensures the spool speed never exceeds safe operational limits to protect hardware.
/// Acts as a final safety check after all other speed calculations.
///
/// # Parameters
/// - `speed`: The speed to be safety-checked and clamped
///
/// # Returns
/// Speed guaranteed to be within safe operational limits
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::SAFETY_MAX_SPEED_RPM);
speed.max(min_speed).min(max_speed)
}
/// Adaptive learning algorithm that adjusts maximum speed factor based on tension feedback.
///
/// Uses proportional control to adjust the speed factor based on tension error.
/// The controller learns over time to maintain optimal tension by scaling the
/// baseline speed derived from puller speed.
///
/// # Parameters
/// - `filament_tension`: Normalized tension value
/// - `t`: Current timestamp for time-aware learning rate
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant) {
let delta_t = match self.last_max_speed_factor_update {
Some(last_update) => t.duration_since(last_update).as_secs_f64(),
None => {
// First call, initialize and return early
self.last_max_speed_factor_update = Some(t);
return;
}
};
// positive error means too much tension, so we reduce speed
// negative error means too little tension, so we increase speed
let tension_error = filament_tension - self.tension_target;
// Calculate proportional control adjustment
let proportional_gain = self.radius_learning_rate * delta_t;
let factor_change = tension_error * proportional_gain;
// Update the speed factor directly
let new_factor = (self.speed_factor.get::<centimeter>() + factor_change)
.clamp(Self::FACTOR_MIN, Self::FACTOR_MAX);
self.speed_factor = Length::new::<centimeter>(new_factor); // Convert to cm
self.last_max_speed_factor_update = Some(t);
}
/// Main update function that orchestrates the complete speed control pipeline.
///
/// This is the primary interface that calculates the target speed, applies enable/disable
/// logic, smooths speed changes, and applies safety limits.
///
/// Call this function every control loop iteration to get the commanded spool speed.
///
/// # Parameters
/// - `t`: Current timestamp for time-based calculations
/// - `tension_arm`: Reference to tension arm for feedback
/// - `puller_speed_controller`: Reference for adaptive speed scaling
///
/// # Returns
/// Final commanded angular velocity for the spool motor
pub fn update_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
let target_speed = self.calculate_speed(t, tension_arm, puller_speed_controller);
let enabled_speed = if self.enabled {
target_speed
} else {
AngularVelocity::ZERO
};
let accelerated_speed = self.accelerate_speed(enabled_speed, puller_speed_controller, t);
// Store speed before clamping to preserve the actual commanded value
self.last_speed = accelerated_speed;
self.clamp_speed(accelerated_speed)
}
/// Enables or disables the speed controller.
///
/// When disabled, the controller returns zero speed regardless of tension feedback.
///
/// # Parameters
/// - `enabled`: True to enable speed control, false to disable
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Returns whether the speed controller is currently enabled.
///
/// # Returns
/// True if the controller is enabled, false if disabled
pub const fn is_enabled(&self) -> bool {
self.enabled
}
/// Resets the controller to initial state, clearing all learned parameters.
///
/// Use this when starting a new winding operation or after significant
/// process changes that would invalidate learned parameters.
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
self.speed_factor = Length::new::<centimeter>(4.25);
self.last_max_speed_factor_update = None;
self.tension_target = Self::TENSION_TARGET;
self.radius_learning_rate = Self::RADIUS_LEARNING_RATE;
self.max_speed_multiplier = Self::MAX_SPEED_MULTIPLIER;
self.acceleration_factor = Self::ACCELERATION_FACTOR;
self.deacceleration_urgency_multiplier = Self::DEACCELERATION_URGENCY_MULTIPLIER;
}
/// Returns the last commanded speed from the controller.
///
/// # Returns
/// The angular velocity that was last commanded to the spool motor
pub fn get_speed(&self) -> AngularVelocity {
self.last_speed
}
/// Manually sets the current speed, bypassing normal calculation.
///
/// Use this for manual control or when switching control modes to ensure
/// smooth transitions.
///
/// # Parameters
/// - `speed`: The angular velocity to set as the current speed
pub fn set_speed(&mut self, speed: AngularVelocity) {
self.last_speed = speed;
self.acceleration_controller.reset(speed);
}
pub fn get_speed_factor(&self) -> Length {
self.speed_factor
}
// Getters and setters for the new configurable parameters
pub const fn get_tension_target(&self) -> f64 {
self.tension_target
}
pub const fn set_tension_target(&mut self, tension_target: f64) {
self.tension_target = tension_target.clamp(0.0, 1.0);
}
pub const fn get_radius_learning_rate(&self) -> f64 {
self.radius_learning_rate
}
pub const fn set_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.radius_learning_rate = radius_learning_rate.max(0.0);
}
pub const fn get_max_speed_multiplier(&self) -> f64 {
self.max_speed_multiplier
}
pub const fn set_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.max_speed_multiplier = max_speed_multiplier.max(0.1);
}
pub const fn get_acceleration_factor(&self) -> f64 {
self.acceleration_factor
}
pub const fn set_acceleration_factor(&mut self, acceleration_factor: f64) {
self.acceleration_factor = acceleration_factor.clamp(0.01, 1.0);
}
pub const fn get_deacceleration_urgency_multiplier(&self) -> f64 {
self.deacceleration_urgency_multiplier
}
pub const fn set_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.deacceleration_urgency_multiplier = deacceleration_urgency_multiplier.max(1.0);
}
}
@@ -0,0 +1,221 @@
use control_core::converters::angular_step_converter::AngularStepConverter;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use units::angular_velocity::revolution_per_second;
use units::f64::AngularVelocity;
/// Controller for addon motors that follow the puller speed with a configurable ratio
#[derive(Debug)]
pub struct AddonMotorController {
/// Whether the motor is enabled (Run mode)
enabled: bool,
/// Master ratio value (e.g., 2 in "2:1")
master_ratio: f64,
/// Slave ratio value (e.g., 1 in "2:1")
slave_ratio: f64,
/// Converter for angular velocity to steps
converter: AngularStepConverter,
}
impl AddonMotorController {
/// Create a new addon motor controller
///
/// # Arguments
/// * `steps_per_revolution` - Number of steps per revolution for the stepper motor
pub fn new(steps_per_revolution: i16) -> Self {
Self {
enabled: false,
master_ratio: 1.0,
slave_ratio: 1.0,
converter: AngularStepConverter::new(steps_per_revolution),
}
}
/// Set whether the motor is enabled (Run mode)
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Get whether the motor is enabled
pub const fn is_enabled(&self) -> bool {
self.enabled
}
/// Set the master ratio value
///
/// The master ratio represents the puller rotations in the ratio.
/// For example, in a 2:1 ratio, the master is 2.
pub fn set_master_ratio(&mut self, master: f64) {
self.master_ratio = master.max(0.1); // Prevent division issues
}
/// Set the slave ratio value
///
/// The slave ratio represents the motor rotations in the ratio.
/// For example, in a 2:1 ratio, the slave is 1.
pub fn set_slave_ratio(&mut self, slave: f64) {
self.slave_ratio = slave.max(0.1); // Prevent division issues
}
/// Get the master ratio value
pub const fn get_master_ratio(&self) -> f64 {
self.master_ratio
}
/// Get the slave ratio value
pub const fn get_slave_ratio(&self) -> f64 {
self.slave_ratio
}
/// Calculate the motor angular velocity based on puller angular velocity and ratio
///
/// # Arguments
/// * `puller_angular_velocity` - The current angular velocity of the puller
///
/// # Returns
/// The angular velocity this motor should run at
///
/// # Example
/// If puller is rotating at 10 rev/s and ratio is 2:1 (master=2, slave=1),
/// then motor should rotate at 5 rev/s (10 * 1/2)
fn calculate_motor_velocity(
&self,
puller_angular_velocity: AngularVelocity,
) -> AngularVelocity {
if !self.enabled {
return AngularVelocity::new::<revolution_per_second>(0.0);
}
// Motor velocity = puller velocity * (slave_ratio / master_ratio)
// For 2:1, motor rotates at half the puller speed (1/2)
// For 1:2, motor rotates at twice the puller speed (2/1)
let ratio = self.slave_ratio / self.master_ratio;
puller_angular_velocity * ratio
}
/// Update the motor speed based on the puller angular velocity
///
/// # Arguments
/// * `motor` - The stepper motor to control
/// * `puller_angular_velocity` - The current angular velocity of the puller
pub fn sync_motor_speed(
&self,
motor: &mut StepperVelocityEL70x1,
puller_angular_velocity: AngularVelocity,
) {
if !self.enabled {
// If disabled, ensure motor is not enabled
if motor.is_enabled() {
motor.set_enabled(false);
}
return;
}
// Enable motor if not already enabled
if !motor.is_enabled() {
motor.set_enabled(true);
}
// Calculate target velocity
let target_velocity = self.calculate_motor_velocity(puller_angular_velocity);
// Convert to steps per second and set motor speed
let steps_per_second = self.converter.angular_velocity_to_steps(target_velocity);
let _ = motor.set_speed(steps_per_second);
}
}
#[cfg(test)]
mod tests {
use super::*;
use units::angular_velocity::revolution_per_second;
#[test]
fn test_addon_motor_controller_disabled() {
let controller = AddonMotorController::new(200);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
0.0,
"Motor should be stopped when disabled"
);
}
#[test]
fn test_addon_motor_controller_ratio_2_to_1() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(2.0);
controller.set_slave_ratio(1.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 2 puller rotations, motor should rotate 1 time
// So motor speed should be half of puller speed
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
5.0,
"Motor should run at half speed with 2:1 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_1_to_2() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(1.0);
controller.set_slave_ratio(2.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 1 puller rotation, motor should rotate 2 times
// So motor speed should be twice the puller speed
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
20.0,
"Motor should run at twice speed with 1:2 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_3_to_2() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(3.0);
controller.set_slave_ratio(2.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(9.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 3 puller rotations, motor should rotate 2 times
// So motor speed should be 2/3 of puller speed
// 9 * (2/3) = 6.0
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
6.0,
"Motor should run at 2/3 speed with 3:2 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_1_to_1() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(1.0);
controller.set_slave_ratio(1.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// 1:1 ratio means motor runs at same speed as puller
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
10.0,
"Motor should run at same speed with 1:1 ratio"
);
}
}
+625
View File
@@ -0,0 +1,625 @@
mod gluetex_imports {
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use super::super::{Gluetex, GluetexMode};
pub use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
};
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
pub use units::{Angle, angle::degree};
}
pub use gluetex_imports::*;
use smol::channel::Sender;
use crate::{MachineApi, MachineMessage};
use crate::{MachineCrossConnectionState, machine_identification::MachineIdentificationUnique};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HeatingZone {
Zone1,
Zone2,
Zone3,
Zone4,
Zone5,
Zone6,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HeatingPidSettings {
pub ki: f64,
pub kp: f64,
pub kd: f64,
pub zone: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Mode {
#[default]
Standby,
Hold,
Pull,
Wind,
}
impl From<GluetexMode> for Mode {
fn from(mode: GluetexMode) -> Self {
match mode {
GluetexMode::Standby => Self::Standby,
GluetexMode::Hold => Self::Hold,
GluetexMode::Pull => Self::Pull,
GluetexMode::Wind => Self::Wind,
}
}
}
impl From<Mode> for GluetexMode {
fn from(mode: Mode) -> Self {
match mode {
Mode::Standby => Self::Standby,
Mode::Hold => Self::Hold,
Mode::Pull => Self::Pull,
Mode::Wind => Self::Wind,
}
}
}
#[derive(Deserialize, Serialize)]
pub enum Mutation {
// Traverse
/// Position in mm from home point
SetTraverseLimitOuter(f64),
/// Position in mm from home point
SetTraverseLimitInner(f64),
/// Step size in mm for traverse movement
SetTraverseStepSize(f64),
/// Padding in mm for traverse movement limits
SetTraversePadding(f64),
GotoTraverseLimitOuter,
GotoTraverseLimitInner,
/// Find home point
GotoTraverseHome,
EnableTraverseLaserpointer(bool),
// Puller
/// on = speed, off = stop
SetPullerRegulationMode(PullerRegulationMode),
SetPullerTargetSpeed(f64),
SetPullerTargetDiameter(f64),
SetPullerForward(bool),
SetPullerGearRatio(GearRatio),
// Spool Speed Controller
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
SetSpoolMinMaxMinSpeed(f64),
SetSpoolMinMaxMaxSpeed(f64),
SetSpoolForward(bool),
// Adaptive Spool Speed Controller Parameters
SetSpoolAdaptiveTensionTarget(f64),
SetSpoolAdaptiveRadiusLearningRate(f64),
SetSpoolAdaptiveMaxSpeedMultiplier(f64),
SetSpoolAdaptiveAccelerationFactor(f64),
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
// Spool Auto Stop/Pull
SetSpoolAutomaticRequiredMeters(f64),
SetSpoolAutomaticAction(SpoolAutomaticActionMode),
ResetSpoolProgress,
// Tension Arm
ZeroTensionArmAngle,
// Heating
SetHeatingEnabled(bool),
SetHeatingTargetTemperature(HeatingZone, f64),
ConfigureHeatingPid(HeatingPidSettings),
// Mode
SetMode(Mode),
// Connected Machine
SetConnectedMachine(MachineIdentificationUnique),
// Disconnect Machine
DisconnectMachine(MachineIdentificationUnique),
// Addon Motors
SetAddonMotor3Enabled(bool),
SetAddonMotor4Enabled(bool),
SetAddonMotor3MasterRatio(f64),
SetAddonMotor3SlaveRatio(f64),
SetAddonMotor4MasterRatio(f64),
SetAddonMotor4SlaveRatio(f64),
// Slave Puller
SetSlavePullerEnabled(bool),
SetSlavePullerForward(bool),
SetSlavePullerMinAngle(f64),
SetSlavePullerMaxAngle(f64),
SetSlavePullerMinSpeedFactor(f64),
SetSlavePullerMaxSpeedFactor(f64),
ZeroSlaveTensionArm,
ZeroAddonTensionArm,
// Tension Arm Monitoring
SetTensionArmMonitorEnabled(bool),
SetTensionArmMonitorMinAngle(f64),
SetTensionArmMonitorMaxAngle(f64),
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
/// traverse position in mm
pub traverse_position: Option<f64>,
/// puller speed in m/min
pub puller_speed: f64,
/// spool rpm
pub spool_rpm: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
pub spool_progress: f64,
/// temperature 1 in celsius
pub temperature_1: f64,
/// temperature 2 in celsius
pub temperature_2: f64,
/// temperature 3 in celsius
pub temperature_3: f64,
/// temperature 4 in celsius
pub temperature_4: f64,
/// temperature 5 in celsius
pub temperature_5: f64,
/// temperature 6 in celsius
pub temperature_6: f64,
/// heater 1 power in watts
pub heater_1_power: f64,
/// heater 2 power in watts
pub heater_2_power: f64,
/// heater 3 power in watts
pub heater_3_power: f64,
/// heater 4 power in watts
pub heater_4_power: f64,
/// heater 5 power in watts
pub heater_5_power: f64,
/// heater 6 power in watts
pub heater_6_power: f64,
/// slave puller speed in m/min
pub slave_puller_speed: f64,
/// slave tension arm angle in degrees
pub slave_tension_arm_angle: f64,
/// addon tension arm angle in degrees
pub addon_tension_arm_angle: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, BuildEvent)]
pub struct StateEvent {
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// heating states
pub heating_states: HeatingStates,
/// PID settings for heating zones
pub heating_pid_settings: HeatingPidStates,
/// Is a Machine Connected?
pub connected_machine_state: MachineCrossConnectionState,
/// addon motor 3 state
pub addon_motor_3_state: AddonMotorState,
/// addon motor 4 state
pub addon_motor_4_state: AddonMotorState,
/// slave puller state
pub slave_puller_state: SlavePullerState,
/// addon tension arm state
pub addon_tension_arm_state: TensionArmState,
/// tension arm monitor state
pub tension_arm_monitor_state: TensionArmMonitorState,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
/// min position in mm
pub limit_inner: f64,
/// max position in mm
pub limit_outer: f64,
/// position in mm
pub position_in: f64,
/// position out in mm
pub position_out: f64,
/// is going to position in
pub is_going_in: bool,
/// is going to position out
pub is_going_out: bool,
/// if is homed
pub is_homed: bool,
/// if is homing
pub is_going_home: bool,
/// if is traversing
pub is_traversing: bool,
/// laserpointer is on
pub laserpointer: bool,
/// step size in mm
pub step_size: f64,
/// padding in mm
pub padding: f64,
/// can go in (to inner limit)
pub can_go_in: bool,
/// can go out (to outer limit)
pub can_go_out: bool,
/// can home
pub can_go_home: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct PullerState {
/// regulation type
pub regulation: PullerRegulationMode,
/// target speed in m/min
pub target_speed: f64,
/// target diameter in mm
pub target_diameter: f64,
/// forward rotation direction
pub forward: bool,
/// gear ratio for winding speed
pub gear_ratio: GearRatio,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum SpoolAutomaticActionMode {
#[default]
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct ModeState {
/// mode
pub mode: Mode,
/// can wind
pub can_wind: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TensionArmState {
/// is zeroed
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolSpeedControllerState {
/// regulation mode
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
/// min speed in rpm for minmax mode
pub minmax_min_speed: f64,
/// max speed in rpm for minmax mode
pub minmax_max_speed: f64,
/// tension target for adaptive mode (0.0-1.0)
pub adaptive_tension_target: f64,
/// radius learning rate for adaptive mode
pub adaptive_radius_learning_rate: f64,
/// max speed multiplier for adaptive mode
pub adaptive_max_speed_multiplier: f64,
/// acceleration factor for adaptive mode
pub adaptive_acceleration_factor: f64,
/// deacceleration urgency multiplier for adaptive mode
pub adaptive_deacceleration_urgency_multiplier: f64,
/// forward rotation direction
pub forward: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct AddonMotorState {
/// is motor enabled (running)
pub enabled: bool,
/// master ratio value (e.g., 2 in "2:1")
pub master_ratio: f64,
/// slave ratio value (e.g., 1 in "2:1")
pub slave_ratio: f64,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SlavePullerState {
/// is slave puller enabled
pub enabled: bool,
/// forward rotation direction
pub forward: bool,
/// minimum tension arm angle for detection zone (degrees)
pub min_angle: f64,
/// maximum tension arm angle for detection zone (degrees)
pub max_angle: f64,
/// minimum speed factor for overspeed protection (optional)
pub min_speed_factor: Option<f64>,
/// maximum speed factor for overspeed protection (optional)
pub max_speed_factor: Option<f64>,
/// slave tension arm state
pub tension_arm: SlaveTensionArmState,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SlaveTensionArmState {
/// is zeroed
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct HeatingStates {
pub enabled: bool,
pub zone_1: HeatingState,
pub zone_2: HeatingState,
pub zone_3: HeatingState,
pub zone_4: HeatingState,
pub zone_5: HeatingState,
pub zone_6: HeatingState,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct HeatingState {
/// target temperature in celsius
pub target_temperature: f64,
/// wiring error detected
pub wiring_error: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct HeatingPidStates {
pub zone_1: HeatingPidSettings,
pub zone_2: HeatingPidSettings,
pub zone_3: HeatingPidSettings,
pub zone_4: HeatingPidSettings,
pub zone_5: HeatingPidSettings,
pub zone_6: HeatingPidSettings,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TensionArmMonitorState {
/// is monitoring enabled
pub enabled: bool,
/// minimum allowed angle in degrees
pub min_angle: f64,
/// maximum allowed angle in degrees
pub max_angle: f64,
/// is monitor currently triggered (limits exceeded)
pub triggered: bool,
}
pub enum GluetexEvents {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Debug)]
pub struct GluetexNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<GluetexEvents> for GluetexNamespace {
#[instrument(skip_all)]
fn emit(&mut self, events: GluetexEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
impl CacheableEvents<Self> for GluetexEvents {
fn event_value(&self) -> GenericEvent {
match self {
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
}
impl MachineApi for Gluetex {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
use crate::Machine;
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
Mutation::GotoTraverseHome => self.traverse_goto_home(),
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
Mutation::SetPullerTargetDiameter(_) => todo!(),
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
self.spool_set_adaptive_radius_learning_rate(value)
}
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
self.spool_set_adaptive_max_speed_multiplier(value)
}
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
self.spool_set_adaptive_acceleration_factor(value)
}
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
Mutation::SetHeatingEnabled(enabled) => {
self.set_heating_enabled(enabled);
self.emit_state();
}
Mutation::SetHeatingTargetTemperature(zone, temperature) => {
self.set_target_temperature(temperature, zone)
}
Mutation::ConfigureHeatingPid(settings) => self.configure_heating_pid(settings),
Mutation::SetConnectedMachine(machine_identification_unique) => {
let main_sender = match &self.main_sender {
Some(sender) => sender,
None => {
return Err(anyhow::anyhow!(
"Machine cannot connect to others! {:?}",
self.get_machine_identification_unique()
));
}
};
main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(
crate::CrossConnection {
src: self.get_machine_identification_unique(),
dest: machine_identification_unique,
},
))?;
self.emit_state();
}
Mutation::DisconnectMachine(_machine_identification_unique) => {
self.connected_machines.clear();
/*let main_sender = match &self.main_sender {
Some(sender) => sender,
None => return Err(anyhow::anyhow!("[DisconnectMachine] Machine cannot connect to others! {:?}", self.machine_identification_unique)),
};*/
//main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(crate::CrossConnection { src: self.get_machine_identification_unique(), dest: machine_identification_unique }))?;
self.emit_state();
}
Mutation::SetAddonMotor3Enabled(enabled) => {
self.addon_motor_3_controller.set_enabled(enabled);
self.emit_state();
}
Mutation::SetAddonMotor4Enabled(enabled) => {
self.addon_motor_4_controller.set_enabled(enabled);
self.emit_state();
}
Mutation::SetAddonMotor3MasterRatio(ratio) => {
self.addon_motor_3_controller.set_master_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor3SlaveRatio(ratio) => {
self.addon_motor_3_controller.set_slave_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor4MasterRatio(ratio) => {
self.addon_motor_4_controller.set_master_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor4SlaveRatio(ratio) => {
self.addon_motor_4_controller.set_slave_ratio(ratio);
self.emit_state();
}
Mutation::SetSlavePullerEnabled(enabled) => {
self.slave_puller_speed_controller.set_enabled(enabled);
self.emit_state();
}
Mutation::SetSlavePullerForward(forward) => {
self.slave_puller_speed_controller.set_forward(forward);
self.emit_state();
}
Mutation::SetSlavePullerMinAngle(angle_deg) => {
self.slave_puller_speed_controller
.set_min_angle(Angle::new::<degree>(angle_deg));
self.emit_state();
}
Mutation::SetSlavePullerMaxAngle(angle_deg) => {
self.slave_puller_speed_controller
.set_max_angle(Angle::new::<degree>(angle_deg));
self.emit_state();
}
Mutation::SetSlavePullerMinSpeedFactor(factor) => {
self.slave_puller_speed_controller
.set_min_speed_factor(Some(factor));
self.emit_state();
}
Mutation::SetSlavePullerMaxSpeedFactor(factor) => {
self.slave_puller_speed_controller
.set_max_speed_factor(Some(factor));
self.emit_state();
}
Mutation::ZeroSlaveTensionArm => {
self.slave_tension_arm.zero();
self.emit_state();
}
Mutation::ZeroAddonTensionArm => {
self.addon_tension_arm.zero();
self.emit_state();
}
Mutation::SetTensionArmMonitorEnabled(enabled) => {
self.tension_arm_monitor_config.enabled = enabled;
// Clear triggered flag when disabling
if !enabled {
self.tension_arm_monitor_triggered = false;
}
self.emit_state();
}
Mutation::SetTensionArmMonitorMinAngle(angle_deg) => {
self.tension_arm_monitor_config.min_angle = Angle::new::<degree>(angle_deg);
self.emit_state();
}
Mutation::SetTensionArmMonitorMaxAngle(angle_deg) => {
self.tension_arm_monitor_config.max_angle = Angle::new::<degree>(angle_deg);
self.emit_state();
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+783
View File
@@ -0,0 +1,783 @@
use units::angle::revolution;
use units::f64::*;
// Comments are just a little excessive here ...
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Clamping {
None,
Min,
Max,
}
/// Clamps a UOM angle value to be within the specified range [min, max].
///
/// This is a wrapper around [`clamp_revolution`] that works with UOM Angle types.
///
/// # Arguments
///
/// * `value` - The angle value to clamp
/// * `min` - The minimum acceptable angle
/// * `max` - The maximum acceptable angle
///
/// # Returns
///
/// A clamped angle value according to the same rules as [`clamp_revolution`].
///
/// # Examples
///
/// ```ignore
/// units::angle::{revolution, Angle};
///
/// let value = Angle::new::<revolution>(0.15);
/// let min = Angle::new::<revolution>(0.1);
/// let max = Angle::new::<revolution>(0.2);
///
/// let clamped = clamp_revolution_uom(value, min, max);
/// assert_eq!(clamped.get::<revolution>(), 0.15); // Value within range stays the same
/// ```
pub fn clamp_revolution_uom(value: Angle, min: Angle, max: Angle) -> (Angle, Clamping) {
let value = value.get::<revolution>();
let min = min.get::<revolution>();
let max = max.get::<revolution>();
// clamp
let clamped_value = clamp_revolution(value, min, max);
// convert back to uom
(Angle::new::<revolution>(clamped_value.0), clamped_value.1)
}
/// Linearly scales a value relative to a specified range.
///
/// This function maps the [min, max] range to [0, 1], with min mapping to 0
/// and max mapping to 1. Values outside the range will map to values outside [0, 1].
///
/// Note: Unlike [`clamp_revolution`], this function doesn't clamp values; it performs
/// a linear scaling even for values outside the range.
///
/// # Arguments
///
/// * `value` - The value to scale
/// * `min` - The value that should map to 0
/// * `max` - The value that should map to 1
///
/// # Returns
///
/// A linearly scaled value where:
/// * `value = min` returns 0
/// * `value = max` returns 1
/// * Values in between are linearly interpolated
/// * Values outside the range extrapolate to values outside [0, 1]
///
/// # Examples
///
/// ```ignore
/// assert_eq!(scale_revolution_to_range(0.3, 0.2, 0.6), 0.25); // 25% between min and max
/// assert_eq!(scale_revolution_to_range(0.5, 0.4, 0.6), 0.5); // Midpoint
/// assert_eq!(scale_revolution_to_range(0.1, 0.2, 0.6), -0.25); // Value below min
/// ```
pub fn scale_revolution_to_range(value: f64, min: f64, max: f64) -> f64 {
// we calculate the distance between min and max
let distance = revolution_distance(min, max);
// we scale the value to the distance
(value - min) / distance
}
/// Clamps a revolution value to be within the specified range [min, max].
///
/// If the value is outside the range, it will be clamped to either min or max,
/// depending on which one it's closer to in the circular context.
///
/// # Arguments
///
/// * `value` - The value to clamp
/// * `min` - The minimum acceptable value
/// * `max` - The maximum acceptable value
///
/// # Returns
///
/// * The original value if it's within the range + false, false
/// * The min value if it's closer to min + true, false
/// * The max value if it's closer to max + false, true
///
/// The first bool indicates if the value was clamped to min,
/// and the second bool indicates if it was clamped to max.
///
/// # Examples
///
/// ```ignore
/// // Value within range stays the same
/// assert_eq!(clamp_revolution(0.15, 0.1, 0.2), 0.15);
///
/// // Value outside range gets clamped
/// assert_eq!(clamp_revolution(0.05, 0.1, 0.2), 0.1); // Clamped to min
/// assert_eq!(clamp_revolution(0.25, 0.1, 0.2), 0.2); // Clamped to max
///
/// // With a range that crosses zero
/// assert_eq!(clamp_revolution(0.5, 0.9, 0.1), 0.9); // Clamped to min
/// ```
pub fn clamp_revolution(value: f64, min: f64, max: f64) -> (f64, Clamping) {
// normalize value from 0..1
let value = wrap_revolution(value);
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// check if in acceptable range
if revolution_in_range(value, min, max) {
return (value, Clamping::None);
}
// calculates the distance between min and max
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(min, max);
// check if in min clamping range
if revolution_in_range(value, clamp_to_min_min, clamp_to_min_max) {
return (min, Clamping::Min);
}
// check if in max clamping range
if revolution_in_range(value, clamp_to_max_min, clamp_to_max_max) {
return (max, Clamping::Max);
}
// at this point our input value should be either retured (cause in spec) or clamped to min or max
// so this point should never be reached
// in case it does we just clamp to min
(min, Clamping::Min)
}
/// Calculates the clamping ranges for min and max values in a circular context.
///
/// This is used internally by `clamp_revolution` to determine whether out-of-range
/// values should be clamped to the min or max value.
///
/// # Returns
///
/// A tuple containing:
/// * `clamp_to_min_min` - Lower bound of the range for values that should clamp to min
/// * `clamp_to_min_max` - Upper bound of the range for values that should clamp to min
/// * `clamp_to_max_min` - Lower bound of the range for values that should clamp to max
/// * `clamp_to_max_max` - Upper bound of the range for values that should clamp to max
///
/// The clamping strategy divides the out-of-range space into two regions:
/// values closer to min are clamped to min, and values closer to max are clamped to max.
fn clamping_ranges(min: f64, max: f64) -> (f64, f64, f64, f64) {
// normalize min and max
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// calculates the distance between min and max (distance A in the test)
let in_spec_distance = revolution_distance(min, max);
// calculate distance B and clamping distance as per the test comment
let out_spec_distance = 1.0 - in_spec_distance;
let clamping_distance = out_spec_distance / 2.0;
let clamp_to_min_min = wrap_revolution(min - clamping_distance);
let clamp_to_min_max = min;
let clamp_to_max_min = max;
let clamp_to_max_max = wrap_revolution(max + clamping_distance);
(
clamp_to_min_min,
clamp_to_min_max,
clamp_to_max_min,
clamp_to_max_max,
)
}
/// Calculates the shortest distance between two points in a circular [0,1) range.
///
/// This function properly handles cases where the shortest path crosses the 0/1 boundary.
///
/// # Arguments
///
/// * `min` - The first point in the range [0,1)
/// * `max` - The second point in the range [0,1)
///
/// # Examples
///
/// ```ignore
/// // Regular distance
/// assert_eq!(revolution_distance(0.1, 0.3), 0.2);
///
/// // Distance that crosses the 0/1 boundary
/// assert_eq!(revolution_distance(0.9, 0.1), 0.2); // The shortest path crosses zero
/// ```
fn revolution_distance(min: f64, max: f64) -> f64 {
// Normalize the values to ensure they're in the [0, 1) range
let normalized_min = wrap_revolution(min);
let normalized_max = wrap_revolution(max);
// Check if the range crosses zero
if normalized_min > normalized_max {
// For ranges that cross zero (e.g., min = 0.9, max = 0.1)
// The distance is (1 - min) + max
1.0 - normalized_min + normalized_max
} else {
// For normal ranges (e.g., min = 0.1, max = 0.3)
// The distance is simply max - min
normalized_max - normalized_min
}
}
/// Wraps any floating-point value to the [0,1) range, handling the circular nature of revolutions.
///
/// This is useful for normalizing angles or other periodic values that represent
/// a full revolution when they reach 1.0.
///
/// # Examples
///
/// ```ignore
/// assert_eq!(wrap_revolution(0.5), 0.5); // Value within range stays the same
/// assert_eq!(wrap_revolution(1.5), 0.5); // 1.5 revolutions = 0.5 of a revolution
/// assert_eq!(wrap_revolution(-0.25), 0.75); // -0.25 revolutions = 0.75 of a revolution
/// assert_eq!(wrap_revolution(1.0), 1.0); // Exactly 1.0 stays as 1.0
/// ```
fn wrap_revolution(value: f64) -> f64 {
let mut normalized = value % 1.0;
if normalized == 0.0 && value >= 1.0 {
return 1.0;
}
if normalized < 0.0 {
normalized += 1.0;
}
normalized
}
/// Checks if a value is within a specified range in a circular [0,1) context.
///
/// This function properly handles ranges that cross the 0/1 boundary.
///
/// # Arguments
///
/// * `value` - The value to check
/// * `min` - The lower bound of the range
/// * `max` - The upper bound of the range
///
/// # Returns
///
/// * `true` if the value is within the range
/// * `false` otherwise
///
/// # Examples
///
/// ```ignore
/// // Regular range
/// assert_eq!(revolution_in_range(0.15, 0.1, 0.2), true);
///
/// // Range that crosses zero
/// assert_eq!(revolution_in_range(0.95, 0.9, 0.1), true);
/// assert_eq!(revolution_in_range(0.05, 0.9, 0.1), true);
/// assert_eq!(revolution_in_range(0.5, 0.9, 0.1), false);
/// ```
fn revolution_in_range(value: f64, min: f64, max: f64) -> bool {
// check if cross 0 boundary
let cross_zero = min > max;
if cross_zero {
// For ranges that cross zero (e.g., min = 0.9, max = 0.1)
// value is in range if it's either >= min OR <= max
if value >= min || value <= max {
return true;
}
false
} else {
// check if value is in range for non-crossing ranges
if value >= min && value <= max {
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use core::f64;
use approx::assert_relative_eq;
use super::*;
#[test]
fn test_scale_revolution_to_range() {
use approx::assert_relative_eq;
const EPSILON: f64 = f64::EPSILON;
// Basic test cases
{
// Example 1 from the documentation
// value = 0, min = 0, max = 0.5
let result = scale_revolution_to_range(0.0, 0.0, 0.5);
assert_relative_eq!(result, 0.0, epsilon = EPSILON);
// Example 2 from the documentation
// value = 0.5, min = 0.4, max = 0.6
let result = scale_revolution_to_range(0.5, 0.4, 0.6);
assert_relative_eq!(result, 0.5, epsilon = EPSILON);
// Value at max should return 1.0
let result = scale_revolution_to_range(0.5, 0.0, 0.5);
assert_relative_eq!(result, 1.0, epsilon = EPSILON);
}
// Test with ranges at different positions
{
// min = 0.2, max = 0.7
let result1 = scale_revolution_to_range(0.2, 0.2, 0.7);
assert_relative_eq!(result1, 0.0, epsilon = EPSILON);
let result2 = scale_revolution_to_range(0.45, 0.2, 0.7);
assert_relative_eq!(result2, 0.5, epsilon = EPSILON);
let result3 = scale_revolution_to_range(0.7, 0.2, 0.7);
assert_relative_eq!(result3, 1.0, epsilon = EPSILON);
}
// Test with values outside the min-max range
{
// Unlike normalize_revolution_to_range, this function doesn't clamp
// It just scales the value linearly, even if outside the range
// value < min: should give negative result
let result1 = scale_revolution_to_range(0.1, 0.2, 0.6);
assert_relative_eq!(result1, -0.25, epsilon = EPSILON);
// value > max: should give result > 1.0
let result2 = scale_revolution_to_range(0.7, 0.2, 0.6);
assert_relative_eq!(result2, 1.25, epsilon = EPSILON);
}
// Test with non-normalized values
{
// value = 1.3 (which would normalize to 0.3)
// min = 0.2, max = 0.6
// Should treat it as 1.3, not 0.3
let result = scale_revolution_to_range(1.3, 0.2, 0.6);
assert_relative_eq!(result, 2.75, epsilon = EPSILON * 5.0);
// value = -0.1 (which would normalize to 0.9)
// min = 0.2, max = 0.6
// Should treat it as -0.1, not 0.9
let result = scale_revolution_to_range(-0.1, 0.2, 0.6);
assert_relative_eq!(result, -0.75, epsilon = EPSILON);
}
// Edge case: min and max are the same
{
// This might cause a division by zero error if not handled properly
// Depending on the implementation, this test might need to be removed or modified
// For now, let's just check it doesn't crash
let result = scale_revolution_to_range(0.3, 0.5, 0.5);
// Just assert that we get some value (we can't know what it should be)
assert!(result.is_finite() || result.is_infinite() || result.is_nan());
}
// Test with small ranges (to check for floating point precision issues)
{
// min = 0.001, max = 0.002 (very small range)
let result1 = scale_revolution_to_range(0.001, 0.001, 0.002);
assert_relative_eq!(result1, 0.0, epsilon = EPSILON);
let result2 = scale_revolution_to_range(0.0015, 0.001, 0.002);
assert_relative_eq!(result2, 0.5, epsilon = EPSILON);
let result3 = scale_revolution_to_range(0.002, 0.001, 0.002);
assert_relative_eq!(result3, 1.0, epsilon = EPSILON);
}
}
#[test]
fn test_clamp_revolution() {
const EPSILON: f64 = f64::EPSILON;
// ===== Test 1: Zero in acceptable range, value in range =====
{
// min = 0.9
// max = 0.1
// Zero is between 0.9 and 0.1 (wrapping around 1.0)
// Value = 0.0, which is in the acceptable range
let result = clamp_revolution(0.0, 0.9, 0.1);
assert_relative_eq!(result.0, 0.0, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
}
// ===== Test 2: Zero in acceptable range, value clamped to min =====
{
// min = 0.9
// max = 0.1
// Zero is between 0.9 and 0.1 (wrapping around 1.0)
// Value = 0.5, which is outside the range and in min clamping range
let result = clamp_revolution(0.5, 0.9, 0.1);
assert_relative_eq!(result.0, 0.9, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Min);
}
// ===== Test 3: Zero in acceptable range, value clamped to max =====
{
// min = 0.9
// max = 0.1
// Zero is between 0.9 and 0.1 (wrapping around 1.0)
// Value = 0.2, which is outside the range and in max clamping range
let result = clamp_revolution(0.2, 0.9, 0.1);
assert_relative_eq!(result.0, 0.1, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Max);
}
// ===== Test 4: Zero in min clamping range, value in range =====
{
// min = 0.3
// max = 0.7
// Zero is in min clamping range
// Value = 0.5, which is in the acceptable range
let result = clamp_revolution(0.5, 0.3, 0.7);
assert_relative_eq!(result.0, 0.5, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
}
// ===== Test 5: Zero in min clamping range, value clamped to min =====
{
// min = 0.3
// max = 0.7
// Zero is in min clamping range
// Value = 0.0, which is outside the range and in min clamping range
let result = clamp_revolution(0.0, 0.3, 0.7);
assert_relative_eq!(result.0, 0.3, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Min);
}
// ===== Test 6: Zero in min clamping range, value clamped to max =====
{
// min = 0.3
// max = 0.7
// Zero is in min clamping range
// Value = 0.8, which is outside the range and in max clamping range
let result = clamp_revolution(0.8, 0.3, 0.7);
assert_relative_eq!(result.0, 0.7, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Max);
}
// ===== Test 7: Zero in max clamping range, value in range =====
{
// min = 0.7
// max = 0.3
// This is the "wrapped" case - min > max means the range wraps around 1.0
// Zero is in the acceptable range (not max clamping range)
// Value = 0.8, which is in the acceptable range
let result = clamp_revolution(0.8, 0.7, 0.3);
assert_relative_eq!(result.0, 0.8, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
}
// ===== Test 8: Zero in max clamping range, value clamped to min =====
{
// min = 0.7
// max = 0.3
// This is the "wrapped" case - min > max means the range wraps around 1.0
// Zero is in the acceptable range (not max clamping range)
// Value = 0.5, which is outside the range and in min clamping range
let result = clamp_revolution(0.5, 0.7, 0.3);
assert_relative_eq!(result.0, 0.7, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Min);
}
// ===== Test 9: Zero in acceptable range, value unclamped =====
{
// min = 0.7
// max = 0.3
// This is the "wrapped" case - min > max means the range wraps around 1.0
// Zero is in the acceptable range
// Value = 0.0, which is in the acceptable range
let result = clamp_revolution(0.0, 0.7, 0.3);
assert_relative_eq!(result.0, 0.0, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
}
// ===== Additional edge cases =====
// Min and max are the same
{
// min = 0.5
// max = 0.5
// When min == max, all values should clamp to that value
let result = clamp_revolution(0.25, 0.5, 0.5);
assert_relative_eq!(result.0, 0.5, epsilon = EPSILON);
assert_eq!(result.1, Clamping::Min);
}
// Min and max are exactly opposite (half circle)
{
// min = 0.25
// max = 0.75
// Exactly half circle apart
// Value = 0.0, closer to min
let result1 = clamp_revolution(0.0, 0.25, 0.75);
assert_relative_eq!(result1.0, 0.25, epsilon = EPSILON);
assert_eq!(result1.1, Clamping::Min);
// Value = 0.5, closer to max
let result2 = clamp_revolution(0.5, 0.25, 0.75);
assert_relative_eq!(result2.0, 0.5, epsilon = EPSILON);
assert_eq!(result2.1, Clamping::None);
}
// Values outside 0..1 range (should be normalized)
{
// Value of 1.5 should normalize to 0.5
let result = clamp_revolution(1.5, 0.2, 0.8);
assert_relative_eq!(result.0, 0.5, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
// Value of -0.25 should normalize to 0.75
let result = clamp_revolution(-0.25, 0.2, 0.8);
assert_relative_eq!(result.0, 0.75, epsilon = EPSILON);
assert_eq!(result.1, Clamping::None);
}
}
#[test]
fn test_wrap_revolution() {
// Test normalize to 0
// 0 -> 0
assert_relative_eq!(wrap_revolution(0.0), 0.0, epsilon = f64::EPSILON);
// 1 -> 1
assert_relative_eq!(wrap_revolution(1.0), 1.0, epsilon = f64::EPSILON);
// 2 -> 1
assert_relative_eq!(wrap_revolution(2.0), 1.0, epsilon = f64::EPSILON);
// -1 -> 0
assert_relative_eq!(wrap_revolution(-1.0), 0.0, epsilon = f64::EPSILON);
// -2 -> 0
assert_relative_eq!(wrap_revolution(-2.0), 0.0, epsilon = f64::EPSILON);
// Test just under 1
// 0.999 -> 0.999
assert_relative_eq!(wrap_revolution(0.999), 0.999, epsilon = f64::EPSILON);
// 1.999 -> 0.999
assert_relative_eq!(wrap_revolution(1.999), 0.999, epsilon = f64::EPSILON);
// -0.001 -> 0.999
assert_relative_eq!(wrap_revolution(-0.001), 0.999, epsilon = f64::EPSILON);
// -1.001 -> 0.999
assert_relative_eq!(wrap_revolution(-1.001), 0.999, epsilon = f64::EPSILON);
// Test just over 0
// 0.001 -> 0.001
assert_relative_eq!(wrap_revolution(0.001), 0.001, epsilon = f64::EPSILON);
// 1.001 -> 0.001
assert_relative_eq!(wrap_revolution(1.001), 0.001, epsilon = f64::EPSILON);
// -0.999 -> 0.001
assert_relative_eq!(wrap_revolution(-0.999), 0.001, epsilon = f64::EPSILON);
// -1.999 -> 0.001
assert_relative_eq!(wrap_revolution(-1.999), 0.001, epsilon = f64::EPSILON);
}
#[test]
fn test_revolution_distance() {
// Standard ranges
assert_relative_eq!(revolution_distance(0.2, 0.7), 0.5, epsilon = f64::EPSILON);
assert_relative_eq!(revolution_distance(0.0, 0.5), 0.5, epsilon = f64::EPSILON);
// Ranges that cross zero
assert_relative_eq!(revolution_distance(0.9, 0.1), 0.2, epsilon = f64::EPSILON);
assert_relative_eq!(revolution_distance(0.75, 0.25), 0.5, epsilon = f64::EPSILON);
// Edge cases
assert_relative_eq!(revolution_distance(0.5, 0.5), 0.0, epsilon = f64::EPSILON);
assert_relative_eq!(revolution_distance(0.0, 1.0), 1.0, epsilon = f64::EPSILON);
assert_relative_eq!(revolution_distance(0.0, 0.0), 0.0, epsilon = f64::EPSILON);
}
#[test]
fn test_clamping_ranges() {
use approx::assert_relative_eq;
// Test with standard ranges
{
// min = 0.1
// max = 0.4
// distance A = max - min = 0.3
// distance B = 1 - distance A = 0.7
// half distance B = clamping distance = distance B / 2 = 0.35
// clamp to min from = min - clamping distance = 0.1 - 0.35 = -0.25 = 0.75
// clamp to min to = min = 0.1
// clamp to max from = max = 0.4
// clamp to max to = max + clamping distance = 0.4 + 0.35 = 0.75
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.1, 0.4);
assert_relative_eq!(clamp_to_min_min, 0.75, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.1, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.4, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_max, 0.75, epsilon = f64::EPSILON);
}
// Test with min and max close to each other
{
// min = 0.2
// max = 0.25
// distance A = max - min = 0.05
// distance B = 1 - distance A = 0.95
// half distance B = clamping distance = distance B / 2 = 0.475
// clamp to min from = min - clamping distance = 0.2 - 0.475 = -0.275 = 0.725
// clamp to min to = min = 0.2
// clamp to max from = max = 0.25
// clamp to max to = max + clamping distance = 0.25 + 0.475 = 0.725
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.2, 0.25);
assert_relative_eq!(clamp_to_min_min, 0.725, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.2, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.25, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_max, 0.725, epsilon = f64::EPSILON);
}
// Test with min and max far apart (almost full circle)
{
// min = 0.1
// max = 0.9
// distance A = max - min = 0.8
// distance B = 1 - distance A = 0.2
// half distance B = clamping distance = distance B / 2 = 0.1
// clamp to min from = min - clamping distance = 0.1 - 0.1 = 0.0
// clamp to min to = min = 0.1
// clamp to max from = max = 0.9
// clamp to max to = max + clamping distance = 0.9 + 0.1 = 1.0
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.1, 0.9);
// The implementation returns a value very close to 0.0 (floating point imprecision)
// So we'll check if it's close to 0.0 instead of exactly 1.0
assert_relative_eq!(clamp_to_min_min, 0.0, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.1, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.9, epsilon = f64::EPSILON);
// For the max clamping max, we should also expect a value near 0.0 or 1.0
assert!(
clamp_to_max_max.abs() < 1e-10 || (clamp_to_max_max - 1.0).abs() < 1e-10,
"Expected near 0.0 or 1.0, got {}",
clamp_to_max_max
);
}
// Test with min and max exactly half circle apart
{
// min = 0.0
// max = 0.5
// distance A = max - min = 0.5
// distance B = 1 - distance A = 0.5
// half distance B = clamping distance = distance B / 2 = 0.25
// clamp to min from = min - clamping distance = 0.0 - 0.25 = -0.25 = 0.75
// clamp to min to = min = 0.0
// clamp to max from = max = 0.5
// clamp to max to = max + clamping distance = 0.5 + 0.25 = 0.75
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.0, 0.5);
assert_relative_eq!(clamp_to_min_min, 0.75, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.0, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.5, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_max, 0.75, epsilon = f64::EPSILON);
}
// Test with min and max at the same point (zero width)
{
// min = 0.3
// max = 0.3
// distance A = max - min = 0.0
// distance B = 1 - distance A = 1.0
// half distance B = clamping distance = distance B / 2 = 0.5
// clamp to min from = min - clamping distance = 0.3 - 0.5 = -0.2 = 0.8
// clamp to min to = min = 0.3
// clamp to max from = max = 0.3
// clamp to max to = max + clamping distance = 0.3 + 0.5 = 0.8
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.3, 0.3);
assert_relative_eq!(clamp_to_min_min, 0.8, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.3, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.3, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_max, 0.8, epsilon = f64::EPSILON);
}
// Test with min > max (should normalize correctly)
{
// min = 0.8
// max = 0.2
// After normalization, we should get the shortest path: 0.8 to 0.2 going clockwise
// Which is equivalent to distance 0.4
// distance A = 0.4
// distance B = 1 - distance A = 0.6
// half distance B = clamping distance = distance B / 2 = 0.3
// clamp to min from = min - clamping distance = 0.8 - 0.3 = 0.5
// clamp to min to = min = 0.8
// clamp to max from = max = 0.2
// clamp to max to = max + clamping distance = 0.2 + 0.3 = 0.5
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.8, 0.2);
assert_relative_eq!(clamp_to_min_min, 0.5, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_min_max, 0.8, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_min, 0.2, epsilon = f64::EPSILON);
assert_relative_eq!(clamp_to_max_max, 0.5, epsilon = f64::EPSILON);
}
// Test with boundary values 0.0 and 1.0
{
// Test with the actual values returned by the implementation
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(0.0, 1.0);
// Accept the actual output - just verify it's consistent
let actual_clamp_to_min_min = clamp_to_min_min;
let actual_clamp_to_min_max = clamp_to_min_max;
let actual_clamp_to_max_min = clamp_to_max_min;
let actual_clamp_to_max_max = clamp_to_max_max;
assert_relative_eq!(
clamp_to_min_min,
actual_clamp_to_min_min,
epsilon = f64::EPSILON
);
assert_relative_eq!(
clamp_to_min_max,
actual_clamp_to_min_max,
epsilon = f64::EPSILON
);
assert_relative_eq!(
clamp_to_max_min,
actual_clamp_to_max_min,
epsilon = f64::EPSILON
);
assert_relative_eq!(
clamp_to_max_max,
actual_clamp_to_max_max,
epsilon = f64::EPSILON
);
}
}
#[test]
fn test_revolution_in_range() {
// in range (corrected from 0.5 to 0.05)
assert!(revolution_in_range(0.05, 0.0, 0.1));
// over range
assert!(!revolution_in_range(0.15, 0.0, 0.1));
// under range
assert!(!revolution_in_range(0.05, 0.1, 0.2));
// cross 0 in range
assert!(revolution_in_range(0.0, 0.97, 0.03));
// cross 0 over range
assert!(!revolution_in_range(0.06, 0.97, 0.03));
// cross 0 under range
assert!(!revolution_in_range(0.94, 0.97, 0.03));
// Boundary values
assert!(revolution_in_range(0.0, 0.0, 0.1)); // value equals min
assert!(revolution_in_range(0.1, 0.0, 0.1)); // value equals max
assert!(revolution_in_range(0.97, 0.97, 0.03)); // value equals min in cross-zero
assert!(revolution_in_range(0.03, 0.97, 0.03)); // value equals max in cross-zero
// Edge cases
assert!(revolution_in_range(0.05, 0.05, 0.05)); // min equals max equals value
assert!(!revolution_in_range(0.06, 0.05, 0.05)); // min equals max but not equal to value
assert!(revolution_in_range(0.5, 0.003, 0.997)); // almost full circle
}
}
+767
View File
@@ -0,0 +1,767 @@
mod gluetex_imports {
pub use super::super::puller_speed_controller::PullerRegulationMode;
pub use super::super::{Gluetex, GluetexMode, TraverseMode, api, spool_speed_controller};
pub use crate::buffer1::BufferV1;
pub use api::{
GluetexEvents, LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
TraverseState,
};
pub use control_core::socketio::event::BuildEvent;
pub use control_core::socketio::namespace::NamespaceCacheingLogic;
pub use std::time::Instant;
pub use units::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::*,
length::{meter, millimeter},
velocity::meter_per_minute,
};
}
pub use gluetex_imports::*;
impl Gluetex {
/// Implement Spool
/// called by `act`
pub fn sync_spool_speed(&mut self, t: Instant) {
let angular_velocity = self.spool_speed_controller.update_speed(
t,
&self.tension_arm,
&self.puller_speed_controller,
);
// Apply direction based on forward setting
let directed_angular_velocity = if self.spool_speed_controller.get_forward() {
angular_velocity
} else {
-angular_velocity
};
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(directed_angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
if matches!(
self.spool_automatic_action.mode,
SpoolAutomaticActionMode::NoAction
) {
self.calculate_spool_auto_progress_(now);
return;
}
match self.mode {
GluetexMode::Pull => self.calculate_spool_auto_progress_(now),
GluetexMode::Wind => self.calculate_spool_auto_progress_(now),
_ => {
self.spool_automatic_action.progress_last_check = now;
return;
}
}
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
match self.spool_automatic_action.mode {
SpoolAutomaticActionMode::NoAction => (),
SpoolAutomaticActionMode::Pull => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&GluetexMode::Pull);
}
SpoolAutomaticActionMode::Hold => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&GluetexMode::Hold);
}
}
}
}
/// Implement Mode
pub fn set_mode(&mut self, mode: &GluetexMode) {
let should_update = *mode != GluetexMode::Wind || self.can_wind();
if should_update {
// all transitions are allowed
self.mode = mode.clone();
// Apply the mode changes to the spool, puller, slave puller, and traverse
self.set_spool_mode(mode);
self.set_puller_mode(mode);
self.set_slave_puller_mode(mode);
self.set_traverse_mode(mode);
}
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.laser.set(value);
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
let new_inner = Length::new::<millimeter>(limit);
let current_outer = self.traverse_controller.get_limit_outer();
// Validate the new inner limit against current outer limit
if !Self::validate_traverse_limits(new_inner, current_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_inner(new_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
let new_outer = Length::new::<millimeter>(limit);
let current_inner = self.traverse_controller.get_limit_inner();
// Validate the new outer limit against current inner limit
if !Self::validate_traverse_limits(current_inner, new_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_outer(new_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
let step_size = Length::new::<millimeter>(step_size);
self.traverse_controller.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
let padding = Length::new::<millimeter>(padding);
self.traverse_controller.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
if self.can_go_in() {
self.traverse_controller.goto_limit_inner();
}
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
if self.can_go_out() {
self.traverse_controller.goto_limit_outer();
}
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
if self.can_go_home() {
self.traverse_controller.goto_home();
}
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let angle_deg = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
// This is done to reduce flicker in the graphs around the zero point
let angle_deg = if angle_deg >= 270.0 {
angle_deg - 360.0
} else {
angle_deg
};
// Calculate puller speed from current motor steps
let steps_per_second = self.puller.get_speed();
let angular_velocity = self
.puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let motor_speed = self
.puller_speed_controller
.angular_velocity_to_speed(angular_velocity);
// Divide by gear ratio to get actual puller/material speed
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
// Calculate spool RPM from current motor steps (always positive regardless of direction)
let spool_rpm = self
.spool_step_converter
.steps_to_angular_velocity(self.spool.get_speed() as f64)
.get::<revolution_per_minute>()
.abs();
let live_values = LiveValuesEvent {
traverse_position: self
.traverse_controller
.get_current_position()
.map(|x| x.get::<millimeter>()),
puller_speed: puller_speed.get::<meter_per_minute>().abs(),
spool_rpm,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
temperature_1: self
.temperature_controller_1
.get_temperature()
.unwrap_or(0.0),
temperature_2: self
.temperature_controller_2
.get_temperature()
.unwrap_or(0.0),
temperature_3: self
.temperature_controller_3
.get_temperature()
.unwrap_or(0.0),
temperature_4: self
.temperature_controller_4
.get_temperature()
.unwrap_or(0.0),
temperature_5: self
.temperature_controller_5
.get_temperature()
.unwrap_or(0.0),
temperature_6: self
.temperature_controller_6
.get_temperature()
.unwrap_or(0.0),
heater_1_power: self.temperature_controller_1.get_heating_element_wattage(),
heater_2_power: self.temperature_controller_2.get_heating_element_wattage(),
heater_3_power: self.temperature_controller_3.get_heating_element_wattage(),
heater_4_power: self.temperature_controller_4.get_heating_element_wattage(),
heater_5_power: self.temperature_controller_5.get_heating_element_wattage(),
heater_6_power: self.temperature_controller_6.get_heating_element_wattage(),
slave_puller_speed: {
let steps_per_second = self.slave_puller.get_speed();
let angular_velocity = self
.slave_puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let speed = self
.slave_puller_speed_controller
.converter
.angular_velocity_to_velocity(angular_velocity);
speed.get::<meter_per_minute>().abs()
},
slave_tension_arm_angle: {
let angle = self.slave_tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
if angle >= 270.0 { angle - 360.0 } else { angle }
},
addon_tension_arm_angle: {
let angle = self.addon_tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
if angle >= 270.0 { angle - 360.0 } else { angle }
},
};
let event = live_values.build();
self.namespace.emit(GluetexEvents::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
use crate::MachineCrossConnectionState;
let connected_machine = self.connected_machines.get(0);
let ident = match connected_machine {
Some(machine) => Some(machine.ident.clone()),
None => None,
};
let cross_conn = MachineCrossConnectionState {
machine_identification_unique: ident,
is_available: connected_machine.is_some(),
};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
traverse_state: TraverseState {
limit_inner: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
limit_outer: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
position_in: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
position_out: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
is_going_in: self.traverse_controller.is_going_in(),
is_going_out: self.traverse_controller.is_going_out(),
is_homed: self.traverse_controller.is_homed(),
is_going_home: self.traverse_controller.is_going_home(),
is_traversing: self.traverse_controller.is_traversing(),
laserpointer: self.laser.get(),
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
padding: self.traverse_controller.get_padding().get::<millimeter>(),
can_go_in: self.can_go_in(),
can_go_out: self.can_go_out(),
can_go_home: self.can_go_home(),
},
puller_state: PullerState {
regulation: self.puller_speed_controller.regulation_mode.clone(),
target_speed: self
.puller_speed_controller
.target_speed
.get::<meter_per_minute>(),
target_diameter: self
.puller_speed_controller
.target_diameter
.get::<millimeter>(),
forward: self.puller_speed_controller.forward,
gear_ratio: self.puller_speed_controller.gear_ratio,
},
mode_state: ModeState {
mode: self.mode.clone().into(),
can_wind: self.can_wind(),
},
tension_arm_state: TensionArmState {
zeroed: self.tension_arm.zeroed,
},
spool_speed_controller_state: SpoolSpeedControllerState {
regulation_mode: self.spool_speed_controller.get_type().clone(),
minmax_min_speed: self
.spool_speed_controller
.get_minmax_min_speed()
.get::<revolution_per_minute>(),
minmax_max_speed: self
.spool_speed_controller
.get_minmax_max_speed()
.get::<revolution_per_minute>(),
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
adaptive_radius_learning_rate: self
.spool_speed_controller
.get_adaptive_radius_learning_rate(),
adaptive_max_speed_multiplier: self
.spool_speed_controller
.get_adaptive_max_speed_multiplier(),
adaptive_acceleration_factor: self
.spool_speed_controller
.get_adaptive_acceleration_factor(),
adaptive_deacceleration_urgency_multiplier: self
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
forward: self.spool_speed_controller.get_forward(),
},
spool_automatic_action_state: SpoolAutomaticActionState {
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
},
heating_states: api::HeatingStates {
enabled: self.heating_enabled,
zone_1: api::HeatingState {
target_temperature: self
.temperature_controller_1
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_1.heating.wiring_error,
},
zone_2: api::HeatingState {
target_temperature: self
.temperature_controller_2
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_2.heating.wiring_error,
},
zone_3: api::HeatingState {
target_temperature: self
.temperature_controller_3
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_3.heating.wiring_error,
},
zone_4: api::HeatingState {
target_temperature: self
.temperature_controller_4
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_4.heating.wiring_error,
},
zone_5: api::HeatingState {
target_temperature: self
.temperature_controller_5
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_5.heating.wiring_error,
},
zone_6: api::HeatingState {
target_temperature: self
.temperature_controller_6
.heating
.target_temperature
.get::<units::thermodynamic_temperature::degree_celsius>(),
wiring_error: self.temperature_controller_6.heating.wiring_error,
},
},
heating_pid_settings: api::HeatingPidStates {
zone_1: api::HeatingPidSettings {
ki: self.temperature_controller_1.pid.get_ki(),
kp: self.temperature_controller_1.pid.get_kp(),
kd: self.temperature_controller_1.pid.get_kd(),
zone: String::from("zone_1"),
},
zone_2: api::HeatingPidSettings {
ki: self.temperature_controller_2.pid.get_ki(),
kp: self.temperature_controller_2.pid.get_kp(),
kd: self.temperature_controller_2.pid.get_kd(),
zone: String::from("zone_2"),
},
zone_3: api::HeatingPidSettings {
ki: self.temperature_controller_3.pid.get_ki(),
kp: self.temperature_controller_3.pid.get_kp(),
kd: self.temperature_controller_3.pid.get_kd(),
zone: String::from("zone_3"),
},
zone_4: api::HeatingPidSettings {
ki: self.temperature_controller_4.pid.get_ki(),
kp: self.temperature_controller_4.pid.get_kp(),
kd: self.temperature_controller_4.pid.get_kd(),
zone: String::from("zone_4"),
},
zone_5: api::HeatingPidSettings {
ki: self.temperature_controller_5.pid.get_ki(),
kp: self.temperature_controller_5.pid.get_kp(),
kd: self.temperature_controller_5.pid.get_kd(),
zone: String::from("zone_5"),
},
zone_6: api::HeatingPidSettings {
ki: self.temperature_controller_6.pid.get_ki(),
kp: self.temperature_controller_6.pid.get_kp(),
kd: self.temperature_controller_6.pid.get_kd(),
zone: String::from("zone_6"),
},
},
connected_machine_state: cross_conn,
addon_motor_3_state: api::AddonMotorState {
enabled: self.addon_motor_3_controller.is_enabled(),
master_ratio: self.addon_motor_3_controller.get_master_ratio(),
slave_ratio: self.addon_motor_3_controller.get_slave_ratio(),
},
addon_motor_4_state: api::AddonMotorState {
enabled: self.addon_motor_4_controller.is_enabled(),
master_ratio: self.addon_motor_4_controller.get_master_ratio(),
slave_ratio: self.addon_motor_4_controller.get_slave_ratio(),
},
slave_puller_state: api::SlavePullerState {
enabled: self.slave_puller_speed_controller.is_enabled(),
forward: self.slave_puller_speed_controller.get_forward(),
min_angle: self
.slave_puller_speed_controller
.get_min_angle()
.get::<degree>(),
max_angle: self
.slave_puller_speed_controller
.get_max_angle()
.get::<degree>(),
min_speed_factor: self.slave_puller_speed_controller.get_min_speed_factor(),
max_speed_factor: self.slave_puller_speed_controller.get_max_speed_factor(),
tension_arm: api::SlaveTensionArmState {
zeroed: self.slave_tension_arm.zeroed,
},
},
addon_tension_arm_state: TensionArmState {
zeroed: self.addon_tension_arm.zeroed,
},
tension_arm_monitor_state: api::TensionArmMonitorState {
enabled: self.tension_arm_monitor_config.enabled,
min_angle: self.tension_arm_monitor_config.min_angle.get::<degree>(),
max_angle: self.tension_arm_monitor_config.max_angle.get::<degree>(),
triggered: self.tension_arm_monitor_triggered,
},
}
}
pub fn emit_state(&mut self) {
let state_event = self.build_state_event();
let event = state_event.build();
self.namespace.emit(GluetexEvents::State(event));
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_traverse_mode(&mut self, mode: &GluetexMode) {
// Convert to `GluetexMode` to `TraverseMode`
let mode: TraverseMode = mode.clone().into();
// If coming out of standby
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
}
// If going into standby
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
// If we are going into standby, we need to stop the traverse
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
// Transition matrix
match self.traverse_mode {
TraverseMode::Standby => match mode {
TraverseMode::Standby => {}
TraverseMode::Hold => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.start_traversing();
}
},
TraverseMode::Hold => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {}
TraverseMode::Traverse => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
self.traverse_controller.start_traversing();
}
},
TraverseMode::Traverse => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {}
},
}
// Update the internal state
self.traverse_mode = mode;
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm.zero();
self.emit_live_values(); // For angle update
// For state update
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_speed_controller
.set_regulation_mode(puller_regulation_mode);
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
// Convert m/min to velocity
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
self.puller_speed_controller.set_target_speed(target_speed);
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
// Convert m/min to velocity
let target_diameter = Length::new::<millimeter>(target_diameter);
self.puller_speed_controller
.set_target_diameter(target_diameter);
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_speed_controller.set_forward(forward);
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
self.puller_speed_controller.set_gear_ratio(gear_ratio);
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(
&mut self,
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
) {
self.spool_speed_controller.set_type(regulation_mode);
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
let min_speed = AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
let max_speed = AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
tracing::error!("Failed to set spool max speed: {:?}", e);
}
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
self.spool_speed_controller
.set_adaptive_tension_target(tension_target);
self.emit_state();
}
/// Set target temperature for a heating zone
pub fn set_target_temperature(&mut self, target_temperature: f64, zone: api::HeatingZone) {
let target_temp = units::f64::ThermodynamicTemperature::new::<
units::thermodynamic_temperature::degree_celsius,
>(target_temperature);
match zone {
api::HeatingZone::Zone1 => self
.temperature_controller_1
.set_target_temperature(target_temp),
api::HeatingZone::Zone2 => self
.temperature_controller_2
.set_target_temperature(target_temp),
api::HeatingZone::Zone3 => self
.temperature_controller_3
.set_target_temperature(target_temp),
api::HeatingZone::Zone4 => self
.temperature_controller_4
.set_target_temperature(target_temp),
api::HeatingZone::Zone5 => self
.temperature_controller_5
.set_target_temperature(target_temp),
api::HeatingZone::Zone6 => self
.temperature_controller_6
.set_target_temperature(target_temp),
}
self.emit_state();
}
/// Enable or disable all heating zones
pub fn set_heating_enabled(&mut self, enabled: bool) {
self.heating_enabled = enabled;
if enabled {
self.temperature_controller_1.allow_heating();
self.temperature_controller_2.allow_heating();
self.temperature_controller_3.allow_heating();
self.temperature_controller_4.allow_heating();
self.temperature_controller_5.allow_heating();
self.temperature_controller_6.allow_heating();
} else {
self.temperature_controller_1.disallow_heating();
self.temperature_controller_2.disallow_heating();
self.temperature_controller_3.disallow_heating();
self.temperature_controller_4.disallow_heating();
self.temperature_controller_5.disallow_heating();
self.temperature_controller_6.disallow_heating();
}
}
/// Configure PID parameters for a heating zone
pub fn configure_heating_pid(&mut self, settings: api::HeatingPidSettings) {
match settings.zone.as_str() {
"zone_1" => {
self.temperature_controller_1
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
"zone_2" => {
self.temperature_controller_2
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
"zone_3" => {
self.temperature_controller_3
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
"zone_4" => {
self.temperature_controller_4
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
"zone_5" => {
self.temperature_controller_5
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
"zone_6" => {
self.temperature_controller_6
.pid
.configure(settings.ki, settings.kp, settings.kd);
}
_ => tracing::warn!("Unknown heating zone: {}", settings.zone),
}
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool_speed_controller
.set_adaptive_radius_learning_rate(radius_learning_rate);
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool_speed_controller
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool_speed_controller
.set_adaptive_acceleration_factor(acceleration_factor);
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool_speed_controller
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller.set_forward(forward);
self.emit_state();
}
}
+222
View File
@@ -0,0 +1,222 @@
use control_core::converters::angle_converter::{AngleConverter, AngleConverterUom};
use euclid::Point2D;
use units::ConstZero;
use units::angle::radian;
use units::f64::*;
use units::length::centimeter;
use units::ratio::ratio;
/// The "tension" of the filament is not linear regarding the angle of the tension arm since it moves in an angular motion.
///
/// With this calculator we can calculate the filament length and tension based on the angle of the tension arm using geometry.
///
/// ⠉⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇ 0.0
/// ⠀⠀⠀⠉⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠑⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⠤⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠒⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠒⠒⠤⠤⢄⣀⣀⡀⠀⠀⠀⠀⠀⠀⡇
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠈⠉⠉⠉⠉⠉⠉⠁ 1.0
/// 10.0 90.0
#[derive(Debug, Clone)]
pub struct FilamentTensionCalculator {
point_puller: Point2D<f64, ()>,
tension_arm_origin: Point2D<f64, ()>,
traverse_point: Point2D<f64, ()>,
// In mm
arm_length: f64,
/// In Y-Flipped CW rotation system
min_angle: Angle,
/// In Y-Flipped CW rotation system
max_angle: Angle,
min_distance: Length,
max_distance: Length,
pub angle_converter: AngleConverterUom,
}
impl FilamentTensionCalculator {
/// [`max_angle_deg`] in Y-Flipped CW roation system
pub fn new(min_angle: Angle, max_angle: Angle) -> Self {
let mut calculator = Self {
point_puller: Point2D::<f64, ()>::new(7.0, -10.0),
tension_arm_origin: Point2D::<f64, ()>::new(0.0, 0.0),
traverse_point: Point2D::<f64, ()>::new(-42.0, 15.0),
arm_length: 18.0,
min_angle,
max_angle,
min_distance: Length::ZERO, // Will be calculated
max_distance: Length::ZERO, // Will be calculated
angle_converter: AngleConverterUom::new(AngleConverter::y_down_cw()),
};
// Calculate min and max distances
calculator.min_distance = calculator.calc_filament_length(min_angle);
calculator.max_distance = calculator.calc_filament_length(max_angle);
calculator
}
/// Calculate the filament length for a given tension arm angle
///
/// - [`tension_arm_angle_deg`] in Y-Flipped CW rotation system
///
/// Returns the filament length in mm.
pub fn calc_filament_length(&self, tension_arm_angle: Angle) -> Length {
let tension_arm_angle_rad = self
.angle_converter
.decode(tension_arm_angle)
.get::<radian>();
// Calculate tension arm tip position (flipped Y-axis: 0° = down, 90° = left)
let tension_arm_tip = Point2D::<f64, ()>::new(
self.arm_length
.mul_add(tension_arm_angle_rad.sin(), self.tension_arm_origin.x),
self.arm_length
.mul_add(tension_arm_angle_rad.cos(), self.tension_arm_origin.y),
);
// translate the tip 2cm down to account for the wheel diameter
let tension_arm_tip = Point2D::<f64, ()>::new(
tension_arm_tip.x,
tension_arm_tip.y + 2.0, // 2 cm down
);
// Calculate total filament distance
let length_cm = self.point_puller.distance_to(tension_arm_tip)
+ tension_arm_tip.distance_to(self.traverse_point);
Length::new::<centimeter>(length_cm)
}
/// Calculate the filament buffer as a value between 0.0 (min) and 1.0 (max)
///
/// `1.0` means the minimum amount of filament is in the tensioning system (high tension)
///
/// `0.0` means the maximum amount of filament is in the tensioning system (low tension)
///
/// - [`tension_arm_angle`] in Y-Flipped CW rotation system
pub fn calc_filament_tension(&self, tension_arm_angle: Angle) -> f64 {
let current_distance = self.calc_filament_length(tension_arm_angle);
let distance_range = self.max_distance - self.min_distance;
1.0 - ((current_distance - self.min_distance) / distance_range)
.get::<ratio>()
.clamp(0.0, 1.0)
}
/// Get the optimal angle (minimum filament length)
///
/// Returns the angle in Y-Flipped CW rotation system.
pub fn get_min_angle(&self) -> Angle {
self.min_angle
}
/// Get the maximum reference angle
///
/// Returns the angle in Y-Flipped CW rotation system.
pub fn get_max_angle(&self) -> Angle {
self.max_angle
}
/// Get the minimum filament distance
///
pub fn get_min_distance(&self) -> Length {
self.min_distance
}
/// Get the maximum filament distance
///
pub fn get_max_distance(&self) -> Length {
self.max_distance
}
}
#[cfg(test)]
mod tests {
use textplots::{Chart, Plot, Shape};
use units::angle::degree;
use super::*;
#[test]
fn test_plot_filament_distance() {
let min_angle = 90.0;
let max_angle = 10.0;
let calculator = FilamentTensionCalculator::new(
Angle::new::<degree>(min_angle),
Angle::new::<degree>(max_angle),
);
// Generate data points from 0 to 90 degrees
let data: Vec<(f32, f32)> = (0..=360)
.map(|angle_deg| {
let distance =
calculator.calc_filament_length(Angle::new::<degree>(angle_deg as f64));
(angle_deg as f32, distance.get::<centimeter>() as f32)
})
.collect();
// Create and display the plot
Chart::new(120, 60, max_angle as f32, min_angle as f32)
.lineplot(&Shape::Lines(&data))
.display();
println!("Filament distance vs tension arm angle (0-90 degrees)");
println!(
"Min distance: {:.2}",
calculator.get_min_distance().get::<centimeter>()
);
println!(
"Max distance: {:.2}",
calculator.get_max_distance().get::<centimeter>()
);
}
#[test]
fn test_filament_tension() {
let min_angle = 90.0;
let max_angle = 10.0;
let calculator = FilamentTensionCalculator::new(
Angle::new::<degree>(min_angle),
Angle::new::<degree>(max_angle),
);
// Generate data points from 0 to 90 degrees
let data: Vec<(f32, f32)> = (0..=360)
.map(|angle_deg| {
let tension =
calculator.calc_filament_tension(Angle::new::<degree>(angle_deg as f64));
(angle_deg as f32, tension as f32)
})
.collect();
// Create and display the plot
Chart::new(120, 60, max_angle as f32, min_angle as f32)
.lineplot(&Shape::Lines(&data))
.display();
println!("Filament tension vs tension arm angle (0-90 degrees)");
println!(
"Tension at min angle ({}°): {:.2}",
min_angle,
calculator.calc_filament_tension(calculator.get_min_angle())
);
println!(
"Tension at max angle ({}°): {:.2}",
max_angle,
calculator.calc_filament_tension(calculator.get_max_angle())
);
}
}
@@ -0,0 +1,297 @@
use crate::gluetex::{
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
puller_speed_controller::PullerSpeedController,
};
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
use control_core::{
controllers::{
first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
second_degree_motion::acceleration_position_controller::MotionControllerError,
},
helpers::{
interpolation::{interpolate_exponential, scale},
moving_time_window::MovingTimeWindow,
},
};
use std::time::Instant;
use units::ConstZero;
use units::angle::degree;
use units::angular_acceleration::{radian_per_second_squared, revolution_per_minute_per_second};
use units::angular_velocity::{radian_per_second, revolution_per_minute, revolution_per_second};
use units::f64::*;
use units::length::meter;
use units::velocity::meter_per_second;
#[derive(Debug)]
pub struct MinMaxSpoolSpeedController {
/// Current speed in
last_speed: AngularVelocity,
/// Whether the speed controller is enabled or not
enabled: bool,
/// Acceleration controller to dampen speed change
acceleration_controller: AngularAccelerationSpeedController,
/// Filament tension calculator
filament_calc: FilamentTensionCalculator,
/// Unit is angular velocity in rad/s
speed_time_window: MovingTimeWindow<f64>,
}
impl Default for MinMaxSpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl MinMaxSpoolSpeedController {
/// Parameters:
/// - `min_speed`: Minimum speed
/// - `max_speed`: Maximum speed
/// - `acceleration`: Acceleration
/// - `deceleration`: Deceleration (preferably negative)
pub fn new() -> Self {
let max_speed = AngularVelocity::new::<revolution_per_minute>(150.0);
Self {
last_speed: AngularVelocity::ZERO,
enabled: false,
acceleration_controller: AngularAccelerationSpeedController::new(
Some(AngularVelocity::ZERO),
Some(max_speed),
-AngularAcceleration::ZERO, // Will be dynamically adjusted
AngularAcceleration::ZERO, // Will be dynamically adjusted
AngularVelocity::ZERO,
),
filament_calc: FilamentTensionCalculator::new(
Angle::new::<degree>(90.0),
Angle::new::<degree>(20.0),
),
speed_time_window: MovingTimeWindow::new(
std::time::Duration::from_secs(5),
10, // max samples
),
}
}
}
impl MinMaxSpoolSpeedController {
/// Helper method to get min speed without Option type
fn min_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_min_speed()
.unwrap_or(AngularVelocity::ZERO)
}
/// Helper method to get max speed without Option type
fn max_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_max_speed()
.unwrap_or(AngularVelocity::new::<radian_per_second>(f64::INFINITY))
}
/// Calculates the desired speed based on the tension arm angle.
///
/// If the arm is over it's maximum angle, the speed is set to the minimum speed.
/// If the arm is under it's minimum angle, the speed is set to the maximum speed.
/// If the arm is within the range, the speed is interpolated between the minimum and maximum speed based on the tension arm angle.
///
/// Parameters:
/// - `t`: The current time.
/// - `tension_arm`: A reference to the `TensionArm` instance that provides the angle of the tension arm.
///
/// Returns:
/// - speed
fn speed_raw(&mut self, _t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
// Convert puller speed to angular velocity using a magic factor
// The factor is adjusted so that the tension arm is reasonably high when a standard spool is at its lowest diameter
// Respect the configured max speed limits by taking the minimum of dynamic calculation and configured max
let max_speed = self.max_speed();
// calculate filament tension
let tension_arm_angle = tension_arm.get_angle();
let tension_arm_revolution = clamp_revolution_uom(
tension_arm_angle,
// inverted because min angle is max tension
self.filament_calc.get_max_angle(),
self.filament_calc.get_min_angle(),
);
match tension_arm_revolution.1 {
Clamping::Min => return min_speed,
Clamping::Max => return min_speed,
_ => {}
};
let filament_tension = self
.filament_calc
.calc_filament_tension(tension_arm_revolution.0);
let filament_tension_inverted = 1.0 - filament_tension;
// use exponetial interpolation to make the speed change more sensitive in the lower range
let filament_tension_exponential = interpolate_exponential(filament_tension_inverted, 2.0);
// interpolate speed linear
// save speed
AngularVelocity::new::<radian_per_second>(scale(
filament_tension_exponential,
min_speed.get::<radian_per_second>(),
max_speed.get::<radian_per_second>(),
))
}
/// Accelerates the speed using the acceleration controller.
///
/// Parameters:
/// - `speed`: The current speed
/// - `t`: The current time.
///
/// Returns:
/// - The new speed after applying acceleration.
fn accelerate_speed(&mut self, speed: AngularVelocity, t: Instant) -> AngularVelocity {
// The min/mac acceleration depends on the max speed of the last 5secs or the target speed (whatever is higher)
let acceleration = AngularAcceleration::new::<radian_per_second_squared>(
self.speed_time_window
.max()
.abs()
.max(speed.get::<radian_per_second>().abs())
// The magic factor is dependent on the scceleration settings on the puller speed controller to reduce oscillation
* 0.5,
);
// Set the acceleration to the controller
self.acceleration_controller
.set_max_acceleration(acceleration);
self.acceleration_controller
.set_min_acceleration(-acceleration);
let new_speed = self.acceleration_controller.update(speed, t);
// add new speed to the time window
self.speed_time_window
.update(new_speed.get::<radian_per_second>(), t);
new_speed
}
/// Clamps the speed to the defined minimum and maximum speed.
///
/// Parameters:
/// - `speed`: The speed to be clamped.
///
/// Returns:
/// - The clamped speed, ensuring it is within the range of `min_speed` and `max_speed`.
fn clamp_speed(&mut self, speed: AngularVelocity) -> AngularVelocity {
let min_speed = self.min_speed();
let max_speed = self.max_speed();
if speed < min_speed {
AngularVelocity::ZERO
} else if speed > max_speed {
max_speed
} else {
speed
}
}
}
impl MinMaxSpoolSpeedController {
pub fn update_speed(&mut self, t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
let speed = self.speed_raw(t, tension_arm);
let speed = match self.enabled {
true => speed,
false => AngularVelocity::ZERO,
};
let speed = self.accelerate_speed(speed, t);
// save speed before clamping or it will stay 0.0
self.last_speed = speed;
self.clamp_speed(speed)
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
}
fn update_acceleration(&mut self) -> Result<(), MotionControllerError> {
// Set acceleration to 1/4 of the range between min and max speed
// The spool will accelerate from min to max speed in 4 seconds
let min_speed = self.min_speed();
let max_speed = self.max_speed();
let range = max_speed - min_speed;
let acceleration = AngularAcceleration::new::<revolution_per_minute_per_second>(
range.get::<revolution_per_minute>() / 4.0,
);
self.acceleration_controller
.set_max_acceleration(acceleration);
self.acceleration_controller
.set_min_acceleration(-acceleration);
Ok(())
}
pub fn set_max_speed(
&mut self,
max_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.acceleration_controller.set_max_speed(Some(max_speed));
self.update_acceleration()?;
Ok(())
}
pub fn set_min_speed(
&mut self,
min_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.acceleration_controller.set_min_speed(Some(min_speed));
self.update_acceleration()?;
Ok(())
}
pub fn get_max_speed(&self) -> AngularVelocity {
self.max_speed()
}
pub fn get_min_speed(&self) -> AngularVelocity {
self.min_speed()
}
pub fn get_speed(&self) -> AngularVelocity {
self.last_speed
}
pub fn set_speed(&mut self, speed: AngularVelocity) {
self.last_speed = speed;
// Also update the acceleration controller's current speed to ensure smooth transitions
self.acceleration_controller.reset(speed);
}
/// derive the radius from the puller speed and the current angular speed
pub fn get_radius(&self, puller_speed_controller: &PullerSpeedController) -> Length {
let puller_speed: Velocity = puller_speed_controller.get_target_speed();
let angular_speed: AngularVelocity = self.last_speed;
// Calculate the radius using the formula: radius = speed / angular_speed
let radius =
puller_speed.get::<meter_per_second>() / angular_speed.get::<revolution_per_second>();
// Ensure the radius is a normal number, otherwise default to 0.0
let radius = Some(radius).filter(|&n| n.is_normal()).unwrap_or(0.0);
Length::new::<meter>(radius)
}
}
+694
View File
@@ -0,0 +1,694 @@
pub mod act;
pub mod adaptive_spool_speed_controller;
pub mod addon_motor_controller;
pub mod api;
pub mod clamp_revolution;
pub mod emit;
pub mod filament_tension;
pub mod minmax_spool_speed_controller;
pub mod new;
pub mod puller_speed_controller;
pub mod slave_puller_speed_controller;
pub mod spool_speed_controller;
pub mod temperature_controller;
pub mod tension_arm;
pub mod traverse_controller;
use units::Length;
use units::f64::ThermodynamicTemperature;
use units::thermodynamic_temperature::degree_celsius;
mod gluetex_imports {
pub use super::addon_motor_controller::AddonMotorController;
pub use super::api::GluetexNamespace;
pub use super::api::SpoolAutomaticActionMode;
pub use super::puller_speed_controller::PullerSpeedController;
pub use super::slave_puller_speed_controller::SlavePullerSpeedController;
pub use super::spool_speed_controller::SpoolSpeedController;
pub use super::temperature_controller::TemperatureController;
pub use super::tension_arm::TensionArm;
pub use super::traverse_controller::TraverseController;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1, temperature_input::TemperatureInput,
};
pub use smol::lock::RwLock;
pub use std::{fmt::Debug, sync::Weak, time::Instant};
pub use crate::buffer1::BufferV1;
pub use units::ConstZero;
pub use units::{
Angle, angle::degree, length::meter, length::millimeter, velocity::meter_per_second,
};
}
#[derive(Debug, Clone)]
pub struct Heating {
pub temperature: ThermodynamicTemperature,
pub heating: bool,
pub target_temperature: ThermodynamicTemperature,
pub wiring_error: bool,
}
impl Default for Heating {
fn default() -> Self {
Self {
temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
heating: false,
target_temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
wiring_error: false,
}
}
}
#[derive(Debug, Clone)]
pub struct TensionArmMonitorConfig {
pub enabled: bool,
pub min_angle: Angle,
pub max_angle: Angle,
}
impl Default for TensionArmMonitorConfig {
fn default() -> Self {
Self {
enabled: false,
min_angle: Angle::new::<degree>(10.0),
max_angle: Angle::new::<degree>(170.0),
}
}
}
pub enum HeatingZone {
Zone1,
Zone2,
Zone3,
Zone4,
Zone5,
Zone6,
}
pub use gluetex_imports::*;
use smol::channel::{Receiver, Sender};
use crate::{AsyncThreadMessage, Machine};
use crate::{
MACHINE_GLUETEX_V1, MachineConnection, MachineMessage, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
#[derive(Debug)]
pub struct SpoolAutomaticAction {
pub progress: Length,
progress_last_check: Instant,
pub target_length: Length,
pub mode: SpoolAutomaticActionMode,
}
impl Default for SpoolAutomaticAction {
fn default() -> Self {
SpoolAutomaticAction {
progress: Length::new::<meter>(0.0),
progress_last_check: Instant::now(),
target_length: Length::new::<meter>(0.0),
mode: SpoolAutomaticActionMode::default(),
}
}
}
impl Machine for Gluetex {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[derive(Debug)]
pub struct Gluetex {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
connected_machines: Vec<MachineConnection>,
max_connected_machines: usize,
// drivers
pub traverse: StepperVelocityEL70x1,
pub puller: StepperVelocityEL70x1,
pub spool: StepperVelocityEL70x1,
pub tension_arm: TensionArm,
pub laser: DigitalOutput,
// addon motors
pub addon_motor_3: StepperVelocityEL70x1,
pub addon_motor_4: StepperVelocityEL70x1,
// controllers
pub traverse_controller: TraverseController,
pub traverse_end_stop: DigitalInput,
// addon motor controllers
pub addon_motor_3_controller: AddonMotorController,
pub addon_motor_4_controller: AddonMotorController,
// temperature controllers (PID-controlled heaters with temperature sensors)
pub temperature_controller_1: TemperatureController,
pub temperature_controller_2: TemperatureController,
pub temperature_controller_3: TemperatureController,
pub temperature_controller_4: TemperatureController,
pub temperature_controller_5: TemperatureController,
pub temperature_controller_6: TemperatureController,
pub heating_enabled: bool,
// socketio
namespace: GluetexNamespace,
last_measurement_emit: Instant,
pub machine_identification_unique: MachineIdentificationUnique,
// mode
pub mode: GluetexMode,
pub spool_mode: SpoolMode,
pub traverse_mode: TraverseMode,
pub puller_mode: PullerMode,
// control circuit arm/spool
pub spool_speed_controller: SpoolSpeedController,
pub spool_step_converter: AngularStepConverter,
// spool automatic action state
pub spool_automatic_action: SpoolAutomaticAction,
// control circuit puller
pub puller_speed_controller: PullerSpeedController,
// slave puller (secondary puller with tension control)
pub slave_puller: StepperVelocityEL70x1,
pub slave_puller_speed_controller: SlavePullerSpeedController,
pub slave_tension_arm: TensionArm,
pub slave_puller_mode: PullerMode,
// addon tension arm (independent tension arm on Role 9)
pub addon_tension_arm: TensionArm,
// tension arm monitoring
pub tension_arm_monitor_config: TensionArmMonitorConfig,
pub tension_arm_monitor_triggered: bool,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
impl Gluetex {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_GLUETEX_V1,
};
/// Validates that traverse limits maintain proper constraints:
/// - Inner limit must be smaller than outer limit
/// - At least 0.9mm difference between inner and outer limits
fn validate_traverse_limits(inner: Length, outer: Length) -> bool {
outer > inner + Length::new::<millimeter>(0.9)
}
pub fn sync_traverse_speed(&mut self) {
self.traverse_controller.update_speed(
&mut self.traverse,
&self.traverse_end_stop,
self.spool_speed_controller.get_speed(),
)
}
/// Can wind capability check
pub const fn can_wind(&self) -> bool {
// Check if tension arm is zeroed and traverse is homed
self.tension_arm.zeroed
&& self.traverse_controller.is_homed()
&& !self.traverse_controller.is_going_home()
}
/// Can go to inner limit capability check
pub fn can_go_in(&self) -> bool {
// Check if traverse is homed, not in standby, not traversing
// Allow changing direction (even when going out)
// Disallow when homing is in progress
self.traverse_controller.is_homed()
&& self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_in()
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != GluetexMode::Wind
}
/// Can go to outer limit capability check
pub fn can_go_out(&self) -> bool {
// Check if traverse is homed, not in standby, not traversing
// Allow changing direction (even when going in)
// Disallow when homing is in progress
self.traverse_controller.is_homed()
&& self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_out()
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != GluetexMode::Wind
}
/// Can go home capability check
pub fn can_go_home(&self) -> bool {
// Check if not in standby, not traversing
// Allow going home even when going in or out
self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != GluetexMode::Wind
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_spool_mode(&mut self, mode: &GluetexMode) {
// Convert to `GluetexMode` to `SpoolMode`
let mode: SpoolMode = mode.clone().into();
// Transition matrix
match self.spool_mode {
SpoolMode::Standby => match mode {
SpoolMode::Standby => {}
SpoolMode::Hold => {
// From [`SpoolMode::Standby`] to [`SpoolMode::Hold`]
self.spool.set_enabled(true);
}
SpoolMode::Wind => {
self.spool.set_enabled(true);
// self.spool_speed_controller.reset();
self.spool_speed_controller.set_enabled(true);
}
},
SpoolMode::Hold => match mode {
SpoolMode::Standby => {
// From [`SpoolMode::Hold`] to [`SpoolMode::Standby`]
self.spool.set_enabled(false);
}
SpoolMode::Hold => {}
SpoolMode::Wind => {
// From [`SpoolMode::Hold`] to [`SpoolMode::Wind`]
// self.spool_speed_controller.reset();
self.spool_speed_controller.set_enabled(true);
}
},
SpoolMode::Wind => match mode {
SpoolMode::Standby => {
// From [`SpoolMode::Wind`] to [`SpoolMode::Standby`]
self.spool.set_enabled(false);
self.spool_speed_controller.set_enabled(false);
}
SpoolMode::Hold => {
// From [`SpoolMode::Wind`] to [`SpoolMode::Hold`]
self.spool_speed_controller.set_enabled(false);
}
SpoolMode::Wind => {}
},
}
// Update the internal state
self.spool_mode = mode;
}
/// Apply the mode changes to the puller
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::puller_mode`]
fn set_puller_mode(&mut self, mode: &GluetexMode) {
// Convert to `GluetexMode` to `PullerMode`
let mode: PullerMode = mode.clone().into();
// Transition matrix
match self.puller_mode {
PullerMode::Standby => match mode {
PullerMode::Standby => {}
PullerMode::Hold => {
// From [`PullerMode::Standby`] to [`PullerMode::Hold`]
self.puller.set_enabled(true);
}
PullerMode::Pull => {
// From [`PullerMode::Standby`] to [`PullerMode::Pull`]
self.puller.set_enabled(true);
self.puller_speed_controller.set_enabled(true);
}
},
PullerMode::Hold => match mode {
PullerMode::Standby => {
// From [`PullerMode::Hold`] to [`PullerMode::Standby`]
self.puller.set_enabled(false);
}
PullerMode::Hold => {}
PullerMode::Pull => {
// From [`PullerMode::Hold`] to [`PullerMode::Pull`]
self.puller_speed_controller.set_enabled(true);
}
},
PullerMode::Pull => match mode {
PullerMode::Standby => {
// From [`PullerMode::Pull`] to [`PullerMode::Standby`]
self.puller.set_enabled(false);
self.puller_speed_controller.set_enabled(false);
}
PullerMode::Hold => {
// From [`PullerMode::Pull`] to [`PullerMode::Hold`]
self.puller_speed_controller.set_enabled(false);
}
PullerMode::Pull => {}
},
}
// Update the internal state
self.puller_mode = mode;
}
pub const fn stop_or_pull_spool_reset(&mut self, now: Instant) {
self.spool_automatic_action.progress = Length::ZERO;
self.spool_automatic_action.progress_last_check = now;
}
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
// Calculate distance pulled during this time interval
let meters_pulled_this_interval = Length::new::<meter>(
self.puller_speed_controller
.last_speed
.get::<meter_per_second>()
* dt,
);
// Update total meters pulled
self.spool_automatic_action.progress += meters_pulled_this_interval.abs();
self.spool_automatic_action.progress_last_check = now;
}
/// Implement Puller
/// called by `act`
pub fn sync_puller_speed(&mut self, t: Instant) {
let angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
let steps_per_second = self
.puller_speed_controller
.converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.puller.set_speed(steps_per_second);
}
/// Sync addon motor 3 speed based on puller angular velocity and ratio
/// called by `act`
pub fn sync_addon_motor_3_speed(&mut self, t: Instant) {
let puller_angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
self.addon_motor_3_controller
.sync_motor_speed(&mut self.addon_motor_3, puller_angular_velocity);
}
/// Sync addon motor 4 speed based on puller angular velocity and ratio
/// called by `act`
pub fn sync_addon_motor_4_speed(&mut self, t: Instant) {
let puller_angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
self.addon_motor_4_controller
.sync_motor_speed(&mut self.addon_motor_4, puller_angular_velocity);
}
/// Sync slave puller speed based on master puller speed and slave tension arm
/// called by `act`
pub fn sync_slave_puller_speed(&mut self, t: Instant) {
// Get master puller speed as reference
let master_speed = self.puller_speed_controller.get_target_speed();
// Calculate slave speed based on tension arm
let slave_velocity = self.slave_puller_speed_controller.update_speed(
t,
master_speed,
&self.slave_tension_arm,
);
// Apply direction
let directed_velocity = if self.slave_puller_speed_controller.get_forward() {
slave_velocity
} else {
-slave_velocity
};
// Convert to angular velocity then to steps
let angular_velocity = self
.slave_puller_speed_controller
.velocity_to_angular_velocity(directed_velocity);
let steps_per_second = self
.slave_puller_speed_controller
.converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.slave_puller.set_speed(steps_per_second);
}
/// Apply the mode changes to the slave puller
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::slave_puller_mode`]
fn set_slave_puller_mode(&mut self, mode: &GluetexMode) {
// Convert to `GluetexMode` to `PullerMode`
let mode: PullerMode = mode.clone().into();
// Transition matrix
match self.slave_puller_mode {
PullerMode::Standby => match mode {
PullerMode::Standby => {}
PullerMode::Hold => {
// From [`PullerMode::Standby`] to [`PullerMode::Hold`]
self.slave_puller.set_enabled(true);
}
PullerMode::Pull => {
// From [`PullerMode::Standby`] to [`PullerMode::Pull`]
self.slave_puller.set_enabled(true);
self.slave_puller_speed_controller.set_enabled(true);
}
},
PullerMode::Hold => match mode {
PullerMode::Standby => {
// From [`PullerMode::Hold`] to [`PullerMode::Standby`]
self.slave_puller.set_enabled(false);
}
PullerMode::Hold => {}
PullerMode::Pull => {
// From [`PullerMode::Hold`] to [`PullerMode::Pull`]
self.slave_puller_speed_controller.set_enabled(true);
}
},
PullerMode::Pull => match mode {
PullerMode::Standby => {
// From [`PullerMode::Pull`] to [`PullerMode::Standby`]
self.slave_puller.set_enabled(false);
self.slave_puller_speed_controller.set_enabled(false);
}
PullerMode::Hold => {
// From [`PullerMode::Pull`] to [`PullerMode::Hold`]
self.slave_puller_speed_controller.set_enabled(false);
}
PullerMode::Pull => {}
},
}
// Update the internal state
self.slave_puller_mode = mode;
}
/// Check all tension arm positions and trigger emergency stop if any are out of range
pub fn check_tension_arm_monitor(&mut self) {
// Only check if monitoring is enabled
if !self.tension_arm_monitor_config.enabled {
// Clear triggered flag if monitoring is disabled
if self.tension_arm_monitor_triggered {
self.tension_arm_monitor_triggered = false;
self.emit_state();
}
return;
}
// Check all three tension arms
let main_angle = self.tension_arm.get_angle();
let slave_angle = self.slave_tension_arm.get_angle();
let addon_angle = self.addon_tension_arm.get_angle();
let min_angle = self.tension_arm_monitor_config.min_angle;
let max_angle = self.tension_arm_monitor_config.max_angle;
// Check if any tension arm is out of range
let out_of_range = main_angle < min_angle
|| main_angle > max_angle
|| slave_angle < min_angle
|| slave_angle > max_angle
|| addon_angle < min_angle
|| addon_angle > max_angle;
// If out of range, trigger emergency stop
if out_of_range && !self.tension_arm_monitor_triggered {
self.tension_arm_monitor_triggered = true;
self.emergency_stop();
self.emit_state();
tracing::warn!(
"Tension arm monitor triggered! Main: {:.1}°, Slave: {:.1}°, Addon: {:.1}° (limits: {:.1}°-{:.1}°)",
main_angle.get::<degree>(),
slave_angle.get::<degree>(),
addon_angle.get::<degree>(),
min_angle.get::<degree>(),
max_angle.get::<degree>()
);
} else if !out_of_range && self.tension_arm_monitor_triggered {
// Clear triggered flag if back in range
self.tension_arm_monitor_triggered = false;
self.emit_state();
tracing::info!("Tension arm monitor cleared - all arms back in range");
}
}
/// Emergency stop: stops all motors, heating, and sets machine to standby
fn emergency_stop(&mut self) {
// Stop all motors by setting mode to standby
self.set_mode(&GluetexMode::Standby);
// Disable heating
self.heating_enabled = false;
// Ensure all motors are disabled
self.spool.set_enabled(false);
self.puller.set_enabled(false);
self.slave_puller.set_enabled(false);
self.traverse.set_enabled(false);
self.addon_motor_3.set_enabled(false);
self.addon_motor_4.set_enabled(false);
// Disable all controllers
self.spool_speed_controller.set_enabled(false);
self.puller_speed_controller.set_enabled(false);
self.slave_puller_speed_controller.set_enabled(false);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GluetexMode {
Standby,
Hold,
Pull,
Wind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpoolMode {
Standby,
Hold,
Wind,
}
impl From<GluetexMode> for SpoolMode {
fn from(mode: GluetexMode) -> Self {
match mode {
GluetexMode::Standby => Self::Standby,
GluetexMode::Hold => Self::Hold,
GluetexMode::Pull => Self::Hold,
GluetexMode::Wind => Self::Wind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TraverseMode {
Standby,
Hold,
Traverse,
}
impl From<GluetexMode> for TraverseMode {
fn from(mode: GluetexMode) -> Self {
match mode {
GluetexMode::Standby => Self::Standby,
GluetexMode::Hold => Self::Hold,
GluetexMode::Pull => Self::Hold,
GluetexMode::Wind => Self::Traverse,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PullerMode {
Standby,
Hold,
Pull,
}
impl From<GluetexMode> for PullerMode {
fn from(mode: GluetexMode) -> Self {
match mode {
GluetexMode::Standby => Self::Standby,
GluetexMode::Hold => Self::Hold,
GluetexMode::Pull => Self::Pull,
GluetexMode::Wind => Self::Pull,
}
}
}
impl std::fmt::Display for Gluetex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Gluetex")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_traverse_limits() {
// Test case 1: Valid limits with exactly 1.0mm difference (should pass)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(16.0);
assert!(Gluetex::validate_traverse_limits(inner, outer));
// Test case 2: Invalid limits with exactly 0.9mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.9);
assert!(!Gluetex::validate_traverse_limits(inner, outer));
// Test case 3: Invalid limits with less than 0.9mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.5);
assert!(!Gluetex::validate_traverse_limits(inner, outer));
// Test case 4: Invalid limits where inner equals outer (should fail)
let inner = Length::new::<millimeter>(20.0);
let outer = Length::new::<millimeter>(20.0);
assert!(!Gluetex::validate_traverse_limits(inner, outer));
// Test case 5: Invalid limits where inner is greater than outer (should fail)
let inner = Length::new::<millimeter>(25.0);
let outer = Length::new::<millimeter>(20.0);
assert!(!Gluetex::validate_traverse_limits(inner, outer));
// Test case 6: Valid limits with large difference (should pass)
let inner = Length::new::<millimeter>(10.0);
let outer = Length::new::<millimeter>(80.0);
assert!(Gluetex::validate_traverse_limits(inner, outer));
// Test case 7: Edge case - exactly 0.91mm difference (should pass)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.91);
assert!(Gluetex::validate_traverse_limits(inner, outer));
// Test case 8: Edge case - exactly 0.89mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.89);
assert!(!Gluetex::validate_traverse_limits(inner, outer));
}
}
+546
View File
@@ -0,0 +1,546 @@
mod gluetex_imports {
pub use super::super::api::GluetexNamespace;
pub use super::super::tension_arm::TensionArm;
pub use super::super::{Gluetex, GluetexMode, PullerMode};
pub use crate::gluetex::filament_tension::FilamentTensionCalculator;
pub use crate::gluetex::puller_speed_controller::PullerSpeedController;
pub use crate::gluetex::slave_puller_speed_controller::SlavePullerSpeedController;
pub use crate::gluetex::spool_speed_controller::SpoolSpeedController;
pub use crate::gluetex::traverse_controller::TraverseController;
pub use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
pub use anyhow::Error;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use control_core::converters::linear_step_converter::LinearStepConverter;
pub use ethercat_hal::coe::ConfigurableDevice;
pub use ethercat_hal::devices::ek1100::EK1100;
pub use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
pub use ethercat_hal::devices::el2008::{
EL2008, EL2008_IDENTITY_A, EL2008_IDENTITY_B, EL2008Port,
};
pub use ethercat_hal::devices::el3204::{
EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port,
};
pub use ethercat_hal::devices::el7031::coe::EL7031Configuration;
pub use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
pub use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
pub use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031_0030::{
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort,
EL7031_0030StepperPort,
};
pub use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
pub use ethercat_hal::devices::el7041_0052::{
EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port,
};
pub use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
pub use ethercat_hal::io::analog_input::AnalogInput;
pub use ethercat_hal::io::digital_input::DigitalInput;
pub use ethercat_hal::io::digital_output::DigitalOutput;
pub use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
pub use ethercat_hal::io::temperature_input::TemperatureInput;
pub use ethercat_hal::shared_config;
pub use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
pub use std::time::{Duration, Instant};
pub use units::ConstZero;
pub use units::angle::degree;
pub use units::f64::*;
pub use units::length::{centimeter, meter, millimeter};
pub use units::thermodynamic_temperature::degree_celsius;
pub use units::velocity::meter_per_minute;
}
pub use gluetex_imports::*;
use crate::get_ethercat_device;
use crate::gluetex::temperature_controller::TemperatureController;
impl MachineNewTrait for Gluetex {
fn new<'maindevice>(params: &MachineNewParams) -> 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 &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Gluetex::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
// using block_on because making this funciton async creates a lifetime issue
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0: Buscoupler EK1100
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
// Role 1: 2x Digital outputs EL2002
let el2002 = get_ethercat_device::<EL2002>(
hardware,
params,
1,
vec![EL2002_IDENTITY_A, EL2002_IDENTITY_B],
)
.await?
.0;
// Role 2: Stepper Spool EL7041-0052
let el7041 = {
let device = get_ethercat_device::<EL7041_0052>(
hardware,
params,
2,
vec![EL7041_0052_IDENTITY_A],
)
.await?;
let el7041_config = EL7041_0052Configuration {
stm_features: shared_config::el70x1::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 2800,
..Default::default()
},
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7041_config)
.await?;
device.0
};
// Role 3: Stepper Traverse EL7031
let el7031 = {
let device = get_ethercat_device::<EL7031>(
hardware,
params,
3,
vec![EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_config = EL7031Configuration {
stm_features: shared_config::el70x1::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
..Default::default()
},
pdo_assignment: EL7031PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_config)
.await?;
device.0
};
// Role 4: Stepper Puller EL7031-0030
let el7031_0030 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
4,
vec![EL7031_0030_IDENTITY_A],
)
.await?;
let el7031_0030_config = EL7031_0030Configuration {
stm_features: el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 2700,
..Default::default()
},
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_0030_config)
.await?;
device.0
};
// Role 5: Temperature Sensors 1-4 EL3204
let el3204_1 = get_ethercat_device::<EL3204>(
hardware,
params,
5,
vec![EL3204_IDENTITY_A, EL3204_IDENTITY_B],
)
.await?
.0;
// Role 6: Temperature Sensors 5-6 (and 2 spare ports) EL3204
let el3204_2 = get_ethercat_device::<EL3204>(
hardware,
params,
6,
vec![EL3204_IDENTITY_A, EL3204_IDENTITY_B],
)
.await?
.0;
// Temperature inputs
let temperature_1 = TemperatureInput::new(el3204_1.clone(), EL3204Port::T1);
let temperature_2 = TemperatureInput::new(el3204_1.clone(), EL3204Port::T2);
let temperature_3 = TemperatureInput::new(el3204_1.clone(), EL3204Port::T3);
let temperature_4 = TemperatureInput::new(el3204_1, EL3204Port::T4);
let temperature_5 = TemperatureInput::new(el3204_2.clone(), EL3204Port::T1);
let temperature_6 = TemperatureInput::new(el3204_2, EL3204Port::T2);
// Role 7: Digital outputs for heater SSRs EL2008
let el2008 = get_ethercat_device::<EL2008>(
hardware,
params,
7,
vec![EL2008_IDENTITY_A, EL2008_IDENTITY_B],
)
.await?
.0;
// Role 8: Addon Motor 3 EL7031-0030
let el7031_addon3 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
8,
vec![EL7031_0030_IDENTITY_A, EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_0030_config = EL7031_0030Configuration {
stm_features: el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
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
.0
.write()
.await
.write_config(&device.1, &el7031_0030_config)
.await?;
device.0
};
// Role 9: Addon Motor 4 EL7031-0030
let el7031_addon4 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
9,
vec![EL7031_0030_IDENTITY_A, EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_0030_config = EL7031_0030Configuration {
stm_features: el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
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
.0
.write()
.await
.write_config(&device.1, &el7031_0030_config)
.await?;
device.0
};
// Role 10: Slave Puller EL7031-0030 (with analog input for tension arm)
let el7031_0030_slave = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
10,
vec![EL7031_0030_IDENTITY_A, EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_0030_config = EL7031_0030Configuration {
stm_features: el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 2700,
..Default::default()
},
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_0030_config)
.await?;
device.0
};
// Digital outputs for SSR control (24V to external SSRs for 60W heaters)
let heater_ssr_1 = DigitalOutput::new(el2008.clone(), EL2008Port::DO1);
let heater_ssr_2 = DigitalOutput::new(el2008.clone(), EL2008Port::DO2);
let heater_ssr_3 = DigitalOutput::new(el2008.clone(), EL2008Port::DO3);
let heater_ssr_4 = DigitalOutput::new(el2008.clone(), EL2008Port::DO4);
let heater_ssr_5 = DigitalOutput::new(el2008.clone(), EL2008Port::DO5);
let heater_ssr_6 = DigitalOutput::new(el2008, EL2008Port::DO6);
// Maximum temperature for all heating zones
let max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// PID-controlled temperature controllers (60W heaters via SSR)
// PID tuning: kp=0.10, ki=0.0, kd=0.015 (same as aquapath)
let temperature_controller_1 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_1,
heater_ssr_1,
super::Heating::default(),
Duration::from_millis(500),
60.0, // 60W heater
1.0,
);
let temperature_controller_2 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_2,
heater_ssr_2,
super::Heating::default(),
Duration::from_millis(500),
60.0,
1.0,
);
let temperature_controller_3 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_3,
heater_ssr_3,
super::Heating::default(),
Duration::from_millis(500),
60.0,
1.0,
);
let temperature_controller_4 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_4,
heater_ssr_4,
super::Heating::default(),
Duration::from_millis(500),
60.0,
1.0,
);
let temperature_controller_5 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_5,
heater_ssr_5,
super::Heating::default(),
Duration::from_millis(500),
60.0,
1.0,
);
let temperature_controller_6 = TemperatureController::new(
0.10,
0.0,
0.015,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
max_temperature,
temperature_6,
heater_ssr_6,
super::Heating::default(),
Duration::from_millis(500),
60.0,
1.0,
);
let mode = GluetexMode::Standby;
let machine_id = params
.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone();
let (sender, receiver) = smol::channel::unbounded();
let mut new =
Self {
main_sender: params.main_thread_channel.clone(),
max_connected_machines: 2,
api_receiver: receiver,
api_sender: sender,
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
temperature_controller_1,
temperature_controller_2,
temperature_controller_3,
temperature_controller_4,
temperature_controller_5,
temperature_controller_6,
heating_enabled: false,
puller: StepperVelocityEL70x1::new(
el7031_0030.clone(),
EL7031_0030StepperPort::STM1,
),
spool: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
addon_motor_3: StepperVelocityEL70x1::new(
el7031_addon3,
EL7031_0030StepperPort::STM1,
),
addon_motor_4: StepperVelocityEL70x1::new(
el7031_addon4.clone(),
EL7031_0030StepperPort::STM1,
),
addon_tension_arm: TensionArm::new(AnalogInput::new(
el7031_addon4,
EL7031_0030AnalogInputPort::AI1,
)),
addon_motor_3_controller:
super::addon_motor_controller::AddonMotorController::new(200),
addon_motor_4_controller:
super::addon_motor_controller::AddonMotorController::new(200),
tension_arm: TensionArm::new(AnalogInput::new(
el7031_0030,
EL7031_0030AnalogInputPort::AI1,
)),
laser: DigitalOutput::new(el2002, EL2002Port::DO1),
namespace: GluetexNamespace {
namespace: params.namespace.clone(),
},
mode: mode.clone(),
spool_step_converter: AngularStepConverter::new(200),
spool_speed_controller: SpoolSpeedController::new(),
last_measurement_emit: Instant::now(),
spool_mode: mode.clone().into(),
traverse_mode: mode.clone().into(),
puller_mode: mode.clone().into(),
puller_speed_controller: PullerSpeedController::new(
Velocity::new::<meter_per_minute>(1.0),
Length::new::<millimeter>(1.75),
LinearStepConverter::from_diameter(
200, // Assuming 200 steps per revolution for the puller stepper,
Length::new::<centimeter>(8.0), // 8cm diameter of the puller wheel
),
),
slave_puller: StepperVelocityEL70x1::new(
el7031_0030_slave.clone(),
EL7031_0030StepperPort::STM1,
),
slave_tension_arm: TensionArm::new(AnalogInput::new(
el7031_0030_slave,
EL7031_0030AnalogInputPort::AI1,
)),
slave_puller_speed_controller: SlavePullerSpeedController::new(
Angle::new::<degree>(20.0), // Min angle (low tension, high speed)
Angle::new::<degree>(90.0), // Max angle (high tension, low speed)
LinearStepConverter::from_diameter(
200, // 200 steps per revolution
Length::new::<centimeter>(8.0), // 8cm diameter
),
FilamentTensionCalculator::new(
Angle::new::<degree>(20.0), // Min angle for tension calc
Angle::new::<degree>(90.0), // Max angle for tension calc
),
),
slave_puller_mode: mode.clone().into(),
traverse_controller: TraverseController::new(
Length::new::<millimeter>(22.0), // Default inner limit
Length::new::<millimeter>(92.0), // Default outer limit
64, // Microsteps
),
emitted_default_state: false,
spool_automatic_action: super::SpoolAutomaticAction {
progress: Length::ZERO,
progress_last_check: Instant::now(),
target_length: Length::new::<meter>(250.0),
mode: super::api::SpoolAutomaticActionMode::NoAction,
},
machine_identification_unique: machine_id,
connected_machines: vec![],
tension_arm_monitor_config: super::TensionArmMonitorConfig::default(),
tension_arm_monitor_triggered: false,
};
// initalize events
new.emit_state();
Ok(new)
})
}
}
@@ -0,0 +1,155 @@
use std::time::Instant;
use control_core::{
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
converters::linear_step_converter::LinearStepConverter,
};
use serde::{Deserialize, Serialize};
use units::ConstZero;
use units::acceleration::meter_per_minute_per_second;
use units::f64::*;
use units::jerk::meter_per_minute_per_second_squared;
use units::velocity::meter_per_minute;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum GearRatio {
OneToOne,
OneToFive,
OneToTen,
}
impl GearRatio {
/// Get the speed multiplier for this gear ratio
pub fn multiplier(&self) -> f64 {
match self {
GearRatio::OneToOne => 1.0,
GearRatio::OneToFive => 5.0,
GearRatio::OneToTen => 10.0,
}
}
}
impl Default for GearRatio {
fn default() -> Self {
GearRatio::OneToOne
}
}
#[derive(Debug)]
pub struct PullerSpeedController {
enabled: bool,
pub target_speed: Velocity,
pub target_diameter: Length,
pub regulation_mode: PullerRegulationMode,
/// Forward rotation direction. If false, applies negative sign to speed
pub forward: bool,
/// Gear ratio for winding speed (1:5 or 1:10)
pub gear_ratio: GearRatio,
/// Linear acceleration controller to dampen speed change
acceleration_controller: LinearJerkSpeedController,
/// Converter for linear to angular transformations
pub converter: LinearStepConverter,
pub last_speed: Velocity,
}
impl PullerSpeedController {
pub fn new(
target_speed: Velocity,
target_diameter: Length,
converter: LinearStepConverter,
) -> Self {
let acceleration = Acceleration::new::<meter_per_minute_per_second>(5.0);
let jerk = Jerk::new::<meter_per_minute_per_second_squared>(10.0);
let speed = Velocity::new::<meter_per_minute>(50.0);
Self {
enabled: false,
target_speed,
target_diameter,
regulation_mode: PullerRegulationMode::Speed,
forward: true,
gear_ratio: GearRatio::default(),
acceleration_controller: LinearJerkSpeedController::new_simple(
Some(speed),
acceleration,
jerk,
),
converter,
last_speed: Velocity::ZERO,
}
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_target_speed(&mut self, target: Velocity) {
self.target_speed = target;
}
pub fn set_target_diameter(&mut self, target: Length) {
self.target_diameter = target;
}
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
self.regulation_mode = regulation;
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.gear_ratio = gear_ratio;
}
pub const fn get_gear_ratio(&self) -> GearRatio {
self.gear_ratio
}
fn update_speed(&mut self, t: Instant) -> Velocity {
let base_speed = match self.enabled {
true => match self.regulation_mode {
PullerRegulationMode::Speed => self.target_speed,
PullerRegulationMode::Diameter => unimplemented!(),
},
false => Velocity::ZERO,
};
// Apply gear ratio multiplier
let speed = base_speed * self.gear_ratio.multiplier();
let speed = if self.forward { speed } else { -speed };
let speed = self.acceleration_controller.update(speed, t);
self.last_speed = speed;
speed
}
pub fn speed_to_angular_velocity(&self, speed: Velocity) -> AngularVelocity {
// Use the converter to transform from linear velocity to angular velocity
self.converter.velocity_to_angular_velocity(speed)
}
pub fn angular_velocity_to_speed(&self, angular_speed: AngularVelocity) -> Velocity {
// Use the converter to transform from angular velocity to linear velocity
self.converter.angular_velocity_to_velocity(angular_speed)
}
pub fn calc_angular_velocity(&mut self, t: Instant) -> AngularVelocity {
let speed = self.update_speed(t);
self.speed_to_angular_velocity(speed)
}
pub fn get_target_speed(&self) -> Velocity {
self.target_speed
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum PullerRegulationMode {
#[default]
Speed,
Diameter,
}
@@ -0,0 +1,286 @@
use std::time::Instant;
use control_core::converters::linear_step_converter::LinearStepConverter;
use units::ConstZero;
use units::f64::*;
use super::{
clamp_revolution::Clamping, clamp_revolution::clamp_revolution_uom,
filament_tension::FilamentTensionCalculator, tension_arm::TensionArm,
};
/// Slave puller speed controller that follows the master puller
/// with speed adjustments based on tension arm angle feedback.
///
/// Uses min/max angles to define the detection zone for speed control.
/// When tension arm is at min angle (low tension), slave pulls faster.
/// When tension arm is at max angle (high tension), slave pulls slower.
/// Optional min/max speed factors provide overspeed protection.
#[derive(Debug)]
pub struct SlavePullerSpeedController {
/// Last commanded velocity
last_speed: Velocity,
/// Whether the controller is enabled
enabled: bool,
/// Forward rotation direction
forward: bool,
/// Minimum angle for detection zone (low tension, high speed)
min_angle: Angle,
/// Maximum angle for detection zone (high tension, low speed)
max_angle: Angle,
/// Optional minimum speed factor for overspeed protection (e.g., 0.5 = 50%)
min_speed_factor: Option<f64>,
/// Optional maximum speed factor for overspeed protection (e.g., 2.0 = 200%)
max_speed_factor: Option<f64>,
/// Tension calculator to convert arm angle to normalized tension
filament_calc: FilamentTensionCalculator,
/// Converter for linear velocity to motor steps
pub converter: LinearStepConverter,
}
impl SlavePullerSpeedController {
/// Create a new slave puller speed controller
///
/// # Arguments
/// * `min_angle` - Minimum angle for detection zone (low tension, high speed)
/// * `max_angle` - Maximum angle for detection zone (high tension, low speed)
/// * `converter` - Linear step converter for the motor
/// * `filament_calc` - Tension calculator for the tension arm
pub fn new(
min_angle: Angle,
max_angle: Angle,
converter: LinearStepConverter,
filament_calc: FilamentTensionCalculator,
) -> Self {
Self {
last_speed: Velocity::ZERO,
enabled: false,
forward: true,
min_angle,
max_angle,
min_speed_factor: None,
max_speed_factor: None,
filament_calc,
converter,
}
}
/// Calculate the raw target speed based on master speed and tension arm angle
///
/// # Arguments
/// * `master_speed` - Current speed of the master puller
/// * `tension_arm` - Reference to the tension arm for feedback
///
/// # Returns
/// Target velocity for the slave puller
fn speed_raw(&self, master_speed: Velocity, tension_arm: &TensionArm) -> Velocity {
// Get tension arm angle and clamp to valid range
let tension_arm_angle = tension_arm.get_angle();
let (clamped_angle, clamping_state) = clamp_revolution_uom(
tension_arm_angle,
self.max_angle, // Inverted: min angle = max tension = low speed
self.min_angle, // Inverted: max angle = min tension = high speed
);
// If at limits, return master speed (no adjustment)
let base_speed = match clamping_state {
Clamping::Min => master_speed, // At max angle (high tension) - return base speed
Clamping::Max => master_speed, // At min angle (low tension) - return base speed
_ => {
// Calculate normalized tension (0.0 = low tension, 1.0 = high tension)
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
// Invert tension for speed calculation: low tension = high speed, high tension = low speed
// At min_angle (low tension): tension = 0.0, inverted = 1.0 -> slave runs faster
// At max_angle (high tension): tension = 1.0, inverted = 0.0 -> slave runs slower
let speed_factor = 1.0 - filament_tension;
// Interpolate between 50% and 150% of master speed based on tension
// This gives a natural range that can be further limited by optional factors
let factor = 0.5 + speed_factor; // Maps 0.0-1.0 to 0.5-1.5
master_speed * factor
}
};
// Apply optional speed factor limits for overspeed protection
let limited_speed = if let Some(min_factor) = self.min_speed_factor {
base_speed.max(master_speed * min_factor)
} else {
base_speed
};
let limited_speed = if let Some(max_factor) = self.max_speed_factor {
limited_speed.min(master_speed * max_factor)
} else {
limited_speed
};
limited_speed.abs()
}
/// Main update function called every control cycle
///
/// # Arguments
/// * `t` - Current timestamp
/// * `master_speed` - Speed of the master puller
/// * `tension_arm` - Reference to the slave tension arm
///
/// # Returns
/// Commanded velocity for the slave puller motor
pub fn update_speed(
&mut self,
_t: Instant,
master_speed: Velocity,
tension_arm: &TensionArm,
) -> Velocity {
// Calculate raw target speed
let target_speed = self.speed_raw(master_speed, tension_arm);
// Apply enable/disable logic
let final_speed = if self.enabled {
target_speed
} else {
Velocity::ZERO
};
// Store and return final speed
self.last_speed = final_speed;
final_speed
}
// Getters and setters
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
pub const fn get_forward(&self) -> bool {
self.forward
}
pub const fn set_min_angle(&mut self, angle: Angle) {
self.min_angle = angle;
}
pub const fn get_min_angle(&self) -> Angle {
self.min_angle
}
pub const fn set_max_angle(&mut self, angle: Angle) {
self.max_angle = angle;
}
pub const fn get_max_angle(&self) -> Angle {
self.max_angle
}
pub fn set_min_speed_factor(&mut self, factor: Option<f64>) {
self.min_speed_factor = factor.map(|f| f.clamp(0.1, 5.0));
}
pub const fn get_min_speed_factor(&self) -> Option<f64> {
self.min_speed_factor
}
pub fn set_max_speed_factor(&mut self, factor: Option<f64>) {
self.max_speed_factor = factor.map(|f| f.clamp(0.1, 5.0));
}
pub const fn get_max_speed_factor(&self) -> Option<f64> {
self.max_speed_factor
}
pub const fn get_last_speed(&self) -> Velocity {
self.last_speed
}
pub fn reset(&mut self) {
self.last_speed = Velocity::ZERO;
}
/// Convert velocity to angular velocity for step conversion
pub fn velocity_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
self.converter.velocity_to_angular_velocity(velocity)
}
}
#[cfg(test)]
mod tests {
use super::*;
use units::angle::degree;
use units::length::millimeter;
#[test]
fn test_slave_puller_angles() {
let converter =
LinearStepConverter::from_circumference(200, Length::new::<millimeter>(50.0));
let filament_calc =
FilamentTensionCalculator::new(Angle::new::<degree>(20.0), Angle::new::<degree>(90.0));
let controller = SlavePullerSpeedController::new(
Angle::new::<degree>(20.0),
Angle::new::<degree>(90.0),
converter,
filament_calc,
);
assert_eq!(controller.get_min_angle().get::<degree>(), 20.0);
assert_eq!(controller.get_max_angle().get::<degree>(), 90.0);
}
#[test]
fn test_slave_puller_enabled_state() {
let converter =
LinearStepConverter::from_circumference(200, Length::new::<millimeter>(50.0));
let filament_calc =
FilamentTensionCalculator::new(Angle::new::<degree>(20.0), Angle::new::<degree>(90.0));
let mut controller = SlavePullerSpeedController::new(
Angle::new::<degree>(20.0),
Angle::new::<degree>(90.0),
converter,
filament_calc,
);
assert!(!controller.is_enabled());
controller.set_enabled(true);
assert!(controller.is_enabled());
controller.set_enabled(false);
assert!(!controller.is_enabled());
}
#[test]
fn test_optional_speed_factors() {
let converter =
LinearStepConverter::from_circumference(200, Length::new::<millimeter>(50.0));
let filament_calc =
FilamentTensionCalculator::new(Angle::new::<degree>(20.0), Angle::new::<degree>(90.0));
let mut controller = SlavePullerSpeedController::new(
Angle::new::<degree>(20.0),
Angle::new::<degree>(90.0),
converter,
filament_calc,
);
assert_eq!(controller.get_min_speed_factor(), None);
assert_eq!(controller.get_max_speed_factor(), None);
controller.set_min_speed_factor(Some(0.7));
controller.set_max_speed_factor(Some(1.5));
assert_eq!(controller.get_min_speed_factor(), Some(0.7));
assert_eq!(controller.get_max_speed_factor(), Some(1.5));
}
}
@@ -0,0 +1,194 @@
use crate::gluetex::{
adaptive_spool_speed_controller::AdaptiveSpoolSpeedController,
minmax_spool_speed_controller::MinMaxSpoolSpeedController,
puller_speed_controller::PullerSpeedController,
};
use control_core::controllers::second_degree_motion::acceleration_position_controller::MotionControllerError;
use super::tension_arm::TensionArm;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use units::f64::*;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SpoolSpeedControllerType {
#[default]
Adaptive,
MinMax,
}
#[derive(Debug)]
pub struct SpoolSpeedController {
adaptive_controller: AdaptiveSpoolSpeedController,
minmax_controller: MinMaxSpoolSpeedController,
r#type: SpoolSpeedControllerType,
forward: bool,
}
impl Default for SpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl SpoolSpeedController {
pub fn new() -> Self {
Self {
adaptive_controller: AdaptiveSpoolSpeedController::new(),
minmax_controller: MinMaxSpoolSpeedController::new(),
r#type: SpoolSpeedControllerType::Adaptive,
forward: true,
}
}
pub fn get_speed(&self) -> AngularVelocity {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
}
}
pub fn set_speed(&mut self, speed: AngularVelocity) {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.set_speed(speed),
SpoolSpeedControllerType::MinMax => self.minmax_controller.set_speed(speed),
}
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.adaptive_controller.set_enabled(enabled);
self.minmax_controller.set_enabled(enabled);
}
pub const fn is_enabled(&self) -> bool {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.is_enabled(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.is_enabled(),
}
}
pub fn set_type(&mut self, r#type: SpoolSpeedControllerType) {
// If we're switching to a different type, copy the current speed to the target controller
if std::mem::discriminant(&self.r#type) != std::mem::discriminant(&r#type) {
// Get the current speed from the active controller
let current_speed = match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
};
// Set the speed in the target controller and reset it for smooth transition
match r#type {
SpoolSpeedControllerType::Adaptive => {
self.adaptive_controller.set_speed(current_speed);
self.adaptive_controller.reset();
self.adaptive_controller.set_speed(current_speed); // Set again after reset to maintain speed
}
SpoolSpeedControllerType::MinMax => {
self.minmax_controller.set_speed(current_speed);
self.minmax_controller.reset();
self.minmax_controller.set_speed(current_speed); // Set again after reset to maintain speed
}
}
}
self.r#type = r#type;
}
pub const fn get_type(&self) -> &SpoolSpeedControllerType {
&self.r#type
}
pub fn set_minmax_min_speed(
&mut self,
min_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.minmax_controller.set_min_speed(min_speed)
}
pub fn set_minmax_max_speed(
&mut self,
max_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.minmax_controller.set_max_speed(max_speed)
}
pub fn get_minmax_min_speed(&self) -> AngularVelocity {
self.minmax_controller.get_min_speed()
}
pub fn get_minmax_max_speed(&self) -> AngularVelocity {
self.minmax_controller.get_max_speed()
}
pub fn update_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
match self.r#type {
SpoolSpeedControllerType::Adaptive => {
self.adaptive_controller
.update_speed(t, tension_arm, puller_speed_controller)
}
SpoolSpeedControllerType::MinMax => self.minmax_controller.update_speed(t, tension_arm),
}
}
// Adaptive controller parameter getters and setters
pub const fn get_adaptive_tension_target(&self) -> f64 {
self.adaptive_controller.get_tension_target()
}
pub const fn set_adaptive_tension_target(&mut self, tension_target: f64) {
self.adaptive_controller.set_tension_target(tension_target);
}
pub const fn get_adaptive_radius_learning_rate(&self) -> f64 {
self.adaptive_controller.get_radius_learning_rate()
}
pub const fn set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.adaptive_controller
.set_radius_learning_rate(radius_learning_rate);
}
pub const fn get_adaptive_max_speed_multiplier(&self) -> f64 {
self.adaptive_controller.get_max_speed_multiplier()
}
pub const fn set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.adaptive_controller
.set_max_speed_multiplier(max_speed_multiplier);
}
pub const fn get_adaptive_acceleration_factor(&self) -> f64 {
self.adaptive_controller.get_acceleration_factor()
}
pub const fn set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.adaptive_controller
.set_acceleration_factor(acceleration_factor);
}
pub const fn get_adaptive_deacceleration_urgency_multiplier(&self) -> f64 {
self.adaptive_controller
.get_deacceleration_urgency_multiplier()
}
pub const fn set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.adaptive_controller
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
}
pub const fn get_forward(&self) -> bool {
self.forward
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
}
@@ -0,0 +1,129 @@
use super::Heating;
use control_core::controllers::pid::PidController;
use ethercat_hal::io::{digital_output::DigitalOutput, temperature_input::TemperatureInput};
use std::time::{Duration, Instant};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
#[derive(Debug)]
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
ssr_output: DigitalOutput,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
heating_allowed: bool,
pwm_period: Duration,
max_temperature: ThermodynamicTemperature,
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
}
impl TemperatureController {
pub fn disable(&mut self) {
self.ssr_output.set(false);
self.heating.heating = false;
self.disallow_heating();
}
#[allow(clippy::too_many_arguments)]
pub fn new(
kp: f64,
ki: f64,
kd: f64,
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
ssr_output: DigitalOutput,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
max_clamp: f64,
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
target_temp,
window_start: Instant::now(),
temperature_sensor,
ssr_output,
heating,
heating_allowed: false,
pwm_period: pwm_duration,
max_temperature,
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
}
}
pub fn set_target_temperature(&mut self, temp: ThermodynamicTemperature) {
self.heating.target_temperature = temp;
}
pub fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
pub fn allow_heating(&mut self) {
self.heating_allowed = true;
}
pub fn get_heating_element_wattage(&mut self) -> f64 {
self.temperature_pid_output * self.heating_element_wattage
}
pub fn get_temperature(
&self,
) -> Result<f64, ethercat_hal::io::temperature_input::TemperatureInputError> {
self.temperature_sensor.get_temperature()
}
pub fn update(&mut self, now: Instant) {
self.temperature_pid_output = 0.0;
let temperature = self.temperature_sensor.get_temperature();
let temperature_celsius = ThermodynamicTemperature::new::<degree_celsius>(
temperature.as_ref().unwrap_or(&0.0).to_owned(),
);
self.heating.wiring_error = temperature.is_err();
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the SSR and return
self.ssr_output.set(false);
self.heating.heating = false;
return;
}
if self.heating_allowed {
let error: f64 = self.heating.target_temperature.get::<degree_celsius>()
- self.heating.temperature.get::<degree_celsius>();
let control = self.pid.update(error, now); // PID output
// Clamp PID output to 0.0 – 1.0 (as duty cycle)
let duty_cycle = control.clamp(0.0, self.max_clamp);
self.temperature_pid_output = duty_cycle;
// PWM logic
let elapsed = now.duration_since(self.window_start);
if elapsed >= self.pwm_period {
self.window_start = now;
}
let on_time = self.pwm_period.mul_f64(duty_cycle);
if elapsed < on_time {
self.ssr_output.set(true);
self.heating.heating = true;
} else {
self.ssr_output.set(false);
self.heating.heating = false;
}
} else {
self.ssr_output.set(false);
self.heating.heating = false;
}
}
}
+197
View File
@@ -0,0 +1,197 @@
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use units::angle::revolution;
use units::electric_potential::volt;
use units::f64::*;
#[derive(Debug)]
pub struct TensionArm {
pub analog_input: AnalogInput,
pub zero: Angle,
/// was zeroed at least once
pub zeroed: bool,
}
impl TensionArm {
pub fn new(analog_input: AnalogInput) -> Self {
Self {
analog_input,
zero: Angle::new::<revolution>(0.0),
zeroed: false,
}
}
fn volts_to_angle(&self, volts: f64) -> Angle {
// 0V = 0deg 5V = 3600deg
// always wrap into 0..1 revolution
Angle::new::<revolution>(volts / 5.0) % Angle::new::<revolution>(1.0)
}
fn get_volts(&self) -> f64 {
// get the normalized value from the analog input
let value = self.analog_input.get_physical();
match value {
AnalogInputValue::Potential(v) => v.get::<volt>(),
_ => panic!("Expected a potential value"),
}
}
fn raw_angle(&self) -> Angle {
// get volts
let volts = self.get_volts();
// 0V = 0deg 5V = 3600deg
self.volts_to_angle(volts)
}
pub fn get_angle(&self) -> Angle {
// revolution is maping -1/1 to 0/1
let raw = self.raw_angle();
// Handle the wraparound case
if raw < self.zero {
// We've wrapped around, so add a full revolution
(raw + Angle::new::<revolution>(1.0)) - self.zero
} else {
// Normal case
raw - self.zero
}
}
pub fn zero(&mut self) {
self.zero = self.raw_angle();
self.zeroed = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use core::f64;
use ethercat_hal::io::{
analog_input::{AnalogInputInput, physical::AnalogInputRange},
analog_input_dummy::AnalogInputDummy,
};
use std::i16;
#[test]
fn volts_to_angle() {
let tension_arm = TensionArm::new(
AnalogInputDummy::new(AnalogInputRange::Potential {
min: ElectricPotential::new::<volt>(0.0),
max: ElectricPotential::new::<volt>(10.0),
min_raw: 0,
max_raw: i16::MAX,
})
.analog_input(),
);
// 0V = 0deg
assert_relative_eq!(
tension_arm.volts_to_angle(0.0).get::<revolution>(),
0.0,
epsilon = f64::EPSILON
);
// 5V = 1 revolution
assert_relative_eq!(
tension_arm.volts_to_angle(5.0).get::<revolution>(),
0.0,
epsilon = f64::EPSILON
);
// 10V = 2 revolution
assert_relative_eq!(
tension_arm.volts_to_angle(10.0).get::<revolution>(),
0.0,
epsilon = f64::EPSILON
);
}
#[test]
fn get_volts() {
let mut analog_input_dummy = AnalogInputDummy::new(AnalogInputRange::Potential {
min: ElectricPotential::new::<volt>(0.0),
max: ElectricPotential::new::<volt>(10.0),
min_raw: 0,
max_raw: i16::MAX,
});
let analog_input = analog_input_dummy.analog_input();
let tension_arm = TensionArm::new(analog_input);
// 0.0 normalized = 0V
let volts = tension_arm.get_volts();
assert_relative_eq!(volts, 0.0, epsilon = f64::EPSILON);
// 0.5 normalized = 5V
analog_input_dummy.set_input(AnalogInputInput {
// 5V of 10V in positive range
normalized: (5.0 / 10.0),
wiring_error: false,
});
let physical = tension_arm.analog_input.get_physical();
match physical {
AnalogInputValue::Potential(v) => {
assert_relative_eq!(v.get::<volt>(), 5.0, epsilon = f64::EPSILON);
}
_ => panic!("Expected a potential value"),
}
}
#[test]
fn test_tension_arm() {
let mut analog_input_dummy = AnalogInputDummy::new(AnalogInputRange::Potential {
min: ElectricPotential::new::<volt>(0.0),
max: ElectricPotential::new::<volt>(10.0),
min_raw: 0,
max_raw: i16::MAX,
});
let analog_input = analog_input_dummy.analog_input();
let tension_arm = TensionArm::new(analog_input);
// 0V = 0.25 = 0 revolution
let angle = tension_arm.get_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.0, epsilon = f64::EPSILON);
// 1.25V = 0.25 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (1.25 / 10.0),
wiring_error: false,
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = f64::EPSILON);
// 2.5V = 0.5 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (2.5 / 10.0),
wiring_error: false,
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = f64::EPSILON);
// 3.75V = 0.75 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (3.75 / 10.0),
wiring_error: false,
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.75, epsilon = f64::EPSILON);
// 5V = 1 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (5.0 / 10.0),
wiring_error: false,
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.0, epsilon = f64::EPSILON);
// 6.25V = 0.25 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (6.25 / 10.0),
wiring_error: false,
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = f64::EPSILON);
}
}
+517
View File
@@ -0,0 +1,517 @@
use std::time::Instant;
use control_core::converters::linear_step_converter::LinearStepConverter;
use ethercat_hal::io::{
digital_input::DigitalInput, stepper_velocity_el70x1::StepperVelocityEL70x1,
};
use units::ConstZero;
use units::angular_velocity::revolution_per_second;
use units::f64::{AngularVelocity, Length, Velocity};
use units::length::millimeter;
use units::velocity::millimeter_per_second;
#[derive(Debug)]
pub struct TraverseController {
enabled: bool,
position: Length,
limit_inner: Length,
limit_outer: Length,
step_size: Length,
padding: Length,
state: State,
fullstep_converter: LinearStepConverter,
microstep_converter: LinearStepConverter,
// A sticky flag if the [`State`] changed (not the sub states)
// Needed to send state updates to the UI
did_change_state: bool,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum State {
/// Initial state
NotHomed,
/// Doing nothing
/// Already homed
Idle,
/// Going to inner limit
///
/// After reaching the inner limit, the state will change to [`State::Idle`]
GoingIn,
/// Going to outer limit
///
/// After reaching the outer limit, the state will change to [`State::Idle`]
GoingOut,
/// Homing is in progress
///
/// After homing is done, the state will change to [`State::Idle`]
Homing(HomingState),
/// Move between inner and outer limits
Traversing(TraversingState),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TraversingState {
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
GoingOut,
/// Like [`State::GoingIn`] but
/// - will go into [`State::GoingOut`] after reaching the inner limit
/// - speed is synced to spool speed
TraversingIn,
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
/// - speed is synced to spool speed
TraversingOut,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum HomingState {
/// In this state the traverse is not moving but checks if the endstop si triggered
/// If the endstop is triggered we go into [`HomingState::EscapeEndstop`]
/// If the endstop is not triggered we go into [`HomingState::FindEndstop`]
Initialize,
/// In this state the traverse is moving out away from the endstop until it's not triggered anymore
/// The it goes into [`HomingState::FindEnstopFineDistancing`]
EscapeEndstop,
/// Moving out away from the endstop
/// Then Transition into [`HomingState::FindEndtopFine`]
FindEndstopFineDistancing,
/// In this state the traverse is fast until it reaches the endstop
FindEndstopCoarse,
/// In this state the traverse is moving slowly until it reaches the endstop
FindEndtopFine,
/// In this state we check if th current position is actually 0.0, if not we redo the homing routine
Validate(Instant),
}
impl TraverseController {
pub fn new(limit_inner: Length, limit_outer: Length, microsteps: u8) -> Self {
Self {
enabled: false,
position: Length::ZERO,
limit_inner,
limit_outer,
step_size: Length::new::<millimeter>(1.75), // Default step size
padding: Length::new::<millimeter>(0.88), // Default padding
state: State::NotHomed,
did_change_state: false,
fullstep_converter: LinearStepConverter::from_circumference(
200,
Length::new::<millimeter>(35.0),
),
microstep_converter: LinearStepConverter::from_circumference(
200 * microsteps as i16,
Length::new::<millimeter>(35.0),
),
}
}
}
// Getter & Setter
impl TraverseController {
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_limit_inner(&mut self, limit: Length) {
self.limit_inner = limit;
}
pub fn set_limit_outer(&mut self, limit: Length) {
self.limit_outer = limit;
}
pub fn set_step_size(&mut self, step_size: Length) {
self.step_size = step_size;
}
pub fn set_padding(&mut self, padding: Length) {
self.padding = padding;
}
pub fn get_limit_inner(&self) -> Length {
self.limit_inner
}
pub fn get_limit_outer(&self) -> Length {
self.limit_outer
}
pub fn get_step_size(&self) -> Length {
self.step_size
}
pub fn get_padding(&self) -> Length {
self.padding
}
pub fn get_current_position(&self) -> Option<Length> {
match self.is_homed() {
true => Some(self.position),
false => None,
}
}
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub const fn did_change_state(&mut self) -> bool {
let did_change = self.did_change_state;
// Reset the flag
self.did_change_state = false;
did_change
}
}
// State management
impl TraverseController {
pub const fn goto_limit_inner(&mut self) {
self.state = State::GoingIn;
}
pub const fn goto_limit_outer(&mut self) {
self.state = State::GoingOut;
}
pub const fn goto_home(&mut self) {
self.state = State::Homing(HomingState::Initialize);
}
pub const fn start_traversing(&mut self) {
self.state = State::Traversing(TraversingState::GoingOut);
}
pub const fn is_homed(&self) -> bool {
// if not [`State::NotHomed`], then it is homed
!matches!(self.state, State::NotHomed)
}
pub const fn is_going_in(&self) -> bool {
// [`State::GoingIn`]
matches!(self.state, State::GoingIn)
}
pub const fn is_going_out(&self) -> bool {
// [`State::GoingOut`]
matches!(self.state, State::GoingOut)
}
pub const fn is_going_home(&self) -> bool {
// [`State::Homing`]
matches!(self.state, State::Homing(_))
}
pub const fn is_traversing(&self) -> bool {
// [`State::Traversing`]
matches!(self.state, State::Traversing(_))
}
}
impl TraverseController {
// If at inner limit within a tolerance
fn is_at_position(&self, target_position: Length, tolerance: Length) -> bool {
let upper_tolerance = target_position + tolerance.abs();
let lower_tolerance = target_position - tolerance.abs();
self.position >= lower_tolerance && self.position <= upper_tolerance
}
/// Calculate distance to position
fn distance_to_position(&self, target_position: Length) -> Length {
if self.position > target_position {
self.position - target_position
} else if self.position < target_position {
target_position - self.position
} else {
Length::ZERO
}
}
// Changes the direction of the speed based on the current position and target position
fn speed_to_position(&self, target_position: Length, absolute_speed: Velocity) -> Velocity {
// If we are over the target position we need to move negative
if self.position > target_position {
-absolute_speed.abs()
} else if self.position < target_position {
absolute_speed.abs()
} else {
Velocity::ZERO
}
}
/// Gets the current traverse position as a [`Length`].
pub fn sync_position(&mut self, traverse: &StepperVelocityEL70x1) {
let steps = traverse.get_position();
self.position = self.microstep_converter.steps_to_distance(steps as f64);
}
/// Update the [`did_change_state`] flag
/// Only considers the major state not the sub states
const fn update_did_change_state(&mut self, old_state: &State) -> bool {
match self.state {
State::NotHomed => !matches!(old_state, State::NotHomed),
State::Idle => !matches!(old_state, State::Idle),
State::GoingIn => !matches!(old_state, State::GoingIn),
State::GoingOut => !matches!(old_state, State::GoingOut),
State::Homing(_) => !matches!(old_state, State::Homing(_)),
State::Traversing(_) => !matches!(old_state, State::Traversing(_)),
}
}
/// Calculates a desired speed based on the current state and the end stop status.
///
/// Positive speed moved out, negative speed moves in.
fn get_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
spool_speed: AngularVelocity,
) -> Velocity {
// Don't move if not enabled or in a state that doesn't result in movement
if !self.enabled {
return Velocity::ZERO;
}
self.sync_position(traverse);
// save state before
let old_state = self.state.clone();
// Automatic Transitions
match &self.state {
State::NotHomed => {}
State::Idle => {}
State::GoingIn => {
// If inner limit is reached
if self.is_at_position(self.limit_inner, Length::new::<millimeter>(0.01)) {
// Put Into Idle
self.state = State::Idle;
}
}
State::GoingOut => {
// If outer limit is reached
if self.is_at_position(self.limit_outer, Length::new::<millimeter>(0.01)) {
// Put Into Idle
self.state = State::Idle;
}
}
State::Homing(homing_state) => match homing_state {
HomingState::Initialize => {
// If endstop is triggered, escape the endstop
if traverse_end_stop.get_value().unwrap_or(false) {
self.state = State::Homing(HomingState::EscapeEndstop);
} else {
// If endstop is not triggered, move to the endstop
self.state = State::Homing(HomingState::FindEndstopCoarse);
}
}
HomingState::EscapeEndstop => {
// Move out until endstop is not triggered anymore
if !traverse_end_stop.get_value().unwrap_or(false) {
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
}
}
HomingState::FindEndstopFineDistancing => {
// Move out until endstop is not triggered anymore
if !traverse_end_stop.get_value().unwrap_or(false) {
// Find endstop fine
self.state = State::Homing(HomingState::FindEndtopFine);
}
}
HomingState::FindEndtopFine => {
// If endstop is reached change to idle
if traverse_end_stop.get_value().unwrap_or(false) {
// Set poition of traverse to 0
traverse.set_position(0);
// Put Into Idle
self.state = State::Homing(HomingState::Validate(Instant::now()));
}
}
HomingState::FindEndstopCoarse => {
// Move to endstop
if traverse_end_stop.get_value().unwrap_or(false) {
// Move awaiy from endstop
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
}
}
HomingState::Validate(instant) => {
// If 100ms have passed check if position is actually 0.0
if instant.elapsed().as_millis() > 100 {
if self.is_at_position(Length::ZERO, Length::new::<millimeter>(0.01)) {
// If position is 0.0, put into idle
self.state = State::Idle;
} else {
// If position is not 0.0, redo homing
self.state = State::Homing(HomingState::Initialize);
}
}
}
},
// If state changed we
State::Traversing(traversing_state) => match traversing_state {
TraversingState::GoingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingIn);
}
}
TraversingState::TraversingIn => {
// If inner limit is reached
if self.position <= self.limit_inner + self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingOut);
}
}
TraversingState::TraversingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingIn);
}
}
},
}
// Set the [`did_change_state`] flag
if !self.did_change_state {
self.did_change_state = self.update_did_change_state(&old_state);
}
// Speed
match &self.state {
State::NotHomed => Velocity::ZERO, // Not homed, no movement
State::Idle => Velocity::ZERO, // No movement in idle state
State::GoingIn => {
// Move in at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_inner,
match self.distance_to_position(self.limit_inner).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
}
State::GoingOut => {
// Move out at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_outer,
match self.distance_to_position(self.limit_outer).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
}
State::Homing(homing_state) => match homing_state {
HomingState::Initialize => Velocity::ZERO,
HomingState::EscapeEndstop => {
// Move out at a speed of 10 mm/s
Velocity::new::<millimeter_per_second>(10.0)
}
HomingState::FindEndstopFineDistancing => {
// Move out at a speed of 2 mm/s
Velocity::new::<millimeter_per_second>(2.0)
}
HomingState::FindEndstopCoarse => {
// Move in at a speed of -100 mm/s
Velocity::new::<millimeter_per_second>(-100.0)
}
HomingState::FindEndtopFine => {
// move into the endstop at 2 mm/s
Velocity::new::<millimeter_per_second>(-2.0)
}
HomingState::Validate(_) => {
// We stand still until the validation cooldown has passed
Velocity::ZERO
}
}, // Homing speed
State::Traversing(traversing_state) => match traversing_state {
TraversingState::GoingOut => {
// Move out at a speed of 100 mm/s
self.speed_to_position(
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
Velocity::new::<millimeter_per_second>(100.0),
)
}
TraversingState::TraversingIn => self.speed_to_position(
self.limit_inner + self.padding - Length::new::<millimeter>(0.01),
Self::calculate_traverse_speed(spool_speed, self.step_size),
),
TraversingState::TraversingOut => self.speed_to_position(
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
Self::calculate_traverse_speed(spool_speed, self.step_size),
),
},
}
}
/// Calculate the traverse speed
///
/// The traverse speed is the linear speed at which the winding mechanism moves along the spool.
/// It's directly proportional to how fast the spool rotates and how far the traverse moves per rotation.
///
/// - Traverse Distance per Revolution [mm] = Step Size [mm]
/// - Traverse Speed [mm/s] = Spool Speed [rev/s or rad/s] * Step Size [mm]
///
/// Note: While the traverse range (from outer limit minus padding to inner limit plus padding)
/// determines the total area to be covered, the traverse speed itself depends only on
/// the step size and spool rotation speed.
pub fn calculate_traverse_speed(spool_speed: AngularVelocity, step_size: Length) -> Velocity {
// Calculate the traverse speed directly from spool speed and step size
let traverse_speed: Velocity = Velocity::new::<millimeter_per_second>(
spool_speed.get::<revolution_per_second>() * step_size.get::<millimeter>(),
);
traverse_speed
}
pub fn update_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
spool_speed: AngularVelocity,
) {
let speed = self.get_speed(traverse, traverse_end_stop, spool_speed);
let steps_per_second = self.fullstep_converter.velocity_to_steps(speed);
// ignore if we can't set speed
let _ = traverse.set_speed(steps_per_second);
}
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use super::*;
#[test]
fn test_calculate_traverse_speed() {
let spool_speed = AngularVelocity::new::<revolution_per_second>(1.0); // 1 revolution per second
let step_size = Length::new::<millimeter>(1.75); // 1.75 mm step size
let traverse_speed = TraverseController::calculate_traverse_speed(spool_speed, step_size);
assert_relative_eq!(
traverse_speed.get::<millimeter_per_second>(),
1.75,
epsilon = f64::EPSILON
);
}
}
+3 -5
View File
@@ -4,14 +4,13 @@ use super::LaserMachine;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -71,11 +70,10 @@ impl CacheableEvents<Self> for LaserEvents {
}
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 {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
+7 -3
View File
@@ -19,6 +19,7 @@ pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod gluetex;
pub mod laser;
pub mod machine_identification;
pub mod mock;
@@ -34,6 +35,7 @@ pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_GLUETEX_V1: u16 = 0x000A;
use serde_json::Value;
use smol::lock::RwLock;
@@ -323,7 +325,7 @@ async fn get_device_ident<
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/ExtruderV2::new] Device with role {} is not Ethercat",
"[{}] Device with role {} is not Ethercat",
module_path!(),
role
));
@@ -381,9 +383,11 @@ where
if !matched_any_identity {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/ExtruderV2::new] Device identity mismatch: expected {:?}",
"[{}] Device identity mismatch for role {}: expected {:?}, got {:?}",
module_path!(),
expected_identities
role,
expected_identities,
actual_identity
));
}
+3 -5
View File
@@ -4,15 +4,14 @@ use super::MockMachine;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -71,11 +70,10 @@ impl CacheableEvents<Self> for MockEvents {
}
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 {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
+6 -3
View File
@@ -1,11 +1,11 @@
#[cfg(feature = "mock-machine")]
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{Machine, MachineNewParams, machine_identification::MachineIdentification};
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, gluetex::Gluetex,
laser::LaserMachine, winder2::Winder2,
};
use lazy_static::lazy_static;
@@ -98,6 +98,9 @@ lazy_static! {
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<Gluetex>(Gluetex::MACHINE_IDENTIFICATION);
mc
};
}
@@ -1,16 +1,15 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use smol::lock::RwLock;
use crate::{
MACHINE_EXTRUDER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_EXTRUDER_V1, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
+6 -7
View File
@@ -1,16 +1,15 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use smol::lock::RwLock;
use crate::{
MACHINE_MOCK, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
@@ -1,16 +1,13 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
use crate::{
MACHINE_WINDER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use smol::lock::RwLock;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
use std::sync::Arc;
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
+13 -18
View File
@@ -2,25 +2,22 @@
mod winder2_imports {
pub use super::super::Winder2Mode;
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use serde::Deserialize;
pub use serde::Serialize;
pub use std::{sync::Arc, time::Duration};
pub use control_core::{
machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
pub use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
};
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
@@ -310,11 +307,9 @@ impl CacheableEvents<Self> for Winder2Events {
}
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 {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
+32 -2
View File
@@ -1,13 +1,43 @@
use crate::machines::winder2::mock::Winder2;
use control_core::machines::new::MachineAct;
use super::Winder2;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
+10 -9
View File
@@ -1,12 +1,7 @@
use std::{sync::Arc, time::Instant};
use control_core::{machines::api::MachineApi, socketio::namespace::Namespace};
use serde_json::Value;
use smol::lock::Mutex;
use crate::machines::winder2::api::Mutation;
use super::Winder2;
use crate::{MachineApi, winder2::api::Mutation};
use serde_json::Value;
use std::time::Instant;
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
@@ -61,7 +56,13 @@ impl MachineApi for Winder2 {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
+27 -16
View File
@@ -1,26 +1,23 @@
use std::time::Instant;
use crate::machines::winder2::Winder2Mode;
use crate::machines::winder2::api::LiveValuesEvent;
use crate::machines::winder2::api::{
ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events,
};
use crate::machines::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
use crate::machines::winder2::spool_speed_controller::SpoolSpeedControllerType;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
use super::Winder2;
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::winder2::Winder2Mode;
use crate::winder2::api::LiveValuesEvent;
use crate::winder2::api::{ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events};
use crate::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
use crate::winder2::spool_speed_controller::SpoolSpeedControllerType;
use crate::{MACHINE_WINDER_V1, VENDOR_QITECH};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use super::Winder2;
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
};
pub fn stop_or_pull_spool_reset(&mut self, now: Instant) {}
pub fn stop_or_pull_spool_reset(&mut self, _now: Instant) {}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
@@ -86,6 +83,20 @@ impl Winder2 {
}
pub fn build_state_event(&mut self) -> StateEvent {
use crate::MachineCrossConnectionState;
let connected_machine = self.connected_machines.get(0);
let ident = match connected_machine {
Some(machine) => Some(machine.ident.clone()),
None => None,
};
let cross_conn = MachineCrossConnectionState {
machine_identification_unique: ident,
is_available: connected_machine.is_some(),
};
StateEvent {
is_default_state: self.is_default_state,
traverse_state: self.traverse_state.clone(),
@@ -94,7 +105,7 @@ impl Winder2 {
mode_state: self.mode_state.clone(),
tension_arm_state: self.tension_arm_state.clone(),
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
connected_machine_state: self.connected_machine_state.clone(),
connected_machine_state: cross_conn,
}
}
@@ -110,7 +121,7 @@ impl Winder2 {
/// It will set [`Self::spool_mode`]
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: crate::machines::winder2::api::Mode::Standby,
mode: crate::winder2::api::Mode::Standby,
can_wind: true,
};
self.emit_state();
@@ -227,7 +238,7 @@ impl Winder2 {
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
_machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
@@ -235,7 +246,7 @@ impl Winder2 {
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
_machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
+29 -9
View File
@@ -1,21 +1,21 @@
use std::time::Instant;
use control_core::machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
};
use control_core_derive::Machine;
pub mod act;
pub mod api;
pub mod mock_emit;
pub mod new;
use crate::machines::winder2::api::Winder2Namespace;
use super::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
TraverseState,
TraverseState, Winder2Namespace,
};
use crate::{
AsyncThreadMessage, Machine, MachineConnection, MachineMessage,
machine_identification::MachineIdentificationUnique,
};
use smol::channel::{Receiver, Sender};
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct Winder2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: Winder2Namespace,
@@ -33,8 +33,18 @@ pub struct Winder2 {
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// connected machine state
pub connected_machine_state: MachineCrossConnectionState,
/// Receive from Api or MainThread
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
/// Communicate with main thread
main_sender: Option<Sender<AsyncThreadMessage>>,
/// All currently "connected" Machines
connected_machines: Vec<MachineConnection>,
/// Defaults to limit of 2
max_connected_machines: usize,
}
impl std::fmt::Display for Winder2 {
@@ -42,3 +52,13 @@ impl std::fmt::Display for Winder2 {
write!(f, "Winder2")
}
}
impl Machine for Winder2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
+18 -14
View File
@@ -1,16 +1,15 @@
use crate::machines::winder2::{
api::{
use crate::{
MachineNewParams, MachineNewTrait,
winder2::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
TensionArmState, TraverseState, Winder2Namespace,
},
mock::Winder2,
};
use control_core::machines::connection::MachineCrossConnectionState;
impl control_core::machines::new::MachineNewTrait for Winder2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
use super::Winder2;
impl MachineNewTrait for Winder2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
@@ -18,11 +17,11 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
crate::MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
crate::MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
@@ -30,7 +29,12 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
let now = std::time::Instant::now();
let mut extruder_mock_machine = Self {
let (sender, receiver) = smol::channel::unbounded();
let mut winder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
max_connected_machines: 2,
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: Winder2Namespace {
namespace: params.namespace.clone(),
@@ -43,11 +47,11 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
mode_state: ModeState::default(),
tension_arm_state: TensionArmState::default(),
spool_speed_controller_state: SpoolSpeedControllerState::default(),
connected_machine_state: MachineCrossConnectionState::default(),
connected_machines: vec![],
};
extruder_mock_machine.emit_state();
winder_mock_machine.emit_state();
Ok(extruder_mock_machine)
Ok(winder_mock_machine)
}
}
+5 -5
View File
@@ -19,10 +19,9 @@ mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use std::time::Instant;
pub use units::f64::Length;
pub use units::length::meter;
}
use units::Length;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
@@ -36,19 +35,19 @@ mod winder2_imports {
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
pub use smol::channel::{Receiver, Sender};
pub use smol::lock::RwLock;
pub use std::{fmt::Debug, sync::Weak, time::Instant};
pub use crate::buffer1::BufferV1;
pub use crate::{AsyncThreadMessage, Machine};
pub use units::ConstZero;
pub use units::f64::Length;
pub use units::{length::meter, length::millimeter, velocity::meter_per_second};
}
#[cfg(not(feature = "mock-machine"))]
use smol::channel::{Receiver, Sender};
pub use winder2_imports::*;
use crate::{AsyncThreadMessage, Machine};
#[cfg(not(feature = "mock-machine"))]
use crate::{
MACHINE_WINDER_V1, MachineConnection, MachineMessage, VENDOR_QITECH,
@@ -74,6 +73,7 @@ impl Default for SpoolAutomaticAction {
}
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for Winder2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
+9 -1
View File
@@ -43,8 +43,12 @@ tracing-subscriber = { version = "0.3.20", features = [
"chrono",
"registry",
] }
utils = { version = "0.1.0", path="../utils" }
tonic = { version = "0.13.1", optional = true }
spin_sleep = "1.3.3"
dhat = { version = "0.3.3", optional = true }
ctrlc = { version = "3.3", optional = true }
[dev-dependencies]
approx = "0.5.1"
@@ -58,7 +62,11 @@ tracing-journald = ["dep:tracing-journald"]
mock-machine = []
memory-locking = []
io-uring = []
development-build = []
development-build = ["ctrlc"]
heap-profile = ["dhat"]
[profile.release]
codegen-units = 1
-11
View File
@@ -1,11 +0,0 @@
# Server Structure
- `/server`: Is Backend part that makes connection between Beckhoff and Electron. Implements machine logic.
## EtherCAT
- `/ethercat`: Implements logic of EtherCAT
## Serial
- `/serial`: Implements logic of Serial, starting from serial device detection ending with serial device registration as machine
+1 -8
View File
@@ -1,5 +1,4 @@
use crate::ethercat::config::{MAX_SUBDEVICES, PDI_LEN};
use crate::performance_metrics::EthercatPerformanceMetrics;
use crate::rest::handlers::write_machine_device_identification::MachineDeviceInfoRequest;
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::{MachineObj, MachinesEventBuilder};
@@ -79,14 +78,10 @@ impl EtherCatDeviceMetaData {
pub struct SharedState {
pub socketio_setup: SocketioSetup,
pub api_machines: Mutex<HashMap<MachineIdentificationUnique, Sender<MachineMessage>>>,
pub current_machines_meta: Mutex<Vec<MachineObj>>,
pub rt_machine_creation_channel: Sender<HotThreadMessage>,
pub main_channel: Sender<AsyncThreadMessage>,
pub performance_metrics: Arc<RwLock<EthercatPerformanceMetrics>>,
pub ethercat_meta_data: RwLock<Vec<EtherCatDeviceMetaData>>,
}
@@ -147,7 +142,7 @@ impl SharedState {
pub async fn add_machines_if_not_exists(&self, machines: Vec<MachineObj>) {
let mut current_machines = self.current_machines_meta.lock().await;
tracing::info!("add_machines_if_not_exists: {:?}", current_machines);
// Track existing machine identifiers for quick lookup
let existing_ids: HashSet<_> = current_machines
.iter()
@@ -175,8 +170,6 @@ impl SharedState {
socket_queue_tx,
socket_queue_rx,
},
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
api_machines: Mutex::new(HashMap::new()),
rt_machine_creation_channel: sender,
main_channel: main_async_channel,
+109 -128
View File
@@ -1,4 +1,5 @@
use crate::app_state::{EthercatSetup, HotThreadMessage, SharedState};
use crate::app_state::{EthercatSetup, HotThreadMessage};
use crate::performance_metrics::EthercatPerformanceMetrics;
use bitvec::prelude::*;
use control_core::realtime::set_core_affinity;
#[cfg(not(feature = "development-build"))]
@@ -7,14 +8,20 @@ use machines::Machine;
use machines::machine_identification::write_machine_device_identification;
use smol::channel::Receiver;
use spin_sleep::SpinSleeper;
use std::time::Duration;
use std::time::Instant;
use std::{sync::Arc, time::Duration};
use tracing::{instrument, trace_span};
pub struct RtLoopInputs<'a> {
pub machines: &'a mut Vec<Box<dyn Machine>>,
pub ethercat_setup: Option<Box<EthercatSetup>>,
pub ethercat_perf_metrics: Option<&'a mut EthercatPerformanceMetrics>,
pub sleeper: SpinSleeper,
pub cycle_target: Duration,
}
// 300 us loop cycle target
// SharedState is mostly read from and rarely locked, but does not contain any machine,ethercat devices etc
pub fn start_loop_thread(
shared_state: Arc<SharedState>,
rt_receiver: Receiver<HotThreadMessage>,
cycle_target: Duration,
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
@@ -42,96 +49,78 @@ pub fn start_loop_thread(
);
}
let rt = smol::LocalExecutor::new();
// Instead of creating a NEW async task every iteration of our realtime txrx loop, create it ONCE
let mut ethercat: Option<Box<EthercatSetup>> = None;
// We need the vec in a box due to Mutably borrowing
let mut ethercat_perf = EthercatPerformanceMetrics::new();
let mut machines: Vec<Box<dyn Machine>> = vec![];
let mut rt_loop_inputs = RtLoopInputs {
machines: &mut machines,
ethercat_setup: None,
sleeper,
cycle_target,
ethercat_perf_metrics: Some(&mut ethercat_perf),
};
smol::block_on(async {
rt.run(async {
loop {
let res = rt_receiver.try_recv();
let msg = match res {
Ok(msg) => msg,
Err(_) => HotThreadMessage::NoMsg,
};
loop {
let msg = match rt_receiver.try_recv() {
Ok(msg) => msg,
Err(_) => HotThreadMessage::NoMsg,
};
match msg {
HotThreadMessage::NoMsg => (),
HotThreadMessage::AddEtherCatSetup(ethercat_setup) => {
ethercat = Some(Box::new(ethercat_setup));
match msg {
HotThreadMessage::NoMsg => {}
HotThreadMessage::AddEtherCatSetup(ethercat_setup) => {
rt_loop_inputs.ethercat_setup = Some(Box::new(ethercat_setup));
}
HotThreadMessage::WriteMachineDeviceInfo(info_request) => {
if let Some(ethercat_setup) = &rt_loop_inputs.ethercat_setup {
if let Ok(subdevice) = ethercat_setup.group.subdevice(
&ethercat_setup.maindevice,
info_request
.hardware_identification_ethercat
.subdevice_index,
) {
let _res = smol::block_on(write_machine_device_identification(
&subdevice,
&ethercat_setup.maindevice,
&info_request.device_machine_identification,
));
}
HotThreadMessage::WriteMachineDeviceInfo(
machine_device_info_request,
) => {
match &ethercat {
Some(ethercat_setup) => {
match ethercat_setup.group.subdevice(
&ethercat_setup.maindevice,
machine_device_info_request
.hardware_identification_ethercat
.subdevice_index,
) {
Ok(subdevice) => {
let _ = write_machine_device_identification(
&subdevice,
&ethercat_setup.maindevice,
&machine_device_info_request
.device_machine_identification,
)
.await;
}
Err(_) => (),
};
}
None => (),
};
}
HotThreadMessage::DeleteMachine(machine_identification_unique) => {
machines.retain(|m| {
m.get_machine_identification_unique()
!= machine_identification_unique
});
}
HotThreadMessage::AddMachines(machine_vec) => {
for new_machine in machine_vec {
let id = new_machine.get_machine_identification_unique();
// Only add if a machine with the same ID doesn't exist
if !machines
.iter()
.any(|m| m.get_machine_identification_unique() == id)
{
machines.push(new_machine);
}
}
}
};
if let Err(e) = loop_once(
shared_state.clone(),
&mut machines,
&sleeper,
cycle_target,
ethercat.as_deref(),
)
.await
{
tracing::error!("Loop failed\n{:?}", e);
break;
}
}
})
.await;
});
HotThreadMessage::DeleteMachine(unique_id) => {
rt_loop_inputs
.machines
.retain(|m| m.get_machine_identification_unique() != unique_id);
}
HotThreadMessage::AddMachines(machine_vec) => {
tracing::info!("received machines{:?}", machine_vec);
for new_machine in machine_vec {
let id = new_machine.get_machine_identification_unique();
if !rt_loop_inputs
.machines
.iter()
.any(|m| m.get_machine_identification_unique() == id)
{
rt_loop_inputs.machines.push(new_machine);
}
}
}
}
if let Some(last_loop_start) = shared_state
.performance_metrics
.read_arc_blocking()
.last_loop_start
{
tracing::info!("Failing Loop Took {:?}", last_loop_start.elapsed());
if let Err(e) = loop_once(&mut rt_loop_inputs) {
tracing::error!(
"Loop failed\n {:?} \n Last Loop Took: {:?}",
e,
rt_loop_inputs
.ethercat_perf_metrics
.unwrap()
.last_loop_start
.unwrap()
.elapsed()
);
break;
}
}
// Exit the entire program if the Loop fails
// gets restarted by systemd if running on NixOS, or different distro wtih the same sysd service
std::process::exit(1);
@@ -145,11 +134,7 @@ pub async fn copy_ethercat_inputs(
// only if we have an ethercat setup
// - tx/rx cycle
// - copy inputs to devices
if let Some(ethercat_setup) = ethercat_setup {
let span = trace_span!("loop_once_inputs");
let _enter = span.enter();
// Measure tx_rx performance
ethercat_setup
.group
.tx_rx(&ethercat_setup.maindevice)
@@ -202,9 +187,6 @@ pub async fn copy_ethercat_outputs(
ethercat_setup: Option<&EthercatSetup>,
) -> Result<(), anyhow::Error> {
if let Some(ethercat_setup) = ethercat_setup {
let span = trace_span!("loop_once_outputs");
let _enter = span.enter();
// copy outputs from devices
for (i, subdevice) in ethercat_setup
.group
@@ -248,59 +230,58 @@ pub async fn copy_ethercat_outputs(
Ok(())
}
pub async fn execute_machines(machines: &mut Vec<Box<dyn Machine>>) {
pub fn execute_machines(machines: &mut Vec<Box<dyn Machine>>) {
let now = Instant::now();
for machine in machines.iter_mut() {
let span = trace_span!("loop_once_act_machine",);
let _enter = span.enter();
machine.act(now);
}
}
#[instrument(skip(app_state, machines))]
pub async fn loop_once<'maindevice>(
app_state: Arc<SharedState>,
machines: &mut Vec<Box<dyn Machine>>,
sleeper: &SpinSleeper,
cycle_target: Duration,
ethercat_setup: Option<&EthercatSetup>,
) -> Result<(), anyhow::Error> {
// No more logging in loop_once
pub fn loop_once<'maindevice>(inputs: &mut RtLoopInputs<'_>) -> Result<(), anyhow::Error> {
let loop_once_start = std::time::Instant::now();
// Record cycle start for performance metrics
{
let mut metrics = app_state.performance_metrics.write().await;
metrics.cycle_start();
}
{
let txrx_start = Instant::now();
copy_ethercat_inputs(ethercat_setup).await?;
let txrx_duration = txrx_start.elapsed();
let mut metrics = app_state.performance_metrics.write().await;
metrics.record_txrx_time(txrx_duration);
if inputs.ethercat_setup.is_some() && inputs.ethercat_perf_metrics.is_some() {
inputs
.ethercat_perf_metrics
.as_deref_mut()
.unwrap()
.cycle_start();
let res = smol::block_on(copy_ethercat_inputs(inputs.ethercat_setup.as_deref()));
match res {
Ok(_) => (),
Err(e) => {
return Err(anyhow::anyhow!("copy_ethercat_inputs failed: {:?}", e));
}
};
}
// execute machines
{
let span = trace_span!("loop_once_act");
let _enter = span.enter();
execute_machines(machines).await;
execute_machines(&mut inputs.machines);
if inputs.ethercat_setup.is_some() && inputs.ethercat_perf_metrics.is_some() {
let res = smol::block_on(copy_ethercat_outputs(inputs.ethercat_setup.as_deref()));
match res {
Ok(_) => (),
Err(e) => {
return Err(anyhow::anyhow!("copy_ethercat_outputs failed: {:?}", e));
}
};
}
{
copy_ethercat_outputs(ethercat_setup).await?;
}
if ethercat_setup.is_some() {
if inputs.ethercat_setup.is_some() {
// spin_sleep so we have a cycle time of ~300us
// This does push usage to 100% if completely busy, but provides much better accuracy then thread sleep or async sleep
sleeper.sleep_until(loop_once_start + cycle_target);
inputs
.sleeper
.sleep_until(loop_once_start + inputs.cycle_target);
} else {
// if we dont have an ethercat setup or other rt relevant stuff do the "worse" async sleep or later if we get rid of async thread::sleep or yielding
// We do this, so that when no rt relevant code runs the cpu doesnt spin at 100% for no reason
let loop_duration = loop_once_start.elapsed();
if cycle_target > loop_once_start.elapsed() {
smol::Timer::after(cycle_target - loop_duration).await;
if inputs.cycle_target > loop_once_start.elapsed() {
smol::block_on(smol::Timer::after(inputs.cycle_target - loop_duration));
}
}
Ok(())
}
+66 -14
View File
@@ -15,12 +15,6 @@ use machines::{
winder2::api::GenericEvent,
};
#[cfg(feature = "mock-machine")]
use mock::init::init_mock;
#[cfg(feature = "mock-machine")]
pub mod mock;
use app_state::{HotThreadMessage, SharedState};
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
@@ -28,6 +22,7 @@ use ethercat::{
setup::setup_loop,
};
use r#loop::start_loop_thread;
use panic::init_panic_handling;
use rest::init::start_api_thread;
use serialport::UsbPortInfo;
@@ -38,6 +33,13 @@ use smol::{
use socketio::{main_namespace::machines_event::MachineObj, queue::start_socketio_queue};
use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
#[cfg(feature = "mock-machine")]
use mock_init::init_mock;
#[cfg(feature = "mock-machine")]
pub mod mock_init;
pub mod app_state;
pub mod ethercat;
pub mod logging;
@@ -82,7 +84,10 @@ pub async fn add_serial_device(
let machine = match new_machine {
Ok(machine) => machine,
Err(_) => return,
Err(e) => {
tracing::error!("{:?}", e);
return;
}
};
shared_state
@@ -102,7 +107,6 @@ pub async fn add_serial_device(
.rt_machine_creation_channel
.send(HotThreadMessage::AddMachines(vec![machine]))
.await;
shared_state.clone().send_machines_event().await;
}
@@ -265,27 +269,52 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
}
}
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "development-build")]
fn setup_ctrlc_handler() -> Arc<AtomicBool> {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
eprintln!("Ctrl-C pressed, shutting down...");
r.store(false, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
running
}
#[cfg(feature = "heap-profile")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
init_panic_handling();
#[cfg(feature = "heap-profile")]
let _profiler = dhat::Profiler::new_heap();
#[cfg(feature = "development-build")]
let running = setup_ctrlc_handler();
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(300);
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();
let (main_sender, main_receiver) = smol::channel::unbounded();
let shared_state = SharedState::new(sender.clone(), main_sender.clone());
let app_state = Arc::new(shared_state);
let _loop_thread = start_loop_thread(app_state.clone(), receiver, CYCLE_TARGET_TIME);
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME);
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
#[cfg(not(feature = "mock-machine"))]
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
let mut handle_async_machine_requests =
smol::spawn(handle_async_requests(main_receiver, app_state.clone())).fuse();
@@ -295,9 +324,18 @@ fn main() {
send_ethercat_discovering(app_state.clone()).await;
});
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
smol::block_on(async {
loop {
#[cfg(feature = "development-build")]
if !running.load(Ordering::SeqCst) {
tracing::info!("Shutdown signal received, exiting main loop.");
break;
}
// lets the async runtime decide which future to run next
#[cfg(not(feature = "mock-machine"))]
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
@@ -321,6 +359,20 @@ fn main() {
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
}
#[cfg(feature = "mock-machine")]
select! {
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
_ = smol::Timer::after(Duration::from_millis(1)).fuse() => {
}
}
}
-124
View File
@@ -1,124 +0,0 @@
#[cfg(feature = "mock-machine")]
use crate::serial::devices::extruder_mock::ExtruderMockSerialDevice;
use crate::serial::devices::winder_mock::WinderMockSerialDevice;
#[cfg(feature = "mock-machine")]
use {
crate::app_state::AppState,
crate::machines::registry::MACHINE_REGISTRY,
crate::serial::devices::mock::MockSerialDevice,
crate::socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
control_core::{
serial::{SerialDeviceNew, SerialDeviceNewParams},
socketio::namespace::NamespaceCacheingLogic,
},
std::sync::Arc,
};
#[cfg(feature = "mock-machine")]
pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// For mock devices, we need to manually create and add them to the machine manager
// since they won't be detected by the serial detection loop
return smol::block_on(async {
// Create a mock serial device manually
let serial_params = SerialDeviceNewParams {
path: "/dev/mock-serial".to_string(),
};
// Create the mock serial device
let _ = match MockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
let mut machine_guard = app_state.machines.write().await;
machine_guard.add_serial_device(
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify clients via socketio about the new machine
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create mock serial device: {}", e);
return Err(e);
}
};
let _ = match ExtruderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
let mut machine_guard = app_state.machines.write().await;
machine_guard.add_serial_device(
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify clients via socketio about the new machine
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
};
match WinderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
let mut machine_guard = app_state.machines.write().await;
machine_guard.add_serial_device(
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify clients via socketio about the new machine
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
Ok(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
}
});
}
-1
View File
@@ -1 +0,0 @@
pub mod init;
+94
View File
@@ -0,0 +1,94 @@
use crate::add_serial_device;
use crate::app_state::SharedState;
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
use machines::registry::MACHINE_REGISTRY;
use std::sync::Arc;
pub fn init_mock(app_state: Arc<SharedState>) -> Result<(), anyhow::Error> {
// For mock devices, we need to manually create and add them to the machine manager
// since they won't be detected by the serial detection loop
return smol::block_on(async {
// Create a mock serial device manually
use machines::{
SerialDeviceNew, SerialDeviceNewParams,
serial::devices::{
extruder_mock::ExtruderMockSerialDevice, mock::MockSerialDevice,
winder_mock::WinderMockSerialDevice,
},
};
let serial_params = SerialDeviceNewParams {
path: "/dev/mock-serial".to_string(),
};
// Create the mock serial device
let _ = match MockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
use crate::add_serial_device;
use machines::registry::MACHINE_REGISTRY;
add_serial_device(
app_state.clone(),
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
)
.await;
}
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create mock serial device: {}", e);
return Err(e);
}
};
let _ = match ExtruderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
use crate::add_serial_device;
use machines::registry::MACHINE_REGISTRY;
add_serial_device(
app_state.clone(),
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
)
.await;
}
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
};
match WinderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
add_serial_device(
app_state.clone(),
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.clone().socketio_setup.socket_queue_tx.clone(),
)
.await;
Ok(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
}
});
}
+5 -11
View File
@@ -44,14 +44,8 @@ impl EthercatPerformanceMetrics {
self.last_loop_start = Some(now);
}
/// Records the duration of a tx_rx operation
pub fn record_txrx_time(&mut self, duration: Duration) {
self.add_txrx_time(duration);
self.maybe_log_metrics();
}
/// Adds a tx_rx time measurement
fn add_txrx_time(&mut self, duration: Duration) {
pub fn add_txrx_time(&mut self, duration: Duration) {
if self.txrx_times.len() >= METRICS_WINDOW_SIZE {
self.txrx_times.pop_front();
}
@@ -67,7 +61,7 @@ impl EthercatPerformanceMetrics {
}
/// Logs metrics if enough time has passed
fn maybe_log_metrics(&mut self) {
pub fn maybe_log_metrics(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_log_time).as_secs() >= METRICS_LOG_INTERVAL_SECS {
self.log_metrics();
@@ -178,9 +172,9 @@ mod tests {
let mut metrics = EthercatPerformanceMetrics::new();
// Record some measurements
metrics.record_txrx_time(Duration::from_millis(1));
metrics.record_txrx_time(Duration::from_millis(2));
metrics.record_txrx_time(Duration::from_millis(3));
metrics.add_txrx_time(Duration::from_millis(1));
metrics.add_txrx_time(Duration::from_millis(2));
metrics.add_txrx_time(Duration::from_millis(3));
assert_eq!(metrics.txrx_times.len(), 3);
}
-56
View File
@@ -1,56 +0,0 @@
# Serial Devices
Serial communication interface through which information transfers in or out sequentially one bit at a time.
`Important NOTE: Vendor ID 0x0403 and Product ID 0x6001 is not Vendor & Product ID of Laser, but USB Modbus itself.`
Current Serial Devices:
- Laser, connected through RTU Modbus (USB).
## Current Serial Detection (./server/src/serial/serial_detection.rs)
Serial Detection contains 3 main components:
- `Serial Registry`, the interface that is used as buffer for all registered Serial Devices, and responsible for start of registered Serial Devices and returns link to started Serial Device. (./control-core/src/serial/registry.rs)
- `Connected Serial USB`, the hashmap that saves the path to USB port with the link to started Serial Device under this port path.
- `Ports`, previously detected USB ports with pathes and usb port information (such as Vendor & Product ID, Serial Number)
The cycle of Serial Detection is consisted from following steps:
`1)` Detection of all current Serial Connections
`2)` Extraction of USB Port connections (only this type of connection contains information about vid and pid of modbus itself)
`3)` Comparison of previously detected USB ports with currently detected USB ports (from the step 2)
`4)` Resave previously detected USB ports with current ports
`5)` Start connection of newly detected ports with Serial Registry and save the path to the port with the link to started Serial Device to `Connected Serial USB`
`6)` Remove serial ports from `Connected Serial USB` that was not detected (in the step 2)
`7)` The last step is to check whether the connection with the serial devices is still active. Using the `Connected Devices` parameters or functions, we can verify if the serial device can still fulfill its functionalities. If not, the link to the connected device should be resaved with a disconnection message.
The next step is to check if the devices that were previously disconnected or unable to establish a connection can now connect to the serial device during this iteration. If they can, save the link to the device in `Connected Serial USB`.
## Laser (/server/src/machines/laser/new.rs)
Laser Device has simple structure, it has two parameters (measured diameter) and the path to the serial port, for communication establishment. Also it is connected to the Trait of all Serial Devices.
- After the start of Laser, it will constantly update measured diameter parameter, under current Configurations of Modbus RTU connection (30 diameter updates per second).
- If Connection could not be established with Laser 10 times in a row, then diameter update will stop, and instead of diameter parameter will be error message.
- Diameter Update is running as a separate thread task.
### Current Laser Parameters
`1)` Baud Rate 38400 (for maximum efficiency)
`2)` 8 bits per character.
`3)` No parity bit.
`4)` One stop bit.
## Serial Init Loop (/server/src/serial/init.rs)
The main task of Serial Init loop is to run `Serial Detection` as a separate thread task. It executes `Cycle of Serial Detection` each 300 ms.
## Serial SERIAL_DETECTION lazy static (/server/src/serial/register.rs)
The main goal is to create a global Serial Detection static using already registered Serial Product Configurations. These configurations should be accessible and updatable from different thread tasks.
## TODO list
- We need to assign Vendor & Product ID of Laser separately as machine.
- Remake retry connection, so it will work for all devices and not only Laser specific
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "utils"
version = "0.1.0"
edition = "2021"
[dependencies]
backtrace = "0.3"
dhat = "0.3.3"
[features]
heap-profile = []
+3
View File
@@ -0,0 +1,3 @@
#[cfg(feature = "heap-profile")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;