Compare commits

...
Author SHA1 Message Date
JalleliAchraf 658f524eff glitch fix attempt 2026-02-18 10:07:43 +01:00
JalleliAchraf 88b0c87a51 Merge branch '1004-target-line-with-current-target' of github.com:qitechgmbh/control into 1004-target-line-with-current-target 2026-02-18 10:02:43 +01:00
JalleliAchraf 4178c90769 formatting 2026-02-17 20:49:28 +01:00
JalleliAchraf 2c99231f1f target line jittering replaced with a natural flow motion 2026-02-17 20:49:28 +01:00
Saurabh Bagade 26b417c0ec refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-02-17 20:49:28 +01:00
Saurabh Bagade 7df303bdf4 code formatting fixed for animation.ts 2026-02-17 20:49:28 +01:00
Saurabh Bagade 93c5e8d599 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade c5fbdd0a0f refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade 45e03b9c93 fixed the issue with the target line 2026-02-17 20:49:28 +01:00
Christian 5562cf5a53 Add GitHub Actions workflow for issue status checks
Refactor issue status check workflow to identify stale assigned issues and request updates

Refactor linked pull request check to use search API for improved reliability

Add comprehensive error handling and rate limit monitoring
2026-02-17 17:48:51 +01:00
Achraf Jalleli 3a24c24836 Merge branch 'master' into 1004-target-line-with-current-target 2026-02-13 20:04:49 +01:00
JalleliAchraf 26e4c6ab6c formatting 2026-02-13 19:58:25 +01:00
JalleliAchraf 60f95ec9b6 target line jittering replaced with a natural flow motion 2026-02-13 19:53:25 +01:00
Christian af0b5deff0 Merge pull request #1135 from qitechgmbh/991-graph-tab-in-top-navigation-bar-not-connecting-to-graph-page-2
fixed the graph page not not connecting to navigation bar  bug
2026-02-13 08:06:53 +01:00
Christian 743a589c1d Update README.md to enhance video descriptions and add additional video section 2026-02-12 17:04:35 +01:00
Achraf Jalleli ee7550d89f Merge pull request #1127 from qitechgmbh/1042-implement-minimal-machine-example-for-wago-750-530
1042 implement minimal machine example for wago 750 530
2026-02-12 13:37:26 +01:00
Achraf JalleliandCopilot ec13f715aa slug fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 13:22:14 +01:00
Achraf Jalleli 2af746e7a4 Add files via upload 2026-02-12 13:14:04 +01:00
JalleliAchraf 3af3fb9d79 used array.fill 2026-02-12 11:22:11 +01:00
Achraf Jalleli ff3103bec2 Merge branch 'master' into 1042-implement-minimal-machine-example-for-wago-750-530 2026-02-12 11:00:23 +01:00
Hans Sebastian Pöhlmann 5eb3c39f84 1043 implement minimal machine example for wago 750 1506 (#1106)
* wago 750_1506 minimal machine backend apparently done, frontend still unfinished

* wago 750_1506 frontend apparently done, but untested

* added bus coupler to device roles
2026-02-12 10:54:06 +01:00
Robin Krämer 2c715d6a8e fixed initialization by raising timeouts for eeprom,eeprom write lock (#1136)
* fixed initialization by raising timeouts for eeprom,eeprom write lock

* fixed formatting
2026-02-11 14:24:48 +01:00
JalleliAchraf fb12001721 fixed the graph page not not connecting to navigation bar bug 2026-02-10 18:29:04 +01:00
Robin Krämer ad94754fcf Added missing revision for Watercooling (#1130) 2026-02-09 15:14:30 +01:00
Achraf Jalleli b919721c3a Update npmDepsHash for electron package 2026-02-06 16:20:26 +01:00
JalleliAchraf a428a2aa02 formatting 2026-02-06 16:12:25 +01:00
JalleliAchraf 7b479c6021 Merge branch '1042-implement-minimal-machine-example-for-wago-750-530' of github.com:qitechgmbh/control into 1042-implement-minimal-machine-example-for-wago-750-530 2026-02-06 16:09:03 +01:00
JalleliAchraf 17ac24c79d minor improvements 2026-02-06 16:08:25 +01:00
JalleliAchraf 8c7a04b616 formatting fix 2026-02-06 15:57:55 +01:00
github-actions[bot] fe248c79ae chore: update npmDepsHash to sha256-/7zfLof91S97OmW8dBeaVajYEv3r2eYOZ8Zb2ahykaQ= 2026-02-06 14:54:58 +00:00
JalleliAchraf 038df09611 removed unecessary differences between this branch and master 2026-02-06 15:49:21 +01:00
JalleliAchraf 9e43d8d431 several small fixes 2026-02-06 15:44:13 +01:00
JalleliAchraf d364263d6e wago digital output minimal machine 2026-02-06 15:22:00 +01:00
Christian 376e4a4ea2 Merge pull request #1117 from qitechgmbh/1100-update-the-main-readme
1100 update the main readme
2026-02-05 21:39:54 +01:00
Robin Krämer 0b30ae9a85 removed excessive logging, simplified socketio task with actual error printed … (#1122)
* removed excessive logging, simplified socketio task with actual error message

* fixed formatting
2026-02-05 17:34:48 +01:00
Christian 528952238d Update issue templates 2026-02-05 14:54:10 +01:00
Robin Krämer 8c32ce2a2b loop now uses Different Cycle time when development-build is set (#1112)
* loop now uses Different Cycle time when development-build is set

* fixed formatting

* hid stop_dnsmasq import behind feature flag

* added feature gate for start_dnsmasq in main
2026-02-05 11:52:56 +01:00
Christian 579ef0d2e2 Enhance video section in README.md with descriptive captions for better user engagement 2026-02-04 19:39:34 +01:00
Christian b6263e90d6 Refactor video section in README.md for improved layout and clarity 2026-02-04 19:32:33 +01:00
Christian f67f098ff3 better wording 2026-02-04 19:30:21 +01:00
Christian 1a3c26b789 Update issue templates 2026-02-04 18:30:07 +01:00
Oshgnacknak 49f0c9f289 Merge pull request #1113 from qitechgmbh/add-issue-templates
Add issue templates
2026-02-04 15:40:33 +01:00
Oshgnacknak e60daed274 Merge pull request #1116 from qitechgmbh/1115-remove-gitattributes-and-gitmodules
1115 Remove .gitattributes and .gitmodules files
2026-02-04 15:35:35 +01:00
Oshgnacknak 9a9120398e Merge pull request #1097 from qitechgmbh/dependabot/cargo/serde_json-1.0.149
Bump serde_json from 1.0.145 to 1.0.149
2026-02-04 15:22:07 +01:00
Christian dec342498c Fix link to Device Examples in README.md for improved navigation 2026-02-04 15:21:06 +01:00
Christian bc8cc5dd3d Update README.md to enhance documentation structure and clarity 2026-02-04 15:18:19 +01:00
Oshgnacknak 4a1f3d125d Merge pull request #1096 from qitechgmbh/dependabot/cargo/syn-2.0.114
Bump syn from 2.0.111 to 2.0.114
2026-02-04 15:07:29 +01:00
dependabot[bot] 1d1eeb3d48 Bump serde_json from 1.0.145 to 1.0.149
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.145 to 1.0.149.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.145...v1.0.149)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 14:06:09 +00:00
Oshgnacknak c395d4e172 Merge pull request #1094 from qitechgmbh/dependabot/cargo/euclid-0.22.13
Bump euclid from 0.22.11 to 0.22.13
2026-02-04 15:05:33 +01:00
Oshgnacknak 5009c5ca75 Merge pull request #1092 from qitechgmbh/dependabot/cargo/axum-0.8.8
Bump axum from 0.8.7 to 0.8.8
2026-02-04 15:03:41 +01:00
Oshgnacknak 50dbc46b8c Merge pull request #1089 from qitechgmbh/dependabot/cargo/tracing-subscriber-0.3.22
Bump tracing-subscriber from 0.3.20 to 0.3.22
2026-02-04 15:01:40 +01:00
Oshgnacknak 774e7e2f8b Merge pull request #1108 from qitechgmbh/dependabot/cargo/bytes-1.11.1
Bump bytes from 1.11.0 to 1.11.1
2026-02-04 14:59:55 +01:00
Oshgnacknak 270872fe81 Merge pull request #1114 from qitechgmbh/1081-automate-npm-deps-hash-for-electronnix
feat: automate npm deps hash update for Electron
2026-02-04 14:33:01 +01:00
Oshgnacknak a749f59186 Merge pull request #1110 from qitechgmbh/1109-remove-hooks
1109 remove hooks folder
2026-02-04 14:28:37 +01:00
Christian bae1de3c54 Remove .gitattributes and .gitmodules files 2026-02-04 13:52:33 +01:00
Christian 7681dae32c feat: automate npmDepsHash updates for Electron
- Add GitHub Actions workflow to automatically calculate and update npmDepsHash
- Workflow triggers on package-lock.json changes in PRs
- Uses nix prefetch-npm-deps to calculate correct hash
- Automatically commits hash updates to the PR
- Supports manual triggering via workflow_dispatch
2026-02-04 13:40:58 +01:00
ChristianandCopilot 2d98d369e1 Update .github/ISSUE_TEMPLATE/feature_request.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 13:04:29 +01:00
Christian 310ac42a88 Update issue templates
Add issue templates for better feature requests or bug reports.
2026-02-04 12:57:35 +01:00
Christian 182df9c569 Merge pull request #1107 from qitechgmbh/electron-deps
electron deps
2026-02-03 22:05:39 +01:00
Christian 153a719d64 remove Git hooks and pre-commit script 2026-02-03 21:56:06 +01:00
Christian 84603d1594 update hash 2026-02-03 21:53:28 +01:00
Christian 17b2e3e297 clear hash 2026-02-03 21:52:02 +01:00
Christian fb1cf33c7f Update package lock 2026-02-03 21:44:19 +01:00
dependabot[bot] ae2d0fa8b1 Bump bytes from 1.11.0 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 19:43:19 +00:00
ᴏxᴏ.ᴍɪᴋᴀ ed06b63b77 Merge pull request #1083 from qitechgmbh/1076-minimum-hardware-documentation-links
1076 minimum hardware documentation links
2026-02-03 17:15:21 +01:00
Oshgnacknak 9f0f45d19c Fix hash 2026-02-03 16:26:07 +01:00
dependabot[bot] a75c0ff08a Bump zustand from 5.0.9 to 5.0.11 in /electron
Bumps [zustand](https://github.com/pmndrs/zustand) from 5.0.9 to 5.0.11.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.9...v5.0.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 16:19:11 +01:00
dependabot[bot] 205432c926 Bump vitest from 3.2.4 to 4.0.18 in /electron
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 4.0.18.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.0.18
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 16:18:11 +01:00
dependabot[bot] 781783464b Bump eslint from 9.39.1 to 9.39.2 in /electron
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.1 to 9.39.2.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 16:16:43 +01:00
dependabot[bot] 4fa2a1a221 Bump react-i18next from 15.7.4 to 16.5.4 in /electron
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 15.7.4 to 16.5.4.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v15.7.4...v16.5.4)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 16.5.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 16:14:36 +01:00
Oshgnacknak b734468741 Fix event names in wago power machine 2026-02-03 16:14:02 +01:00
dependabot[bot] 1ed4ae3d2c Bump socket.io-client from 4.8.1 to 4.8.3 in /electron
Bumps [socket.io-client](https://github.com/socketio/socket.io) from 4.8.1 to 4.8.3.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-client@4.8.1...socket.io-client@4.8.3)

---
updated-dependencies:
- dependency-name: socket.io-client
  dependency-version: 4.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 15:57:02 +01:00
dependabot[bot] 8095222cad Bump syn from 2.0.111 to 2.0.114
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.111 to 2.0.114.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.111...2.0.114)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 12:37:25 +00:00
dependabot[bot] d19af0923c Bump axum from 0.8.7 to 0.8.8
Bumps [axum](https://github.com/tokio-rs/axum) from 0.8.7 to 0.8.8.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.8.7...axum-v0.8.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 12:37:25 +00:00
Robin Krämer 08ce5a37bd aquapath now works with all the other machines connected (#1103)
* aquapath now works with all the other machines connected

* fixed formatting

* removed unused import

* fixed formatting AGAIN
2026-02-03 13:36:07 +01:00
Robin Krämer 78c7667d96 Merge pull request #1087 from qitechgmbh/fix-wago-init-logic
fixed Wago Initialization Logic
2026-02-02 11:02:44 +01:00
dependabot[bot] bef3389874 Bump euclid from 0.22.11 to 0.22.13
Bumps [euclid](https://github.com/servo/euclid) from 0.22.11 to 0.22.13.
- [Release notes](https://github.com/servo/euclid/releases)
- [Commits](https://github.com/servo/euclid/commits)

---
updated-dependencies:
- dependency-name: euclid
  dependency-version: 0.22.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 02:25:36 +00:00
dependabot[bot] 985937c227 Bump tracing-subscriber from 0.3.20 to 0.3.22
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.20 to 0.3.22.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.20...tracing-subscriber-0.3.22)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 02:25:04 +00:00
Saurabh Bagade 2c7f65dc78 refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-01-31 13:54:14 +01:00
Saurabh Bagade d3d2ab4828 code formatting fixed for animation.ts 2026-01-31 13:54:14 +01:00
Saurabh Bagade a839a374a3 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 6049737955 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 0c4a1eec39 fixed the issue with the target line 2026-01-31 13:54:14 +01:00
kraemr 22be4b79f0 fixed linter errors and added logic for 750_430 2026-01-30 16:33:50 +01:00
kraemr be84edf0fd removed unused parameter from ModulePDOMapping 2026-01-30 16:23:22 +01:00
kraemr ff3e787a03 fixed Wago Initialization Logic
fixed formatting
2026-01-30 16:16:10 +01:00
Christian 093663c4e6 Merge pull request #1086 from qitechgmbh/1085-change-heater-power-for-extruder-v3
Update heater power to the real values for extruder v3
2026-01-30 15:14:59 +01:00
Christian 1b2d6df459 Merge pull request #967 from qitechgmbh/Hamids-Branch
BECKHOFF minimal example motor
2026-01-30 13:24:54 +01:00
Christian cd2e15cb29 Update heater power to the real values for extruder v3 2026-01-30 13:23:46 +01:00
Christian f1571c0daa Merge pull request #1082 from qitechgmbh/revert-1077-1076-minimum-hardware-documentation-links
Revert "docs(readme): add documentation links and update machine table"
2026-01-29 14:50:33 +01:00
Christian 96e0fb83ef Revert "docs(readme): add documentation links and update machine table" 2026-01-29 14:50:10 +01:00
Christian 94cffbf717 Merge pull request #1077 from qitechgmbh/1076-minimum-hardware-documentation-links
docs(readme): add documentation links and update machine table
2026-01-29 13:22:14 +01:00
EinsPommes 38b2e907f9 docs(readme): update documentation links and improve machine table clarity 2026-01-28 17:16:10 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 7e5498751a Merge branch 'master' into 1076-minimum-hardware-documentation-links 2026-01-28 17:13:52 +01:00
Christian eef9d63de2 Merge pull request #1074 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-c5fe2c841a
Bump seroval and solid-js in /electron
2026-01-27 11:00:51 +01:00
Christian d9d9b9d344 update hash 2026-01-27 10:47:20 +01:00
Christian 2ff3d8df56 remove deps hash 2026-01-27 10:43:38 +01:00
Christian 462672bb6a Add peer dependency flag to multiple packages in package-lock.json 2026-01-27 10:38:58 +01:00
dependabot[bot] 84d539a4fa Bump seroval and solid-js in /electron
Bumps [seroval](https://github.com/lxsmnsyc/seroval) and [solid-js](https://github.com/solidjs/solid). These dependencies needed to be updated together.

Updates `seroval` from 1.3.2 to 1.5.0
- [Release notes](https://github.com/lxsmnsyc/seroval/releases)
- [Commits](https://github.com/lxsmnsyc/seroval/commits)

Updates `solid-js` from 1.9.10 to 1.9.11
- [Release notes](https://github.com/solidjs/solid/releases)
- [Changelog](https://github.com/solidjs/solid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/solidjs/solid/commits)

---
updated-dependencies:
- dependency-name: seroval
  dependency-version: 1.5.0
  dependency-type: indirect
- dependency-name: solid-js
  dependency-version: 1.9.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 10:38:58 +01:00
EinsPommes 6a6c09e283 docs(readme): add documentation links and update machine table 2026-01-25 22:03:53 +01:00
Oshgnacknak 61b9ac8a39 Merge branch 'master' into Hamids-Branch 2026-01-23 16:14:22 +01:00
Christian bb169e3b33 Fix formatting in registry.rs by removing extra blank line 2026-01-22 14:11:19 +01:00
Christian 3d735f8abd Merge branch 'master' into Hamids-Branch 2026-01-22 13:48:58 +01:00
Christian a9a973e336 Merge branch 'master' into Hamids-Branch 2026-01-21 17:11:58 +01:00
Christian b66a59dba9 Fix naming 2026-01-21 14:33:18 +01:00
Christian 9dbc962dbc Update file paths 2026-01-21 14:33:18 +01:00
Christian 93e2558803 Translate comments from German to English 2026-01-21 14:33:18 +01:00
Christian 8cf7480833 Fix path in routes.tsx 2026-01-21 14:33:18 +01:00
Christian 08d655effc Fix naming and folder structure 2026-01-21 14:33:18 +01:00
Christian c1c867fca1 Change minimal motor readme for current implementation 2026-01-21 14:33:18 +01:00
Hamid-el 1378c00e1a added .idea to .gitignore 2026-01-21 14:33:18 +01:00
Hamid-el 85f17befbe removed .idea 2026-01-21 14:33:18 +01:00
Hamid-el f2e764f93a finished stepper motor + doc finished (real version3) 2026-01-21 14:33:18 +01:00
Hamid-el 9adeeda490 finished stepper motor + doc finished (real version2) 2026-01-21 14:33:18 +01:00
Hamid-el 68bf075172 finished stepper motor + doc finished (real version) 2026-01-21 14:33:18 +01:00
Hamid-el 7d23190939 stepper motor doc finished 2026-01-21 14:33:18 +01:00
Hamid-el 5c68e2c78a finished stepper motor 2026-01-21 14:33:18 +01:00
Hamid-el 980e14a96e stepper motor with ethercat beckhoff Ek1100 and EL7031 0030 + UI works 2026-01-21 14:33:18 +01:00
Hamid-el d5fb45e4e3 stepper motor with ethercat beckhoff Ek1100 and EL7031 works 2026-01-21 14:33:18 +01:00
Hamid-el cdb7ab1e0c small adjustment 2026-01-21 14:33:18 +01:00
Hamid-el da934e0bb3 first version of manually running a stepper motor with ethercat beckhoff Ek1100 and EL7031
# Conflicts:
#	machines/src/aquapath1/act.rs
#	machines/src/registry.rs
#	server/src/ethercat/setup.rs
2026-01-21 14:33:18 +01:00
107 changed files with 4046 additions and 834 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
+40
View File
@@ -0,0 +1,40 @@
---
name: Bug report
about: Create a report to help us improve
title: 'BUG: '
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Navigate to '...'
2. Select/Press '....'
3. Execute operation '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/Photos**
If applicable, add screenshots from the HMI display or photos of the machine screen to help explain your problem.
**Machine Information (please complete the following):**
- Machine Type: [Winder2, Laser, Aquapath...]
- Control Software Version: [e.g. 2.4.1]
- Settings: [e.g. Different Gear Ratios, PID Settings, …]
**Device Information (if issue occurs on external device):**
- Device Type: [e.g. Tablet, PLC, Remote Control Panel]
- OS: [e.g. Windows 10, Linux, etc.]
- Connection Type: [e.g. Ethernet, USB]
**Error Messages/Codes**
If applicable, provide exact error codes or messages displayed or found in the troubleshooting terminal.
**Additional context**
Add any other context about the problem here (e.g., recent software updates, environmental factors).
+33
View File
@@ -0,0 +1,33 @@
---
name: Feature request
about: Suggest an idea for this project
title: 'Feat: '
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. During operation, I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions, workarounds, or features you've considered.
**Use Case/Application:**
- Machine Type(s): [e.g. All machines, Winder, Aquapath, ...]
- Frequency of Use: [e.g. Daily, During setup, Occasionally]
**Expected Benefits:**
- Efficiency Improvement: [e.g. Reduces setup time by X minutes, Eliminates manual steps]
- Safety Enhancement: [e.g. Prevents operator error, Adds safety checks]
- Production Impact: [e.g. Increases throughput, Reduces downtime, Improves quality]
- User Experience: [e.g. Simplifies workflow, Reduces training time]
**Mockups/Sketches**
If applicable, add sketches, mockups, or photos showing how the feature would appear on the HMI display or integrate with the machine.
**Additional context**
Add any other context, industry standards, or examples from other control systems about the feature request here.
+213
View File
@@ -0,0 +1,213 @@
name: Stale Assigned Issue Check
on:
schedule:
# Run every Sunday at 9:00 AM UTC
- cron: '0 9 * * 0'
workflow_dispatch: # Allow manual triggering for testing
jobs:
check-stale-issues:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Check assigned issues for status updates
uses: actions/github-script@v7
with:
script: |
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const now = new Date();
// Helper function to check rate limits
async function checkRateLimit() {
try {
const { data: rateLimit } = await github.rest.rateLimit.get();
const remaining = rateLimit.resources.core.remaining;
const reset = new Date(rateLimit.resources.core.reset * 1000);
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
return remaining > 10; // Stop if fewer than 10 requests remaining
} catch (error) {
console.log(`Could not check rate limit: ${error.message}`);
return true; // Continue on error
}
}
// Get all open issues
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
console.log(`Found ${issues.length} open issues`);
// Helper function to check if issue has linked PRs using GraphQL
async function hasLinkedPRs(issueNumber) {
try {
const query = `
query($owner: String!, $repo: String!, $issueNumber: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
nodes {
... on ConnectedEvent {
subject {
... on PullRequest {
number
state
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: issueNumber
});
const linkedPRs = result.repository.issue.timelineItems.nodes
.filter(node => node.subject && node.subject.state === 'OPEN')
.map(node => node.subject.number);
return linkedPRs.length > 0;
} catch (error) {
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
return false; // Assume no linked PRs on error to avoid false positives
}
}
let processedCount = 0;
let commentedCount = 0;
let errorCount = 0;
for (const issue of issues) {
try {
// Check rate limit periodically
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
break;
}
// Skip pull requests (they appear in issues list too)
if (issue.pull_request) {
continue;
}
// Skip if not assigned
if (!issue.assignees || issue.assignees.length === 0) {
console.log(`Issue #${issue.number}: Not assigned, skipping`);
continue;
}
console.log(`Checking issue #${issue.number}: ${issue.title}`);
processedCount++;
// Check if issue has linked PRs
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
if (hasOpenLinkedPR) {
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
continue;
}
// Get all comments on the issue
let comments = [];
try {
comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
errorCount++;
continue;
}
// Find the most recent comment or update
let lastActivity = new Date(issue.created_at);
if (issue.updated_at) {
lastActivity = new Date(issue.updated_at);
}
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
const lastCommentDate = new Date(lastComment.created_at);
if (lastCommentDate > lastActivity) {
lastActivity = lastCommentDate;
}
}
// Check if last activity was more than a week ago
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
if (daysSinceActivity >= 7) {
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
// Check if we already posted a status request in the last 7 days
const recentComments = comments.filter(comment => {
const commentDate = new Date(comment.created_at);
return (now - commentDate) < ONE_WEEK_MS;
});
const alreadyAsked = recentComments.some(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('status update')
);
if (alreadyAsked) {
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
continue;
}
// Post a status request comment
try {
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Hi ${assigneeNames}! 👋\n\n` +
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
`Could you please provide a status update? If you're:\n` +
`- ✅ Working on it: Let us know your progress\n` +
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
`Thank you!`
});
console.log(`Issue #${issue.number}: Posted status request comment`);
commentedCount++;
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
errorCount++;
// Continue to next issue
}
} else {
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
}
} catch (error) {
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
errorCount++;
// Continue to next issue instead of failing the entire workflow
continue;
}
}
console.log(`\nStale issue check complete!`);
console.log(`Processed: ${processedCount} issues`);
console.log(`Commented on: ${commentedCount} issues`);
console.log(`Errors: ${errorCount}`);
+80
View File
@@ -0,0 +1,80 @@
name: Update npm deps hash
on:
pull_request:
paths:
- 'electron/package-lock.json'
- 'electron/package.json'
workflow_dispatch:
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
permissions:
contents: write
pull-requests: write
actions: write
jobs:
update-hash:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v34
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Calculate new npm deps hash
id: calc-hash
run: |
# Calculate the hash using nix run with nixpkgs
NEW_HASH=$(nix run 'nixpkgs#prefetch-npm-deps' electron/package-lock.json)
echo "hash=$NEW_HASH" >> $GITHUB_OUTPUT
echo "Calculated hash: $NEW_HASH"
- name: Update npmDepsHash in electron.nix
run: |
NEW_HASH="${{ steps.calc-hash.outputs.hash }}"
sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$NEW_HASH\";|" nixos/packages/electron.nix
- name: Check if hash changed
id: check-changes
run: |
if git diff --quiet nixos/packages/electron.nix; then
echo "changed=false" >> $GITHUB_OUTPUT
echo "No changes needed - hash is already correct"
else
echo "changed=true" >> $GITHUB_OUTPUT
echo "Hash updated in electron.nix"
fi
- name: Commit and push if changed
if: steps.check-changes.outputs.changed == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add nixos/packages/electron.nix
git commit -m "chore: update npmDepsHash to ${{ steps.calc-hash.outputs.hash }}"
git push
- name: Comment on PR
if: steps.check-changes.outputs.changed == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Automatically updated `npmDepsHash` to `${{ steps.calc-hash.outputs.hash }}`\n\n**Next step:** Trigger the checks by pushing an empty commit:\n```bash\ngit commit --allow-empty -m "trigger ci" && git push\n```'
})
+2 -1
View File
@@ -7,4 +7,5 @@ build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
compile_metrics.csv
.idea/
View File
Whitespace-only changes.
Generated
+50 -40
View File
@@ -204,7 +204,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -221,7 +221,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -232,9 +232,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.8.7"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"axum-macros",
@@ -291,7 +291,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -390,9 +390,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -539,7 +539,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -682,7 +682,7 @@ dependencies = [
"deluxe-macros",
"once_cell",
"proc-macro2",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -695,7 +695,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -710,7 +710,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -721,7 +721,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -962,14 +962,13 @@ dependencies = [
"deluxe",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
name = "ethercrab"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb13becfa15713adb09b27fdb74468b827cdc78aa99aea638a89c1b0db3ce94"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
dependencies = [
"async-io",
"atomic-waker",
@@ -998,8 +997,7 @@ dependencies = [
[[package]]
name = "ethercrab-wire"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15828cb020fb44c92ca67e1ee3c122c9621c2e00d383d066e488f0619023ba2"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
dependencies = [
"ethercrab-wire-derive",
"heapless",
@@ -1008,20 +1006,19 @@ dependencies = [
[[package]]
name = "ethercrab-wire-derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e790b8011211ed70babfcacbd4277b91ee21402ad5ba6a2c293db02e2f12919"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
dependencies = [
"criterion",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
name = "euclid"
version = "0.22.11"
version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63"
dependencies = [
"num-traits",
]
@@ -1878,7 +1875,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -1908,7 +1905,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -2221,7 +2218,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -2251,20 +2248,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -2332,6 +2329,7 @@ dependencies = [
"smol",
"socketioxide",
"spin_sleep",
"ta",
"textplots",
"tokio",
"tower-http",
@@ -2573,9 +2571,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.111"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -2588,6 +2586,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "ta"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "609409d472a0a7d8d4dd9e19891bbdef546b9dce670c3057d0e02192dc541226"
[[package]]
name = "tap"
version = "1.0.1"
@@ -2621,7 +2625,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -2681,7 +2685,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -2843,7 +2847,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -2880,9 +2884,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"chrono",
"matchers",
@@ -3074,7 +3078,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
"wasm-bindgen-shared",
]
@@ -3139,7 +3143,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -3150,7 +3154,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
@@ -3426,5 +3430,11 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.114",
]
[[package]]
name = "zmij"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
+51 -84
View File
@@ -2,109 +2,76 @@
# QiTech Control
QiTech Control is an open-source framework designed to bring modern software paradigms to certified industrial hardware.
QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
It frees developers from proprietary, license-heavy PLC ecosystems and rigid "point-and-click" workflows that are no longer adequate for today's complex automation challenges.
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
## Documentation
[![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
- [Getting Started](https://github.com/qitechgmbh/control/wiki/Getting-Started)
- [Architecture Overview](https://github.com/qitechgmbh/control/wiki/Architecture-Overview)
- [EtherCAT Basics](https://github.com/qitechgmbh/control/wiki/Ethercat-Basics)
- [Device Examples](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)
- [REST API Reference](https://github.com/qitechgmbh/control/wiki/Rest-Api)
# Repo Structure
## Videos
**Frontend**
| Software Demo | Full Explainer |
|---------------|----------------|
| [![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Watch a demo of the control software in action* | [![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)<br/>*Watch a complete overview of QiTech Control* |
- `/electron`: Frontend code for the control software built with React and Electron.
## Repository Structure
**Backend**
- **`/electron`** - React + Electron frontend
- **`/server`** - Rust backend implementing machine logic
- **`/ethercat-hal`** - Hardware abstraction layer for EtherCAT devices
- **`/control-core`** - Core control logic
- **`/nixos`** - Custom Linux OS with realtime kernel
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
- `/control-core`: Core control logic for the server.
## Technology Stack
**Operating System**
**Backend:** Rust with [Ethercrab](https://github.com/ethercrab-rs/ethercrab) for EtherCAT, [SocketIO](https://socket.io/) for real-time communication, [axum](https://docs.rs/axum/latest/axum/) for REST API
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
**Other**
- `/docs`: Documentation for the project.
# Technology Choices
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master software. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
We use [Smol](https://github.com/smol-rs/smol) for EtherCAT IO in the control loop for it's performance and [Tokio](https://tokio.rs/) for server IO because of it's ecosystem and maturity.
## Frontend
We could combine the code of the frontend and backend using [Doxius](https://dioxuslabs.com/) but it lacks good Linux support. We chose [Electron](https://www.electronjs.org/) with [React](https://react.dev/) for it's maturity and ecosystem. For the UI we use [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) for styling. For routing we use [TanStack Router](https://tanstack.com/router/v1).
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
[API Documentation](https://github.com/qitechgmbh/control/blob/master/docs/rest-api.md)
-> Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
- `rust-analyzer` extension for VSCode
- Set your interface in `server/src/ethercat/init.rs` like `en10`
- Connect a Beckhoff EK1100 to your interface
- run `cd server && cargo run` to start the server (localhost:3001)
-> Frontend
- nodejs and npm installed
- run `cd electron && npm i && npm run start` to start the frontend
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
## Minimal Hardware Examples
## Hardware Examples
For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples).
QiTech Control supports a wide range of EtherCAT hardware from WAGO and Beckhoff. We provide complete step-by-step tutorials with wiring diagrams and software configuration for many common modules.
| Example | Vendor | Hardware | Description | How-To Video | Docs |
| :----------------- | :------- | :------------ | :-------------------------------------- | :----------- | :---------------------------- |
| **Digital Input** | WAGO | 750-402 | 4-Channel Digital Input | [ ] | - |
| **Digital Output** | WAGO | 750-753 | 8-Channel Digital Output | [ ] | - |
| **Analog Input** | WAGO | 750-455 | 4-Channel Analog Input | [ ] | - |
| **Analog Output** | WAGO | 750-553 | 4-Channel Analog Output | [ ] | - |
| **Serial Comms** | WAGO | 750-652 | RS-485 Modbus Module | [ ] | - |
| **Stepper Drive** | WAGO | 750-67x | Stepper Controller | [ ] | - |
| **Power Supply** | WAGO | 2787-214 | Power Supply Unit | [ ] | - |
| **LED Control** | Beckhoff | EL2004 | Digital output control (simplest setup) | [ ] | [](./docs/hardware/el2004.md) |
| **Analog Input** | Beckhoff | EL3021 | Reading analog current measurements | [ ] | [](./docs/hardware/el3021.md) |
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
# QiTech Machines
Quick links to popular examples:
- [EL2004 - LED Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El2004) (simplest setup for beginners)
- [EL3021 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El3021)
- [EL7031 - Stepper Motor Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El7031-Motor)
- [WAGO 750-402 - Digital Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago750-402)
- [WAGO 750-455 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago-750-455)
| Machine Name | Description | Machine ID | Implemented | Video | Docs |
| :---------------- | :-------------------------- | :--------- | :---------- | :-------------------------------------------------------------- | :------------------------------ |
| Winder V1 | Winding Filaments & Similar | 1 | Legacy | [Video](https://youtu.be/4aE4cFhioKA?si=Xdj_LVnFrAYWnLm6) | - |
| Winder V2 | Winding Filaments & Similar | 2 | Yes | [Video](https://youtu.be/f2kzh6kpQWE?si=HEYcLOaC9gWSp2Wo) | [](./docs/machines/winder-1.md) |
| XL Winder V1 | Large Scale Winder | 3 | Yes | [Video](https://youtu.be/ynI6ioWIQQY?si=4EDVKXbgIqrvGGtY) | - |
| Buffer V1 | Filament buffering system | 4 | In Progress | [Video](https://youtu.be/VR5mEdZDPA0?si=eip7dQXU4zaXK7Ev) | - |
| Extruder V1 | Single Screw Extruder | 5 | Legacy | [Video](https://youtu.be/gchoG-yGczI?si=bEM4daf5eOVaU5_2) | - |
| Extruder V2 | Single Screw Extruder | 6 | Yes | [Video](https://youtu.be/mexRYGDNWa4?si=h7JBz_XKMwKLPenG) | |
| Waterway V1 | Filament Water Cooling | 7 | In Progress | [Video](https://youtu.be/_T5z1J8bl_k?si=uOC1hIQ1EAP0cHIF) | |
| Laser V1 | Diameter Measuring Laser | 8 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | |
| 2-Axis-Laser V1 | Dual Axis Laser | 9 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | - |
| Mock | Mock Machine for Testing | 10 | Yes | | - |
| Extruder V3 | Single Screw Extruder | 11 | Yes | [Video](https://youtu.be/ipHHuPzCvn4?si=hkZ1b93rVuwDxhFD) | |
| Mini Schredder V1 | mini plastic crusher | 12 | Legacy | [Video](https://youtu.be/m8NplNqdu2Q?si=x6zjDktJtpbSMSmu) | - |
| Pro Schredder V1 | large plastic crusher | 13 | Legacy | [Video](https://youtu.be/pSaVMqp06pU?si=y61enutRpxKscBm4) | - |
| Dryer V1 | polymer dryer | 14 | Reserved | [Video](https://youtu.be/6hdmUUAdZp0?si=eKOW1WlgkqTpnfdS) | - |
| Pelletizier V1 | Filament chopper | 15 | Reserved | | - |
## QiTech Machines
# Current Restructuring Efforts
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
[![](https://img.youtube.com/vi/UlbnSVIhfLI/maxresdefault.jpg)](https://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
*Video: Live stream of software dev meeting*
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
## Contributing
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
## Additional Videos
| Episode Highlights | |
|--------------------|--------------------|
| [![](https://img.youtube.com/vi/U2wNN6sF4to/maxresdefault.jpg)](https://www.youtube.com/watch?v=U2wNN6sF4to&t=163s)<br/>**Day in the Software Team**<br/>*Behind the scenes with Robin and how the team ships control software.* | [![](https://img.youtube.com/vi/R5WGAY3WRWA/maxresdefault.jpg)](https://www.youtube.com/watch?v=R5WGAY3WRWA&t=1s)<br/>**Software Stack Day**<br/>*A fast tour through the tools, stack, and daily workflow.* |
| [![](https://img.youtube.com/vi/xyaHwxXguw4/maxresdefault.jpg)](https://www.youtube.com/watch?v=xyaHwxXguw4&t=32s)<br/>**First Client**<br/>*A milestone week building the first real client project.* | [![](https://img.youtube.com/vi/moI7z2PVdF4/maxresdefault.jpg)](https://www.youtube.com/watch?v=moI7z2PVdF4)<br/>**Steelworks Week**<br/>*A deep-dive week on a demanding steelworks order.* |
## License
See [LICENSE](LICENSE) file for details.
+2 -2
View File
@@ -9,11 +9,11 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
axum = "0.8.6"
axum = "0.8.8"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = "0.6"
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
interfaces = "0.0.9"
serde = "1.0.219"
serialport = "4.7.3"
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

+1
View File
@@ -89,6 +89,7 @@ To get started with actual hardware, check out these step-by-step tutorials:
- **[LED Control with EL2004](./minimal-example-el2004.md)** - Digital output control, the simplest possible hardware setup
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
- **[Stepper Motor Control with EL7031-0030](./minimal-example-el7031-motor.md)** - Complete motor control integration with velocity control
These examples provide complete hardware wiring diagrams and software setup instructions.
@@ -0,0 +1,236 @@
# Minimal Example — Beckhoff EL7031-0030 Stepper Motor Integration
A complete hardware + software walkthrough
---
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
- [3.2 EK1100 Wiring](#32-ek1100-wiring)
- [3.2.1 Safe Wiring Procedure](#321-safe-wiring-procedure-beckhoff-recommended)
- [3.2.2 Wiring (EL7031-0030)](#322-wiring-el7031-0030)
- [3.3 Safety Warning](#33--safety-warning)
4. [Software Setup](#4-software-setup)
- [4.1 Installing on Linux](#41-installing-on-linux-this-depends-on-your-distro)
- [4.2 Running the Backend](#42-running-the-backend)
- [4.3 Running the Frontend](#43-running-the-frontend)
5. [Demo](#5-demo)
- [5.1 Assigning Devices in the Dashboard](#51-assigning-devices-in-the-dashboard)
6. [Documentation](#6-documentation)
7. [Software Architecture](#7-software-architecture)
---
## 1. Introduction
This project documents the successful integration of a **Beckhoff EL7031-0030 Stepper Motor Terminal** into the QiTech control software stack.
The goal was to control a 24V stepper motor via EtherCAT using a custom Rust backend and a React/Electron frontend.
---
## 2. Requirements
### Software
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
### Hardware
- **EtherCAT Master:** Linux PC
- **Bus Coupler:** EtherCAT Beckhoff EK1100
- **Stepper Driver:** Beckhoff EL7031-0030 (THIS IS DIFFERENT FROM EL7031)
- **Motor:** Standard 4-wire Stepper Motor
- **Power Supply:** 24V DC
- **Ethernet Cable:** Standard Ethernet cable
- **Wiring Tools:** Screwdriver, wires
---
## 3. Hardware Setup
### 3.2 EK1100 Wiring
This wiring configuration powers the EL7031-0030.
It is not the only possible wiring but is the **simplest functional setup**.
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
---
#### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](../assets/wiring.png)
---
We supply power using a **DC hollow-plug adapter**, like this one:
https://www.amazon.de/dp/B093FTFZ8Q
Perform the following wiring on the EK1100:
1. Red wire **(+24 V)** → Terminal **2**
2. Black wire **(0 V)** → Terminal **3**
3. Jumper wire from **Terminal 1 → Terminal 6**
4. Jumper wire from **Terminal 5 → Terminal 7**
After wiring, your module should look like **Figure 1**.
---
#### **Figure 1 — EK1100 Minimal Wiring**
<img src="../assets/ek1100.jpeg" width="400">
---
#### **Figure 2 — EL7031-0030 Terminal**
<img src="../assets/EL7031_0030.jpg" width="300">
Slide the EL7031-0030 onto the right side of the EK1100 until it locks.
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
---
#### **Figure 3 — Motor**
<img src="../assets/motor.jpg" width="300">
---
#### **Figure 4 — Motor Wiring**
<img src="../assets/motor_wiring.jpg" width="300">
Now the motor is wired via the pins 4, 5, 12, 13 on the EL7031-0030.
---
#### **Figure 5 — EL7031 Integration Connected**
<img src="../assets/EL7031_0030_connected.jpg" width="400">
That's what the pin should look like.
---
### 3.2.2 Wiring (EL7031-0030)
**Crucial:** The EL7031 requires two power sources: E-Bus (side contacts) for logic, and Front Terminal (6 & 14) for motor power.
| Terminal Point | Function | Cable Color (Example) |
| :------------- | :--------------- | :-------------------- |
| **4** | Motor Coil A1 | Red |
| **12** | Motor Coil A2 | Blue |
| **5** | Motor Coil B1 | Green |
| **13** | Motor Coil B2 | Black |
| **6** | **Power +24V** | PSU Red (+) |
| **14** | **Power 0V/GND** | PSU Black (-) |
---
### 3.3 ⚠️ Safety Warning
- **Mandatory Power Supply:** Without 24V connected to **Pin 6 (+24V)** and **Pin 14 (GND)**, the terminal will remain in `PREOP` state or show a "Warning" LED (No Power). The motor will **not** move without this external supply.
#### Risk of Destruction (Short Circuit):
- **Never** connect the 24V Power Supply to the Motor Output pins (**4, 5, 12, 13**). This will instantly destroy the terminal (causing pins 12 & 13 to glow red/burn).
- Ensure strict separation: **Pins 4/5/12/13 are for the MOTOR ONLY**.
- **Pins 6/14 are for POWER ONLY**.
Also, see the documentation of the EtherCAT terminal ([EL7031-0030](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)) for the different power modes (page 46 ff.)
---
## 4. Software Setup
### 4.1 Installing on Linux (this depends on your distro)
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
### 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
### 4.3 Running the Frontend
```bash
# move to the control directory
cd electron
npm run start
```
This launches the QiTech Control dashboard.
---
## 5. Demo
### 5.1 Assigning Devices in the Dashboard
Once the backend + frontend are running, you should see:
![](../assets/UI_start_motor.png)
Make sure that under "Assign" → "Machine Assignment"
the correct serial number is selected (each device should have the same one, here it is 1).
Under "Machine" select your new Machine however you named it (I named it "TestMotor V1"), else it will detect that there is something connected to it but the connection won't work.
![](../assets/machine_assigment_motor.png)
**NOW THE MOTOR SHOULD TURN!**
In the interface "TestMotor" on the left side, you can now control the motor (its state and its speed).
![](../assets/UI_motor.png)
---
## 6. Documentation
Use the official documentation of the EL7031-0030 for more information:
[Beckhoff EL7031-0030 Documentation](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)
---
## 7. Software Architecture
### Backend (Rust)
Located in [machines/src/motor_test_machine/](/machines/src/motor_test_machine/).
1. **`mod.rs`**: Defines the `MotorTestMachine` struct and holds the state (driver wrapper, enabled state, target velocity).
2. **`api.rs`**: Handles incoming JSON commands from the frontend (Enable/Disable, Set Velocity) via WebSockets/SocketIO.
3. **`act.rs`**: The real-time control loop. It updates the `StepperVelocityEL70x1` driver wrapper in every cycle based on the current state.
4. **`new.rs`**: Initializes the hardware.
### Frontend (TypeScript/React)
Located in [electron/src/machines/motor_test_machine/](/electron/src/machines/motor_test_machine/) and [electron/src/routes/routes.tsx](/electron/src/routes/routes.tsx).
1. **`useTestMotor.ts`**: Custom hook managing the optimistic state and communication with the backend.
2. **`TestMotorControlPage.tsx`**: The UI using QiTech UI components (`ControlCard`, `EditValue`, `SelectionGroupBoolean`) to match the look and feel of the Winder2.
3. **Routing**: Integrated into `routes.tsx` using TanStack Router, ensuring the machine appears in the sidebar and navigation works correctly.
+177 -288
View File
@@ -1,12 +1,12 @@
{
"name": "qitech-control-electron",
"version": "1.0.0",
"version": "2.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qitech-control-electron",
"version": "1.0.0",
"version": "2.4.1",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
@@ -40,12 +40,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-i18next": "^16.5.4",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
"socket.io-client": "^4.8.3",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -54,7 +54,7 @@
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.8"
"zustand": "^5.0.11"
},
"devDependencies": {
"@electron/fuses": "^1.8.0",
@@ -73,7 +73,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -90,7 +90,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^3.0.6"
"vitest": "^4.0.18"
}
},
"node_modules/@adobe/css-tools": {
@@ -162,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -658,6 +659,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -681,6 +683,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1302,9 +1305,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2886,6 +2889,13 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
@@ -3280,6 +3290,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -3370,6 +3381,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -3590,8 +3602,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3757,6 +3768,7 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3773,6 +3785,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3783,6 +3796,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3870,6 +3884,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -4108,39 +4123,40 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
"@vitest/spy": "4.0.18",
"@vitest/utils": "4.0.18",
"chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.2.4",
"@vitest/spy": "4.0.18",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
"vite": "^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
@@ -4152,42 +4168,41 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^2.0.0"
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.2.4",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
"@vitest/utils": "4.0.18",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.4",
"magic-string": "^0.30.17",
"@vitest/pretty-format": "4.0.18",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
@@ -4195,28 +4210,24 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^4.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
"@vitest/pretty-format": "4.0.18",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4228,6 +4239,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4291,7 +4303,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4606,6 +4617,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4630,16 +4642,6 @@
"node": "*"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -4760,18 +4762,11 @@
}
},
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=18"
}
@@ -4833,16 +4828,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -4988,7 +4973,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -5124,16 +5110,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -5261,8 +5237,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5803,11 +5778,12 @@
}
},
"node_modules/eslint": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5815,7 +5791,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.2",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -5868,6 +5844,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5969,6 +5946,7 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6927,6 +6905,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -6960,6 +6939,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7562,6 +7542,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -8054,13 +8035,6 @@
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -8095,7 +8069,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9218,6 +9191,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9406,16 +9390,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -9515,6 +9489,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9631,7 +9606,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9647,7 +9621,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9737,6 +9710,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9746,6 +9720,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9758,6 +9733,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9770,16 +9746,17 @@
}
},
"node_modules/react-i18next": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz",
"integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==",
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 23.4.0",
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
@@ -9800,8 +9777,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -10311,18 +10287,19 @@
}
},
"node_modules/seroval": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz",
"integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.4.0.tgz",
"integrity": "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz",
"integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -10493,13 +10470,13 @@
"license": "ISC"
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
@@ -10507,23 +10484,6 @@
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-msgpack-parser": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-3.0.2.tgz",
@@ -10565,38 +10525,15 @@
}
},
"node_modules/solid-js": {
"version": "1.9.10",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
"version": "1.9.11",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz",
"integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "~1.3.0",
"seroval-plugins": "~1.3.0"
}
},
"node_modules/solid-js/node_modules/seroval": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
},
"node_modules/solid-js/node_modules/seroval-plugins": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz",
"integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
"peerDependencies": {
"seroval": "^1.0"
"seroval": "~1.5.0",
"seroval-plugins": "~1.5.0"
}
},
"node_modules/sonner": {
@@ -10802,26 +10739,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/strip-literal/node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/style-to-js": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
@@ -10916,7 +10833,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -10961,11 +10879,14 @@
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
@@ -11005,6 +10926,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11012,30 +10934,10 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11268,6 +11170,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11604,6 +11507,7 @@
"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",
@@ -11673,29 +11577,6 @@
}
}
},
"node_modules/vite-node": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.1",
"es-module-lexer": "^1.7.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-plugin-electron": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.29.0.tgz",
@@ -11716,7 +11597,8 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
@@ -11754,6 +11636,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11762,51 +11645,50 @@
}
},
"node_modules/vitest": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
"@vitest/mocker": "3.2.4",
"@vitest/pretty-format": "^3.2.4",
"@vitest/runner": "3.2.4",
"@vitest/snapshot": "3.2.4",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
"@vitest/pretty-format": "4.0.18",
"@vitest/runner": "4.0.18",
"@vitest/snapshot": "4.0.18",
"@vitest/spy": "4.0.18",
"@vitest/utils": "4.0.18",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"picomatch": "^4.0.3",
"std-env": "^3.10.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.1",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.4",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3",
"vite": "^6.0.0 || ^7.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.4",
"@vitest/ui": "3.2.4",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.0.18",
"@vitest/browser-preview": "4.0.18",
"@vitest/browser-webdriverio": "4.0.18",
"@vitest/ui": "4.0.18",
"happy-dom": "*",
"jsdom": "*"
},
@@ -11814,13 +11696,19 @@
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/ui": {
@@ -12188,6 +12076,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -12205,9 +12094,9 @@
}
},
"node_modules/zustand": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
+7 -7
View File
@@ -1,8 +1,8 @@
{
"name": "qitech-control-electron",
"productName": "QiTech Control",
"version": "1.0.0",
"description": "",
"version": "2.4.1",
"description": "Next Generation Machine Interface ",
"main": ".vite/build/main.js",
"private": true,
"scripts": {
@@ -36,7 +36,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -53,7 +53,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^3.0.6"
"vitest": "^4.0.18"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@@ -88,12 +88,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-i18next": "^16.5.4",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
"socket.io-client": "^4.8.3",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -102,6 +102,6 @@
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.8"
"zustand": "^5.0.11"
}
}
+77 -4
View File
@@ -5,8 +5,23 @@ import {
AnimationRefs,
AnimationState,
BigGraphProps,
GraphLine,
SeriesData,
} from "./types";
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
function hasHistoricalDashedTargets(
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>,
): boolean {
return (
lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
export function useAnimationRefs(): AnimationRefs {
const animationFrameRef = useRef<number | null>(null);
@@ -24,12 +39,14 @@ export function useAnimationRefs(): AnimationRefs {
values: number[];
}>({ timestamps: [], values: [] });
const realPointsCountRef = useRef(0);
const targetLineCacheRef = useRef<Map<number, number[]>>(new Map());
return {
animationFrame: animationFrameRef,
animationState: animationStateRef,
lastRenderedData: lastRenderedDataRef,
realPointsCount: realPointsCountRef,
targetLineCache: targetLineCacheRef,
};
}
@@ -50,8 +67,12 @@ export function buildUPlotData(
values: number[],
realPointsCount: number | undefined,
realPointsCountRef: React.RefObject<number>,
config: { lines?: Array<{ show?: boolean; value: number }> },
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
allSeriesData?: number[][],
targetLineCache?: React.RefObject<Map<number, number[]>>,
freezeTargetTail: boolean = false,
): uPlot.AlignedData {
const uData: uPlot.AlignedData = [timestamps];
@@ -70,9 +91,55 @@ export function buildUPlotData(
}
// Add config lines
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
uData.push(timestamps.map(() => line.value));
let lineData: number[];
if (line.targetSeries && targetLineCache) {
// Target line with history - use cache to prevent wiggle
const cached = targetLineCache.current.get(lineIndex);
if (cached && cached.length <= timestamps.length) {
// Reuse cached data for existing points, extend with current value
const extension = timestamps.length - cached.length;
if (extension > 0) {
const stableTailValue = cached[cached.length - 1] ?? line.value;
lineData = [
...cached,
...new Array(extension).fill(
freezeTargetTail ? stableTailValue : line.value,
),
];
} else {
lineData = [...cached];
}
if (!freezeTargetTail) {
targetLineCache.current.set(lineIndex, lineData);
}
} else {
// Full recalculation (first time or chart was rebuilt)
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
targetLineCache.current.set(lineIndex, lineData);
}
} else if (line.targetSeries) {
// targetSeries but no cache available - full calculation
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
} else {
// Constant value line (original behavior)
lineData = timestamps.map(() => line.value);
}
uData.push(lineData);
lineIndex++;
}
});
@@ -88,8 +155,10 @@ export function animateNewPoint(
viewMode: string,
selectedTimeWindow: number | "all",
startTimeRef: React.RefObject<number | null>,
config: { lines?: Array<{ show?: boolean; value: number }> },
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
updateYAxisScale: (xMin?: number, xMax?: number) => void,
getAllSeriesData?: () => number[][],
): void {
if (targetData.timestamps.length <= currentData.timestamps.length) {
@@ -123,6 +192,8 @@ export function animateNewPoint(
targetIndex: newIndex,
};
const freezeTargetTail = hasHistoricalDashedTargets(config.lines);
const animate = (currentTime: number) => {
if (
!uplotRef.current ||
@@ -173,6 +244,8 @@ export function animateNewPoint(
refs.realPointsCount,
config,
allSeriesData,
refs.targetLineCache,
freezeTargetTail,
);
uplotRef.current.setData(animatedUData);
+130 -5
View File
@@ -1,5 +1,9 @@
import uPlot from "uplot";
import { seriesToUPlotData, TimeSeries } from "@/lib/timeseries";
import {
seriesToUPlotData,
TimeSeries,
alignTargetSeriesToTimestamps,
} from "@/lib/timeseries";
import {
createEventHandlers,
attachEventHandlers,
@@ -8,6 +12,13 @@ import {
import { BigGraphProps, CreateChartParams } from "./types";
import { normalizeDataSeries } from "./animation";
type CustomDashedTargetLine = {
dataIndex: number;
dash: number[];
color: string;
width: number;
};
export function createChart({
containerRef,
uplotRef,
@@ -56,11 +67,28 @@ export function createChart({
uPlotData.push(values as any);
});
// Add config lines data
// Add config lines data and populate target line cache
animationRefs.targetLineCache.current.clear();
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = new Array(timestamps.length).fill(line.value);
let lineData: number[];
if (line.targetSeries) {
// Use historical target values aligned with data timestamps
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
// Cache the computed target line data so subsequent updates
// can extend it instead of recalculating (prevents wiggle)
animationRefs.targetLineCache.current.set(lineIndex, [...lineData]);
} else {
// Use constant value (original behavior)
lineData = new Array(timestamps.length).fill(line.value);
}
uPlotData.push(lineData as any);
lineIndex++;
}
});
@@ -142,6 +170,7 @@ export function createChart({
// Build series configuration for ALL series (but control visibility)
const seriesConfig: uPlot.Series[] = [{ label: "Time" }];
const customDashedTargetLines: CustomDashedTargetLine[] = [];
// Add ALL data series with visibility control
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
@@ -167,16 +196,35 @@ export function createChart({
});
// Add config lines
let visibleLineIndex = 0;
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const lineDataIndex = firstConfigLineDataIndex + visibleLineIndex;
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
customDashedTargetLines.push({
dataIndex: lineDataIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
seriesConfig.push({
label: line.label,
stroke: line.color,
// Historical dashed targets are custom rendered in draw hook to avoid
// canvas dash shimmer during smooth live updates.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
width: line.width ?? 1,
dash: line.dash ?? (line.type === "threshold" ? [5, 5] : undefined),
dash: isHistoricalDashedTarget ? undefined : dash,
show: true,
points: { show: false },
});
visibleLineIndex++;
}
});
@@ -306,6 +354,83 @@ export function createChart({
}
},
],
draw: [
(u) => {
if (customDashedTargetLines.length === 0) return;
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length === 0) return;
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xMin === undefined || xMax === undefined) return;
const pxPerX = u.bbox.width / Math.max(xMax - xMin, 1e-9);
const ctx = u.ctx;
const left = u.bbox.left;
const top = u.bbox.top;
const right = left + u.bbox.width;
const bottom = top + u.bbox.height;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, u.bbox.width, u.bbox.height);
ctx.clip();
ctx.lineCap = "butt";
customDashedTargetLines.forEach(
({ dataIndex, dash, color, width }) => {
const yData = u.data[dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length === 0) return;
const dashPeriod = dash.reduce((acc, curr) => acc + curr, 0);
ctx.setLineDash(dash);
// Anchor dash phase to world-X so dashes remain stable while axis slides.
ctx.lineDashOffset =
dashPeriod > 0 ? -((xMin * pxPerX) % dashPeriod) : 0;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = 0; i < xData.length; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = u.valToPos(xData[i], "x", true);
const y = u.valToPos(value, "y", true);
if (x < left || x > right || y < top || y > bottom) continue;
if (!started) {
ctx.moveTo(x, y);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
ctx.lineTo(x, prevY);
}
if (y !== prevY) {
ctx.lineTo(x, y);
}
prevX = x;
prevY = y;
}
if (started) {
ctx.stroke();
}
},
);
ctx.restore();
},
],
},
scales: {
x: {
+25 -3
View File
@@ -14,6 +14,17 @@ import {
AnimationRefs,
} from "./types";
function hasHistoricalDashedTargets(config: GraphConfig): boolean {
return (
config.lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
// Retrieve all series data as an array of number arrays
function getAllSeriesData(data: BigGraphProps["newData"]): number[][] {
const normalized = normalizeDataSeries(data);
@@ -77,10 +88,16 @@ export function useLiveMode({
return;
try {
const hasDashedHistoricalTargets = hasHistoricalDashedTargets(config);
const [timestamps, values] = seriesToUPlotData(primaryData.long);
const cur = primaryData.current;
const liveTimestamps = [...timestamps, cur.timestamp];
const liveValues = [...values, cur.value];
const lastHistoricalTimestamp =
timestamps[timestamps.length - 1] ?? cur.timestamp;
const shouldAppendCurrent = cur.timestamp > lastHistoricalTimestamp;
const liveTimestamps = shouldAppendCurrent
? [...timestamps, cur.timestamp]
: timestamps;
const liveValues = shouldAppendCurrent ? [...values, cur.value] : values;
const allSeriesValues = getAllSeriesData(newData);
const normalized = normalizeDataSeries(newData);
@@ -96,7 +113,9 @@ export function useLiveMode({
(seriesValues.length > 0
? seriesValues[seriesValues.length - 1]
: 0);
return [...seriesValues, currentValue];
return shouldAppendCurrent
? [...seriesValues, currentValue]
: seriesValues;
})
: [];
@@ -107,6 +126,8 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
hasDashedHistoricalTargets,
);
uplotRef.current.setData(liveData);
@@ -283,6 +304,7 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
);
uplotRef.current.setData(uData);
+4
View File
@@ -26,7 +26,10 @@ export type PropGraphSync = {
// Configuration types for additional lines
export type GraphLine = {
type: "threshold" | "target";
/** Current value (used as fallback and for Y-axis scaling) */
value: number;
/** Optional TimeSeries for historical target values. When provided, the line shows the target history over time instead of a constant horizontal line */
targetSeries?: TimeSeries;
color: string;
label?: string;
width?: number;
@@ -158,6 +161,7 @@ export interface AnimationRefs {
values: number[];
}>;
realPointsCount: React.RefObject<number>;
targetLineCache: React.RefObject<Map<number, number[]>>;
}
export interface HandlerRefs {
@@ -4,7 +4,11 @@ import uPlot from "uplot";
import { seriesToUPlotData } from "@/lib/timeseries";
import { BigGraphProps, SeriesData, AnimationRefs, HandlerRefs } from "./types";
import { GraphExportData } from "./excelExport";
import { getPrimarySeries, stopAnimations } from "./animation";
import {
getPrimarySeries,
stopAnimations,
normalizeDataSeries,
} from "./animation";
import { createChart } from "./createChart";
import { LiveModeHandlers } from "./liveMode";
import { HistoricalModeHandlers } from "./historicalMode";
@@ -106,6 +110,10 @@ export function useBigGraphEffects({
handleTimeWindowChangeInternal,
}: UseBigGraphEffectsProps) {
const [isChartCreated, setIsChartCreated] = useState(false);
const setChartCreatedState = (created: boolean) => {
setIsChartCreated(created);
chartCreatedRef.current = created;
};
// Track the last synchronized state for comparison
const lastSyncStateRef = useRef({
@@ -239,18 +247,25 @@ export function useBigGraphEffects({
updateYAxisScale,
]);
// Create and initialize the chart when data becomes available
// Compute stable structural keys for chart creation.
// These only change when series count or visible line count changes,
// NOT on every data update (which would destroy/recreate the chart every ~1s).
const seriesWithDataCount = normalizeDataSeries(newData).filter(
(s) => s.newData !== null,
).length;
const visibleLineCount =
config.lines?.filter((l) => l.show !== false)?.length ?? 0;
// Create and initialize the chart when data becomes available or structure changes
useEffect(() => {
if (!containerRef.current || !primarySeries?.newData?.long) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
const [timestamps] = seriesToUPlotData(primarySeries.newData.long);
if (timestamps.length === 0) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
@@ -280,8 +295,7 @@ export function useBigGraphEffects({
showFromTimestamp,
});
setIsChartCreated(true);
chartCreatedRef.current = true;
setChartCreatedState(true);
if (isLiveMode) {
lastProcessedCountRef.current = timestamps.length;
@@ -292,10 +306,9 @@ export function useBigGraphEffects({
uplotRef.current?.destroy();
uplotRef.current = null;
stopAnimations(animationRefs);
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
};
}, [primarySeries?.newData?.long, containerRef.current]);
}, [seriesWithDataCount, visibleLineCount, containerRef.current]);
// Process new historical data in live mode
useEffect(() => {
+53
View File
@@ -304,3 +304,56 @@ export function resetSeries(series: Series): void {
series.lastTimestamp = 0;
series.validCount = 0;
}
/**
* Aligns target series values with a given set of timestamps.
* For each timestamp in dataTimestamps, finds the most recent target value
* that was set before or at that timestamp (step function interpolation).
*
* @param targetSeries - The TimeSeries containing historical target values
* @param dataTimestamps - Array of timestamps to align the target values to
* @param fallbackValue - Value to use when no target was set before a timestamp
* @returns Array of values aligned with dataTimestamps
*/
export function alignTargetSeriesToTimestamps(
targetSeries: TimeSeries,
dataTimestamps: number[],
fallbackValue: number,
): number[] {
if (dataTimestamps.length === 0) {
return [];
}
// Extract target series data in chronological order
const [targetTimestamps, targetValues] = extractDataFromSeries(
targetSeries.long,
);
if (targetTimestamps.length === 0) {
// No target history, use fallback for all timestamps
return new Array(dataTimestamps.length).fill(fallbackValue);
}
const result: number[] = [];
let targetIndex = 0;
for (const dataTs of dataTimestamps) {
// Find the most recent target value at or before this timestamp
// Advance targetIndex while the next target timestamp is still <= dataTs
while (
targetIndex < targetTimestamps.length - 1 &&
targetTimestamps[targetIndex + 1] <= dataTs
) {
targetIndex++;
}
// If the current target timestamp is > dataTs, we haven't reached the first target yet
if (targetTimestamps[targetIndex] > dataTs) {
result.push(fallbackValue);
} else {
result.push(targetValues[targetIndex]);
}
}
return result;
}
@@ -18,7 +18,7 @@ export function Aquapath1Page() {
link: "graph",
title: "Graph",
icon: "lu:ChartSpline",
activeLink: "graphs",
activeLink: "graph",
},
{
link: "settings",
@@ -24,6 +24,13 @@ export function Extruder2GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder2NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder2MessageHandler(
store: StoreApi<Extruder2NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder2NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder2MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder2MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder2NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder2() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder2() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -24,6 +24,13 @@ export function Extruder3GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder3NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder3MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder3MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder3NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder3() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder3() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -10,10 +10,17 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const {
diameter,
x_diameter,
y_diameter,
roundness,
state,
targetDiameter: targetDiameterSeries,
} = useLaser1();
const syncHook = useGraphSync("diameter-roundness-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const targetDiameterValue = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
@@ -71,21 +78,22 @@ export function Laser1GraphsPage() {
// Tolerance for diameter
{
type: "threshold" as const,
value: targetDiameter + higherTolerance,
value: targetDiameterValue + higherTolerance,
label: "Upper Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold" as const,
value: targetDiameter - lowerTolerance,
value: targetDiameterValue - lowerTolerance,
label: "Lower Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "target" as const,
value: targetDiameter,
value: targetDiameterValue,
targetSeries: targetDiameterSeries,
label: "Target",
color: diameterColor,
},
@@ -14,7 +14,7 @@ import {
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
@@ -64,6 +64,7 @@ export type Laser1NamespaceStore = {
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
targetDiameter: TimeSeries;
};
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
@@ -73,6 +74,8 @@ const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries();
const { initialTimeSeries: roundness, insert: addRoundness } =
createTimeSeries();
const { initialTimeSeries: targetDiameter, insert: addTargetDiameter } =
createTimeSeries();
/**
* Factory function to create a new Laser1 namespace store
@@ -87,6 +90,7 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
x_diameter,
y_diameter,
roundness,
targetDiameter,
};
});
@@ -117,6 +121,10 @@ export function laser1MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetDiameter: addTargetDiameter(state.targetDiameter, {
value: stateEvent.data.laser_state.target_diameter,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
@@ -11,8 +11,15 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
const {
state,
defaultState,
diameter,
x_diameter,
y_diameter,
roundness,
targetDiameter,
} = useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -113,6 +120,7 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
x_diameter,
y_diameter,
roundness,
targetDiameter,
// Loading states
isLoading: stateOptimistic.isOptimistic,
@@ -0,0 +1,53 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { useTestMotor } from "./useTestMotor";
export function TestMotorControlPage() {
const { state, setMotorOn, setVelocity } = useTestMotor();
// Fallback in case state is still null
const safeState = state ?? { motor_enabled: false, motor_velocity: 0 };
return (
<Page>
<ControlGrid columns={2}>
{/* Basic control */}
<ControlCard title="Motor Status">
{/* On/Off switch */}
<Label label="Power State">
<SelectionGroupBoolean
value={safeState.motor_enabled}
// Icon mapping for True/False
optionTrue={{ children: "Enabled", icon: "lu:Play" }}
optionFalse={{ children: "Disabled", icon: "lu:CirclePause" }}
onChange={(val) => setMotorOn(val)}
/>
</Label>
</ControlCard>
{/* Velocity */}
<ControlCard title="Settings">
{/* Velocity input with unit */}
<Label label="Target Velocity">
<EditValue
title="Velocity"
value={safeState.motor_velocity}
unit="rpm"
min={0}
max={1000} // Limit
step={1}
onChange={(val) => setVelocity(val)}
// Displays the value (as integer)
renderValue={(v) => v.toFixed(0)}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,22 @@
import { Topbar } from "@/components/Topbar";
import { testMotorSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMotorPage() {
// Access is allowed here because this function is called later
const { serial } = testMotorSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmotor/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,73 @@
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";
// ========== Event Schema (Must match StateEvent) ==========
export const stateEventDataSchema = z.object({
motor_enabled: z.boolean(),
motor_velocity: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMotorNamespaceStore = {
state: StateEvent | null;
};
export const createTestMotorNamespaceStore =
(): StoreApi<TestMotorNamespaceStore> =>
create<TestMotorNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMotorMessageHandler(
chstore: StoreApi<TestMotorNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMotorNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: TestMotorNamespaceStore) => TestMotorNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMotorNamespaceImplementation =
createNamespaceHookImplementation<TestMotorNamespaceStore>({
createStore: createTestMotorNamespaceStore,
createEventHandler: testMotorMessageHandler,
});
export function useTestMotorNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMotorNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMotorNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,96 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMotorSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMotorNamespace, StateEvent } from "./testMotorNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { TestMotor } from "@/machines/properties";
import { z } from "zod";
export function useTestMotor() {
const { serial: serialString } = testMotorSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: TestMotor.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMotorNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
type: z.string(),
payload: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
// --- ACTIONS ---
const setMotorOn = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.motor_enabled = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
// Must exactly match the 'Mutation' enum!
data: { type: "SetMotorOn", payload: on },
}),
);
};
const setVelocity = (velocity: number) => {
updateStateOptimistically(
(current) => {
current.motor_velocity = velocity;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { type: "SetMotorVelocity", payload: velocity },
}),
);
};
return {
state: stateOptimistic.value,
setMotorOn,
setVelocity,
};
}
+90
View File
@@ -390,6 +390,11 @@ export const aquapath1: MachineProperties = {
product_id: 0x7d83052,
revision: 0x110000,
},
{
vendor_id: 2,
product_id: 0x7d83052,
revision: 0x120000,
},
],
},
{
@@ -473,6 +478,30 @@ export const digitalInputTestMachine: MachineProperties = {
},
device_roles: [],
};
export const wago8chDioTestMachine: MachineProperties = {
name: "WAGO 8ch DIO Test",
version: "V1",
slug: "wago8chdiotestmachine",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0041,
},
device_roles: [
{
role: 0,
role_label: "Wago 750-354 Bus Coupler",
allowed_devices: [
{
vendor_id: 0x21,
product_id: 0x07500354,
revision: 0x2,
},
],
},
],
};
export const wagoPower1: MachineProperties = {
name: "WAGO Power",
version: "V1",
@@ -544,6 +573,64 @@ export const ip20TestMachine: MachineProperties = {
],
};
export const TestMotor: MachineProperties = {
name: "TestMotor",
version: "V1",
slug: "testmotor",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0011,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Stepper Motor",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
],
};
export const wagoDoTestMachine: MachineProperties = {
name: "Wago DO Test",
version: "V1",
slug: "wagodotestmachine",
icon: "lu:ToggleRight",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x000e,
},
device_roles: [
{
role: 0,
role_label: "Wago 750-354 Bus Coupler",
allowed_devices: [
{
vendor_id: 0x21,
product_id: 0x07500354,
revision: 0x2,
},
],
},
],
};
export const wagoAiTestMachine: MachineProperties = {
name: "Wago AI Test",
version: "V1",
@@ -581,7 +668,10 @@ export const machineProperties: MachineProperties[] = [
analogInputTestMachine,
wagoAiTestMachine,
digitalInputTestMachine,
wago8chDioTestMachine,
ip20TestMachine,
wagoDoTestMachine,
TestMotor,
];
export const getMachineProperties = (
@@ -0,0 +1,84 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { wago8chDioTestMachineRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { wago8chDioTestMachine } from "@/machines/properties";
import { z } from "zod";
import {
StateEvent,
useWago8chDioTestMachineNamespace,
} from "./wago8chDioTestMachineNameSpace";
export function useWago8chDioTestMachine() {
const { serial: serialString } = wago8chDioTestMachineRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: wago8chDioTestMachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useWago8chDioTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setOutputAtIndex = (index: number, value: boolean) => {
updateStateOptimistically(
(current) => {
current.digital_output[index] = value;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetOutput", value: { index, value } },
}),
);
};
return {
state: stateOptimistic.value,
setOutputAtIndex: setOutputAtIndex,
};
}
@@ -0,0 +1,66 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Label } from "@/control/Label";
import { useWago8chDioTestMachine } from "./useWago8chDioTestMachine";
import { Badge } from "@/components/ui/badge";
export function Wago8chDioTestMachineControlRoute() {
const { state, setOutputAtIndex } = useWago8chDioTestMachine();
const safeState = state ?? {
digital_input: [false, false, false, false, false, false, false, false],
digital_output: [false, false, false, false, false, false, false, false],
};
return (
<Page>
<ControlGrid columns={2}>
{/* Digital Inputs */}
<ControlCard title="Digital Inputs">
<div className="grid grid-cols-2 gap-4">
{safeState.digital_input.map((input, index) => (
<Label key={index} label={`Input ${index + 1}`}>
<Badge variant={input ? "outline" : "destructive"}>
{input ? "ON" : "OFF"}
</Badge>
</Label>
))}
</div>
</ControlCard>
{/* Digital Outputs */}
<ControlCard title="Digital Outputs">
<div className="grid grid-cols-2 gap-4">
{safeState.digital_output.map((output, index) => (
<Label key={index} label={`Output ${index + 1}`}>
<SelectionGroup<"On" | "Off">
value={output ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setOutputAtIndex(index, value === "On")}
/>
</Label>
))}
</div>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,79 @@
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";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
digital_input: z.array(z.boolean()).length(8),
digital_output: z.array(z.boolean()).length(8),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type Wago8chDioTestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createWago8chDioTestMachineNamespaceStore =
(): StoreApi<Wago8chDioTestMachineNamespaceStore> =>
create<Wago8chDioTestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function wago8chDioTestMachineMessageHndler(
store: StoreApi<Wago8chDioTestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Wago8chDioTestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: Wago8chDioTestMachineNamespaceStore,
) => Wago8chDioTestMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useWago8chDioTestMachineImplementation =
createNamespaceHookImplementation<Wago8chDioTestMachineNamespaceStore>({
createStore: createWago8chDioTestMachineNamespaceStore,
createEventHandler: wago8chDioTestMachineMessageHndler,
});
export function useWago8chDioTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): Wago8chDioTestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useWago8chDioTestMachineImplementation(namespaceId);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { wago8chDioTestMachineRoute } from "@/routes/routes";
import React from "react";
export function Wago8chDioTestMachinePage() {
const { serial } = wago8chDioTestMachineRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/wago8chdiotestmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -75,7 +75,7 @@ function wagoPower1MessageHandler(
};
try {
if (eventName === "StateEvent") {
if (eventName === "State") {
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
@@ -86,7 +86,7 @@ function wagoPower1MessageHandler(
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
} else if (eventName === "LiveValues") {
console.log(event);
const liveValues = liveValuesEventSchema.parse(event);
const { voltage, current } = liveValues.data;
@@ -0,0 +1,64 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Label } from "@/control/Label";
import { useWagoDoTestMachine } from "./useWagoDoTestMachine";
export function WagoDoTestMachineControlPage() {
const { state, setLed, setAllLeds } = useWagoDoTestMachine();
const safeState = state ?? {
led_on: Array(8).fill(false),
};
return (
<Page>
<ControlGrid columns={2}>
{/* Digital Output Controls */}
<ControlCard title="Digital Outputs (Do1-Do8)">
<div className="grid grid-cols-2 gap-6">
{safeState.led_on.map((output, index) => (
<Label key={index} label={`Do${index + 1}`}>
<SelectionGroup<"On" | "Off">
value={output ? "On" : "Off"}
orientation="vertical"
className="flex flex-col gap-3"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "flex-1",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "flex-1",
},
}}
onChange={(value) => setLed(index, value === "On")}
/>
</Label>
))}
</div>
</ControlCard>
{/* Master Output Control */}
<ControlCard title="Master Output Control">
<SelectionGroup<"On" | "Off">
value={safeState.led_on.every(Boolean) ? "On" : "Off"}
orientation="horizontal"
options={{
Off: { children: "Turn All Off" },
On: { children: "Turn All On" },
}}
onChange={(value) => setAllLeds(value === "On")}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,78 @@
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";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
led_on: z.array(z.boolean()).length(8),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type WagoDoTestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createWagoDoTestMachineNamespaceStore =
(): StoreApi<WagoDoTestMachineNamespaceStore> =>
create<WagoDoTestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function wagoDoTestMachineMessageHandler(
store: StoreApi<WagoDoTestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<WagoDoTestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: WagoDoTestMachineNamespaceStore,
) => WagoDoTestMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useWagoDoTestMachineNamespaceImplementation =
createNamespaceHookImplementation<WagoDoTestMachineNamespaceStore>({
createStore: createWagoDoTestMachineNamespaceStore,
createEventHandler: wagoDoTestMachineMessageHandler,
});
export function useWagoDoTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): WagoDoTestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useWagoDoTestMachineNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { wagoDoTestMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function WagoDoTestMachinePage() {
const { serial } = wagoDoTestMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/wagodotestmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,99 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { wagoDoTestMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useWagoDoTestMachineNamespace,
StateEvent,
} from "./WagoDoTestMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { wagoDoTestMachine } from "@/machines/properties";
import { z } from "zod";
export function useWagoDoTestMachine() {
const { serial: serialString } = wagoDoTestMachineSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: wagoDoTestMachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useWagoDoTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on[index] = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};
const setAllLeds = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on = Array(8).fill(on);
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAllLeds", value: { on } },
}),
);
};
return {
state: stateOptimistic.value,
setLed,
setAllLeds,
};
}
@@ -19,6 +19,7 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -31,6 +32,7 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
@@ -243,12 +245,14 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
@@ -259,6 +263,7 @@ export function PullerSpeedGraph({
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
@@ -62,6 +62,7 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -638,6 +639,7 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -211,6 +211,7 @@ export type Winder2NamespaceStore = {
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -220,6 +221,8 @@ const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries();
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: targetPullerSpeed, insert: addTargetPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries();
@@ -239,6 +242,7 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -277,6 +281,10 @@ export function winder2MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed: addTargetPullerSpeed(state.targetPullerSpeed, {
value: stateEvent.data.puller_state.target_speed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
+51
View File
@@ -67,12 +67,18 @@ import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestm
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
import { WagoPower1Page } from "@/machines/wago_power/wago_power1/WagoPower1Page";
import { WagoPower1ControlPage } from "@/machines/wago_power/wago_power1/WagoPower1ControlPage";
import { WagoDoTestMachinePage } from "@/machines/wagodotestmachine/WagoDoTestMachinePage";
import { WagoDoTestMachineControlPage } from "@/machines/wagodotestmachine/WagoDoTestMachineControlPage";
import { Wago8chDioTestMachinePage } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
import { Wago8chDioTestMachineControlRoute } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
@@ -87,6 +93,18 @@ export const testMachineSerialRoute = createRoute({
component: () => <TestMachinePage />,
});
export const wago8chDioTestMachineRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "wago8chdiotestmachine/$serial",
component: () => <Wago8chDioTestMachinePage />,
});
export const wago8chDioTestMachineControlRoute = createRoute({
getParentRoute: () => wago8chDioTestMachineRoute,
path: "control",
component: () => <Wago8chDioTestMachineControlRoute />,
});
// Leaf route: control page
export const testMachineControlRoute = createRoute({
getParentRoute: () => testMachineSerialRoute,
@@ -142,6 +160,18 @@ export const ip20TestMachineControlRoute = createRoute({
component: () => <IP20TestMachineControlPage />,
});
export const testMotorSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmotor/$serial",
component: () => <TestMotorPage />,
});
export const testMotorControlRoute = createRoute({
getParentRoute: () => testMotorSerialRoute,
path: "control",
component: () => <TestMotorControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -369,6 +399,18 @@ export const wagoPower1ControlRoute = createRoute({
component: () => <WagoPower1ControlPage />,
});
export const wagoDoTestMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "wagodotestmachine/$serial",
component: () => <WagoDoTestMachinePage />,
});
export const wagoDoTestMachineControlRoute = createRoute({
getParentRoute: () => wagoDoTestMachineSerialRoute,
path: "control",
component: () => <WagoDoTestMachineControlPage />,
});
export const setupRoute = createRoute({
getParentRoute: () => sidebarRoute,
path: "setup",
@@ -470,12 +512,17 @@ export const rootTree = RootRoute.addChildren([
laser1GraphsRoute,
laser1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
analogInputTestMachineSerialRoute.addChildren([
analogInputTestMachineControlRoute,
]),
wago8chDioTestMachineRoute.addChildren([
wago8chDioTestMachineControlRoute,
]),
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
digitalInputTestMachineSerialRoute.addChildren([
@@ -484,6 +531,8 @@ export const rootTree = RootRoute.addChildren([
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
testMotorSerialRoute.addChildren([testMotorControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
@@ -524,6 +573,8 @@ export const rootTree = RootRoute.addChildren([
wagoPower1SerialRoute.addChildren([wagoPower1ControlRoute]),
buffer1SerialRoute.addChildren([buffer1ControlRoute]),
wagoDoTestMachineSerialRoute.addChildren([wagoDoTestMachineControlRoute]),
]),
]),
]);
+1 -1
View File
@@ -8,7 +8,7 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
ethercrab = "0.6"
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
+1 -1
View File
@@ -180,7 +180,6 @@ impl Configuration for EL5152Configuration {
) -> Result<(), anyhow::Error> {
// Configure channel 1
self.channel1.write_channel_config(device, 0x8000).await?;
// Configure channel 2
self.channel2.write_channel_config(device, 0x8010).await?;
// Write PDO assignments
@@ -304,6 +303,7 @@ impl EL5152ChannelConfiguration {
device
.sdo_write(base_index, 0x11, self.frequency_window)
.await?;
device
.sdo_write(base_index, 0x13, self.frequency_scaling)
.await?;
@@ -351,6 +351,7 @@ pub enum EL7031_0030AnalogInputPort {
pub const EL7031_0030_VENDOR_ID: u32 = 0x2;
pub const EL7031_0030_PRODUCT_ID: u32 = 0x1b773052;
pub const EL7031_0030_REVISION_A: u32 = 0x10001E;
pub const EL7031_0030_IDENTITY_A: SubDeviceIdentityTuple = (
EL7031_0030_VENDOR_ID,
EL7031_0030_PRODUCT_ID,
+2 -1
View File
@@ -56,8 +56,9 @@ use std::{any::Any, fmt::Debug, sync::Arc};
use wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
use wago_modules::ip20_ec_di8_do8::{IP20_EC_DI8_DO8_IDENTITY, IP20EcDi8Do8};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct Module {
pub name: String,
pub slot: u16,
pub belongs_to_addr: u16,
pub has_tx: bool,
+118 -53
View File
@@ -2,6 +2,9 @@ use super::{
EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed, NewEthercatDevice,
SubDeviceIdentityTuple,
};
use crate::devices::wago_modules::wago_750_430::{
WAGO_750_430_MODULE_IDENT, WAGO_750_430_PRODUCT_ID,
};
use crate::devices::wago_modules::*;
use crate::{
devices::{
@@ -20,24 +23,25 @@ use crate::{
use anyhow::Error;
use smol::lock::RwLock;
use std::sync::Arc;
const MODULE_COUNT_INDEX: (u16, u8) = (0xf050, 0x00);
const TX_MAPPING_INDEX: (u16, u8) = (0x1c13, 0x00);
const RX_MAPPING_INDEX: (u16, u8) = (0x1c12, 0x00);
// For both the rx and tx The Wago Coupler has 4 bytes, which we dont care about and skip
/// Wago750_354 bus coupler
/*
The "Modules" simply write at an offset into rx and read at an offset in tx
*/
#[derive(Clone, Debug)]
struct ModulePdoMapping {
pub offset: usize,
pub module_i: u32,
}
// For both the rx and tx The Wago Coupler has 4 bytes, which we dont care about and skip
pub struct Wago750_354 {
is_used: bool,
pub slots: [Option<Module>; 64],
pub slot_devices: [Option<Arc<RwLock<dyn DynamicEthercatDevice>>>; 64],
pub dev_count: usize,
pub module_count: usize,
rx_offsets: Vec<usize>,
tx_offsets: Vec<usize>,
rx_pdo_mappings: Vec<ModulePdoMapping>,
tx_pdo_mappings: Vec<ModulePdoMapping>,
tx_size: usize,
rx_size: usize,
}
@@ -105,7 +109,7 @@ impl EthercatDevice for Wago750_354 {
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module);
self.slots[self.module_count] = Some(module.clone());
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -134,8 +138,8 @@ impl NewEthercatDevice for Wago750_354 {
dev_count: 0,
tx_size: 0,
rx_size: 0,
rx_offsets: vec![],
tx_offsets: vec![],
rx_pdo_mappings: vec![],
tx_pdo_mappings: vec![],
}
}
}
@@ -147,39 +151,76 @@ impl std::fmt::Debug for Wago750_354 {
}
impl Wago750_354 {
pub fn calculate_module_index(pdo_mapping: u32, is_tx: bool) -> u32 {
let start_index = match is_tx {
true => 0x6000 as u32,
false => 0x7000 as u32,
};
let index_in_hex = ((pdo_mapping & 0xFFFF0000) >> 16) - start_index;
if index_in_hex < 16 {
return 0;
} else {
return index_in_hex / 16;
}
}
pub async fn get_pdo_offsets<'a>(
&mut self,
device: &EthercrabSubDevicePreoperational<'a>,
get_tx: bool,
) -> Result<(), Error> {
let mut vec: Vec<usize> = vec![];
let mut vec: Vec<ModulePdoMapping> = vec![];
let mut bit_offset = 0;
let start_subindex = 0x1;
let mut module_i;
let start_subindex = 0x2;
let index = match get_tx {
true => (TX_MAPPING_INDEX.0, TX_MAPPING_INDEX.1),
false => (RX_MAPPING_INDEX.0, RX_MAPPING_INDEX.1),
};
let count = device.sdo_read::<u8>(index.0, index.1).await?;
for i in 0..count {
vec.push(bit_offset);
let pdo_index = device.sdo_read(index.0, start_subindex + i).await?;
if pdo_index != 0 {
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
// Iterate over every PDO Mapped entry and add it to the cumulative bit offset
for j in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + j).await?;
// We only need / Want the bit len, which we extract with a bitmask extracting the lsb
let bit_length = (pdo_mapping & 0xFF) as u8;
bit_offset += bit_length as usize;
}
}
let count_mappings = device.sdo_read::<u8>(index.0, index.1).await?;
let pdo_index = device.sdo_read::<u16>(index.0, 1).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for i in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + i).await?;
let bit_length = (pdo_mapping & 0xFF) as u8;
bit_offset += bit_length as usize;
}
let mut mappings_without_coupler: Vec<u32> = vec![];
for i in start_subindex..count_mappings {
let pdo_index = device.sdo_read(index.0, i).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for j in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + j).await?;
mappings_without_coupler.push(pdo_mapping);
}
}
mappings_without_coupler.sort();
for pdo_mapping in mappings_without_coupler {
module_i = Wago750_354::calculate_module_index(pdo_mapping, get_tx);
let bit_length = (pdo_mapping & 0xFF) as u8;
if module_i < 64 {
vec.push(ModulePdoMapping {
offset: bit_offset,
module_i,
});
}
bit_offset += bit_length as usize;
}
vec.sort_by_key(|e| (e.module_i, e.offset));
// deduplicate by module_i, so we only have the offset to the start of inputs/outputs
vec.dedup_by(|a, b| a.module_i == b.module_i);
if get_tx {
self.tx_offsets = vec;
self.tx_pdo_mappings = vec;
} else {
self.rx_offsets = vec;
self.rx_pdo_mappings = vec;
}
Ok(())
}
@@ -222,6 +263,7 @@ impl Wago750_354 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -229,26 +271,37 @@ impl Wago750_354 {
WAGO_750_1506_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-1506".to_string();
}
WAGO_750_455_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-455".to_string();
}
WAGO_750_501_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
module.name = "750-501".to_string();
}
WAGO_750_530_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
module.name = "750-530".to_string();
}
WAGO_750_652_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-652".to_string();
}
WAGO_750_402_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-402".to_string();
}
WAGO_750_430_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-430".to_string();
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
@@ -266,17 +319,42 @@ impl Wago750_354 {
if self.dev_count != 0 {
return;
}
let mut tx_index = 1;
let mut rx_index = 1;
smol::block_on(async {
let _ = self.get_pdo_offsets(device, true).await;
let _ = self.get_pdo_offsets(device, false).await;
});
println!("{:?}\n\n{:?}", self.tx_offsets, self.rx_offsets);
for module in &mut self.slots {
match module {
Some(m) => {
let tx_pdo_mapping = self
.tx_pdo_mappings
.iter()
.find(|map| map.module_i == m.slot.into());
if m.has_tx {
m.tx_offset = match tx_pdo_mapping {
Some(map) => map.offset,
None => 0,
}
}
for module in self.slots {
let rx_pdo_mapping = self
.rx_pdo_mappings
.iter()
.find(|map| map.module_i == m.slot.into());
if m.has_rx {
m.rx_offset = match rx_pdo_mapping {
Some(map) => map.offset,
None => 0,
}
}
}
None => break,
}
}
for module in &self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -302,6 +380,10 @@ impl Wago750_354 {
WAGO_750_402_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
}
WAGO_750_430_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_430::Wago750_430::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -312,27 +394,10 @@ impl Wago750_354 {
return;
}
};
let tx_pdo_offset = self.tx_offsets.get(tx_index as usize);
if m.has_tx {
tx_index += 1;
}
let rx_pdo_offset = self.rx_offsets.get(rx_index as usize);
if m.has_rx {
rx_index += 1;
}
let mut dev_guard = dev.write_blocking();
match tx_pdo_offset {
Some(offset) => dev_guard.set_tx_offset(*offset),
None => (),
}
match rx_pdo_offset {
Some(offset) => dev_guard.set_rx_offset(*offset),
None => (),
}
//println!("For {:?} setting tx: {} rx: {}",m.name,m.tx_offset,m.rx_offset);
dev_guard.set_tx_offset(m.tx_offset);
dev_guard.set_rx_offset(m.rx_offset);
drop(dev_guard);
self.slot_devices[self.dev_count] = Some(dev);
self.dev_count += 1;
@@ -214,7 +214,7 @@ impl EthercatDevice for IP20EcDi8Do8 {
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module);
self.slots[self.module_count] = Some(module.clone());
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -378,6 +378,7 @@ impl IP20EcDi8Do8 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -415,7 +416,7 @@ impl IP20EcDi8Do8 {
let _ = self.get_pdo_offsets(device, false).await;
});
for module in self.slots {
for module in &self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -1,6 +1,7 @@
pub mod ip20_ec_di8_do8;
pub mod wago_750_1506;
pub mod wago_750_402;
pub mod wago_750_430;
pub mod wago_750_455;
pub mod wago_750_501;
pub mod wago_750_530;
@@ -38,27 +38,27 @@ impl From<Wago750_1506OutputPort> for usize {
#[derive(Debug, Clone)]
pub enum Wago750_1506InputPort {
DO1,
DO2,
DO3,
DO4,
DO5,
DO6,
DO7,
DO8,
DI1,
DI2,
DI3,
DI4,
DI5,
DI6,
DI7,
DI8,
}
impl From<Wago750_1506InputPort> for usize {
fn from(value: Wago750_1506InputPort) -> Self {
match value {
Wago750_1506InputPort::DO1 => 0,
Wago750_1506InputPort::DO2 => 1,
Wago750_1506InputPort::DO3 => 2,
Wago750_1506InputPort::DO4 => 3,
Wago750_1506InputPort::DO5 => 4,
Wago750_1506InputPort::DO6 => 5,
Wago750_1506InputPort::DO7 => 6,
Wago750_1506InputPort::DO8 => 7,
Wago750_1506InputPort::DI1 => 0,
Wago750_1506InputPort::DI2 => 1,
Wago750_1506InputPort::DI3 => 2,
Wago750_1506InputPort::DI4 => 3,
Wago750_1506InputPort::DI5 => 4,
Wago750_1506InputPort::DI6 => 5,
Wago750_1506InputPort::DI7 => 6,
Wago750_1506InputPort::DI8 => 7,
}
}
}
@@ -91,14 +91,14 @@ impl DigitalInputDevice<Wago750_1506InputPort> for Wago750_1506 {
fn get_input(&self, port: Wago750_1506InputPort) -> Result<DigitalInputInput, anyhow::Error> {
Ok(DigitalInputInput {
value: match port {
Wago750_1506InputPort::DO1 => self.tx_pdo.port1,
Wago750_1506InputPort::DO2 => self.tx_pdo.port2,
Wago750_1506InputPort::DO3 => self.tx_pdo.port3,
Wago750_1506InputPort::DO4 => self.tx_pdo.port4,
Wago750_1506InputPort::DO5 => self.tx_pdo.port5,
Wago750_1506InputPort::DO6 => self.tx_pdo.port6,
Wago750_1506InputPort::DO7 => self.tx_pdo.port7,
Wago750_1506InputPort::DO8 => self.tx_pdo.port8,
Wago750_1506InputPort::DI1 => self.tx_pdo.port1,
Wago750_1506InputPort::DI2 => self.tx_pdo.port2,
Wago750_1506InputPort::DI3 => self.tx_pdo.port3,
Wago750_1506InputPort::DI4 => self.tx_pdo.port4,
Wago750_1506InputPort::DI5 => self.tx_pdo.port5,
Wago750_1506InputPort::DI6 => self.tx_pdo.port6,
Wago750_1506InputPort::DI7 => self.tx_pdo.port7,
Wago750_1506InputPort::DI8 => self.tx_pdo.port8,
},
})
}
@@ -189,28 +189,28 @@ impl EthercatDevice for Wago750_1506 {
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let idx1 = base + Into::<usize>::into(Wago750_1506InputPort::DO1);
let idx1 = base + Into::<usize>::into(Wago750_1506InputPort::DI1);
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
let idx2 = base + Into::<usize>::into(Wago750_1506InputPort::DO2);
let idx2 = base + Into::<usize>::into(Wago750_1506InputPort::DI2);
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
let idx3 = base + Into::<usize>::into(Wago750_1506InputPort::DO3);
let idx3 = base + Into::<usize>::into(Wago750_1506InputPort::DI3);
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
let idx4 = base + Into::<usize>::into(Wago750_1506InputPort::DO4);
let idx4 = base + Into::<usize>::into(Wago750_1506InputPort::DI4);
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
let idx5 = base + Into::<usize>::into(Wago750_1506InputPort::DO5);
let idx5 = base + Into::<usize>::into(Wago750_1506InputPort::DI5);
self.tx_pdo.port5 = *input.get(idx5).expect("Bit 5 out of bounds");
let idx6 = base + Into::<usize>::into(Wago750_1506InputPort::DO6);
let idx6 = base + Into::<usize>::into(Wago750_1506InputPort::DI6);
self.tx_pdo.port6 = *input.get(idx6).expect("Bit 6 out of bounds");
let idx7 = base + Into::<usize>::into(Wago750_1506InputPort::DO7);
let idx7 = base + Into::<usize>::into(Wago750_1506InputPort::DI7);
self.tx_pdo.port7 = *input.get(idx7).expect("Bit 7 out of bounds");
let idx8 = base + Into::<usize>::into(Wago750_1506InputPort::DO8);
let idx8 = base + Into::<usize>::into(Wago750_1506InputPort::DI8);
self.tx_pdo.port8 = *input.get(idx8).expect("Bit 8 out of bounds");
Ok(())
}
@@ -0,0 +1,211 @@
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput};
#[derive(Clone)]
pub struct Wago750_430 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub txpdo: Wago750_430TxPdo,
module: Option<Module>,
}
#[derive(Debug, Clone)]
pub enum Wago750_430Port {
Port1,
Port2,
Port3,
Port4,
Port5,
Port6,
Port7,
Port8,
}
impl From<Wago750_430Port> for usize {
fn from(value: Wago750_430Port) -> Self {
match value {
Wago750_430Port::Port1 => 0,
Wago750_430Port::Port2 => 1,
Wago750_430Port::Port3 => 2,
Wago750_430Port::Port4 => 3,
Wago750_430Port::Port5 => 4,
Wago750_430Port::Port6 => 5,
Wago750_430Port::Port7 => 6,
Wago750_430Port::Port8 => 7,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_430TxPdo {
pub port1: bool,
pub port2: bool,
pub port3: bool,
pub port4: bool,
pub port5: bool,
pub port6: bool,
pub port7: bool,
pub port8: bool,
}
impl DigitalInputDevice<Wago750_430Port> for Wago750_430 {
fn get_input(
&self,
port: Wago750_430Port,
) -> Result<crate::io::digital_input::DigitalInputInput, anyhow::Error> {
match port {
Wago750_430Port::Port1 => Ok(DigitalInputInput {
value: self.txpdo.port1,
}),
Wago750_430Port::Port2 => Ok(DigitalInputInput {
value: self.txpdo.port2,
}),
Wago750_430Port::Port3 => Ok(DigitalInputInput {
value: self.txpdo.port3,
}),
Wago750_430Port::Port4 => Ok(DigitalInputInput {
value: self.txpdo.port4,
}),
Wago750_430Port::Port5 => Ok(DigitalInputInput {
value: self.txpdo.port5,
}),
Wago750_430Port::Port6 => Ok(DigitalInputInput {
value: self.txpdo.port6,
}),
Wago750_430Port::Port7 => Ok(DigitalInputInput {
value: self.txpdo.port7,
}),
Wago750_430Port::Port8 => Ok(DigitalInputInput {
value: self.txpdo.port8,
}),
}
}
}
impl EthercatDeviceUsed for Wago750_430 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_430 {}
impl EthercatDynamicPDO for Wago750_430 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
self.rx_bit_offset
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, offset: usize) {
self.rx_bit_offset = offset
}
}
impl EthercatDevice for Wago750_430 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
self.txpdo.port1 = input[0];
self.txpdo.port2 = input[1];
self.txpdo.port3 = input[2];
self.txpdo.port4 = input[3];
self.txpdo.port5 = input[4];
self.txpdo.port6 = input[5];
self.txpdo.port7 = input[6];
self.txpdo.port8 = input[7];
Ok(())
}
fn input_len(&self) -> usize {
8
}
fn output(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_len(&self) -> usize {
0
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_checked(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl EthercatDeviceProcessing for Wago750_430 {}
impl NewEthercatDevice for Wago750_430 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
txpdo: Wago750_430TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_430 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_430")
}
}
pub const WAGO_750_430_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_430_PRODUCT_ID: u32 = 2147483777;
pub const WAGO_750_430_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_430_VENDOR_ID, WAGO_750_430_PRODUCT_ID);
@@ -148,8 +148,7 @@ impl EthercatDevice for Wago750_652 {
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = 32;
let base = self.rx_bit_offset;
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
let mut ol1_i = 8;
@@ -174,12 +173,10 @@ impl EthercatDevice for Wago750_652 {
);
ol1_i += 1;
}
println!("{}", self.rx_pdo.control.output_length);
let data_start = base + 16;
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
}
//println!("{:?}",self.rx_pdo.out_buffer);
Ok(())
}
+71
View File
@@ -2,3 +2,74 @@ pub mod counter_wrapper_u16_i128;
pub mod el70xx_velocity_converter;
pub mod ethercrab_types;
pub mod signing_converter_u16;
use ethercrab::{SubDevice, SubDeviceRef};
// This is the Codeword for disabling EEPROM writes on sdo writes
const BECKHOFF_EEPROM_LOCK_CODEWORD: u32 = 0x12345678;
// In this index Codewords can be written for Beckhoff Terminals
const BECKHOFF_CODEWORD_INDEX: u16 = 0xF008;
// This code locks the eeprom and protects it from writes
// When the toggle is in effect no writes propagate to the eeprom
// This is useful if you have a usecase where you very often write into CoE registers, as writes to EEPROM after a long time degrage the EEPROM
pub async fn set_mut_beckhoff_eeprom_lock_active<'a>(
subdevice: &SubDeviceRef<'a, &mut SubDevice>,
) -> Result<(), anyhow::Error> {
let code_word = match subdevice.sdo_read::<u32>(BECKHOFF_CODEWORD_INDEX, 0).await {
Ok(code_word) => code_word,
// This happens when the subdevice has no mailbox
// There is NO check in ethercrab for Mailbox presence, so we just have to pray that it has one and send a request
// If there is no Mailbox to write Coe Request to, then there is also no EEPROM meaning we achieved our goal in a sense
// This is why it returns OK on an error for sdo_read
Err(_) => return Ok(()),
};
let eeprom_lock_toggled = match code_word {
BECKHOFF_EEPROM_LOCK_CODEWORD => true,
_ => false,
};
if !eeprom_lock_toggled {
subdevice
.sdo_write(BECKHOFF_CODEWORD_INDEX, 0, BECKHOFF_EEPROM_LOCK_CODEWORD)
.await?;
tracing::info!(
"Activated EEPROM Lock for {} {}",
subdevice.name(),
subdevice.name()
);
}
Ok(())
}
pub async fn set_beckhoff_eeprom_lock_active<'a>(
subdevice: &SubDeviceRef<'a, &SubDevice>,
) -> Result<(), anyhow::Error> {
let code_word = match subdevice.sdo_read::<u32>(BECKHOFF_CODEWORD_INDEX, 0).await {
Ok(code_word) => code_word,
// This happens when the subdevice has no mailbox
// There is NO check in ethercrab for Mailbox presence, so we just have to pray that it has one and send a request
// If there is no Mailbox to write Coe Request to, then there is also no EEPROM meaning we achieved our goal in a sense
// This is why it returns OK on an error for sdo_read
Err(_) => return Ok(()),
};
let eeprom_lock_toggled = match code_word {
BECKHOFF_EEPROM_LOCK_CODEWORD => true,
_ => false,
};
if !eeprom_lock_toggled {
subdevice
.sdo_write(BECKHOFF_CODEWORD_INDEX, 0, BECKHOFF_EEPROM_LOCK_CODEWORD)
.await?;
tracing::info!(
"Activated EEPROM Lock for {} {}",
subdevice.name(),
subdevice.name()
);
}
Ok(())
}
-13
View File
@@ -1,13 +0,0 @@
# Git Hooks
This directory contains Git hooks for the project.
## Installation
To install the hooks, run the following command from the repository root:
```bash
# Copy the pre-commit hook to your local .git/hooks directory
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```
-49
View File
@@ -1,49 +0,0 @@
#!/bin/sh
#
# Pre-commit hook to generate Rust documentation
#
# This hook runs the rustdoc.sh script to update documentation
# before each commit.
echo "Running rustdoc generation..."
# Get the repository root
REPO_ROOT="$(git rev-parse --show-toplevel)"
# iteralte all directories thst contain a Cargo.toml file
for dir in $(find "$REPO_ROOT" -name "Cargo.toml" -exec dirname {} \;); do
echo "Generating documentation in $dir"
# cargotoml conatains workspace members, skip this directory
# HAS_MEMBERS=$(grep -c '\[workspace\]' "$dir/Cargo.toml")
# if [ "$HAS_MEMBERS" -gt 0 ]; then
# echo "Skipping workspace directory $dir"
# continue
# fi
# get package name from Cargo.toml using cargo metadata
PACKAGE_NAME=$(cargo metadata --format-version 1 --no-deps --manifest-path "$dir/Cargo.toml" | jq -r '.packages[0].name')
if [ -z "$PACKAGE_NAME" ]; then
echo "Could not determine package name for $dir"
continue
fi
# go into the directory
cd "$dir" || continue
# generate the documentation using rustdoc-md
cargo install rustdoc-md --git https://github.com/qitechgmbh/rustdoc-md --rev 6dc34f9c9d878799d9c1b4f2d2bcbb1f2757ede7
RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="-Z unstable-options --output-format json" cargo doc --no-deps
rustdoc-md --path "${REPO_ROOT}/target/doc/${PACKAGE_NAME}.json" --output ./rustdoc.md
# stage the generated documentation file
git add "rustdoc.md"
# go back to the repository root
cd "$REPO_ROOT" || exit
done
git rm --cached rustdoc.md 2>/dev/null
rm rustdoc.md
exit 0
+3 -3
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
ethercrab = "0.6"
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
control_core_derive = { path = "../control-core-derive" }
@@ -15,13 +15,13 @@ anyhow = "1.0.100"
bitvec = "1.0.1"
lazy_static = "1.5.0"
euclid = "0.22.11"
euclid = "0.22.13"
# concurrency
smol = "2.0.2"
# web
serde_json = "1.0.143"
serde_json = "1.0.149"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
# serial
+7 -3
View File
@@ -1,7 +1,6 @@
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use super::{
@@ -92,7 +91,12 @@ impl MachineNewTrait for AquaPathV1 {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
let subdevice =
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
.await?
.1;
el5152
.write()
.await
+4 -4
View File
@@ -159,7 +159,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_1,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -174,7 +174,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_2,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -189,7 +189,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_3,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -204,7 +204,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_4,
Heating::default(),
Duration::from_millis(500),
200.0,
150.0,
0.95,
);
+6
View File
@@ -28,10 +28,13 @@ pub mod ip20_test_machine;
pub mod laser;
pub mod machine_identification;
pub mod mock;
pub mod motor_test_machine;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
pub mod wago_power;
pub mod winder2;
@@ -49,7 +52,10 @@ pub const IP20_TEST_MACHINE: u16 = 0x0034;
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
pub const WAGO_8CH_IO_TEST_MACHINE: u16 = 0x0041;
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
pub const WAGO_DO_TEST_MACHINE: u16 = 0x000E;
use serde_json::Value;
use smol::lock::RwLock;
+3
View File
@@ -63,6 +63,8 @@ impl MachineIdentification {
x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(),
x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(),
x if x == WAGO_AI_TEST_MACHINE => "wago_ai_test_machine".to_string(),
x if x == WAGO_DO_TEST_MACHINE => "wago_do_test_machine".to_string(),
_ => unreachable!("Unknown machine id"),
}
}
@@ -187,6 +189,7 @@ use crate::MACHINE_WINDER_V1;
use crate::TEST_MACHINE;
use crate::VENDOR_QITECH;
use crate::WAGO_AI_TEST_MACHINE;
use crate::WAGO_DO_TEST_MACHINE;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
+40
View File
@@ -0,0 +1,40 @@
use super::MotorTestMachine;
use crate::{MachineAct, MachineMessage};
use std::time::Instant;
impl MachineAct for MotorTestMachine {
fn act(&mut self, _now_ts: Instant) {
// println!("[{}::act] Running act", module_path!());
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
self.motor_driver.set_enabled(self.motor_state.enabled);
if self.motor_state.enabled {
let steps_per_rev = 200.0; // Adjust to match motor
let steps_per_second = (self.motor_state.target_velocity as f64) * steps_per_rev / 60.0;
let _ = self.motor_driver.set_speed(steps_per_second);
} else {
let _ = self.motor_driver.set_speed(0.0);
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(ns) => {
self.namespace.namespace = Some(ns);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => {
self.namespace.namespace = None;
}
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _ = self.api_mutate(value);
}
_ => {}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
use super::MotorTestMachine;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
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;
use tracing::instrument;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub motor_enabled: bool,
pub motor_velocity: i32,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum BeckhoffEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "type", content = "payload")]
enum Mutation {
SetMotorOn(bool),
SetMotorVelocity(i32),
SetMotorOff(bool),
}
#[derive(Debug, Clone)]
pub struct BeckhoffNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<BeckhoffEvents> for BeckhoffNamespace {
#[instrument(skip_all)]
fn emit(&mut self, events: BeckhoffEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<BeckhoffEvents> for BeckhoffEvents {
fn event_value(&self) -> GenericEvent {
match self {
BeckhoffEvents::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for MotorTestMachine {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetMotorOn(t) => {
if t {
self.turn_motor_on();
} else {
self.turn_motor_off();
}
}
Mutation::SetMotorVelocity(vel) => {
self.motor_state.target_velocity = vel;
self.emit_state();
}
_ => {}
}
Ok(())
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use crate::{MOTOR_TEST_MACHINE, VENDOR_QITECH};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use smol::channel::{Receiver, Sender};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug, Clone, PartialEq)]
pub struct MotorState {
pub enabled: bool,
pub target_velocity: i32,
}
#[derive(Debug)]
pub struct MotorTestMachine {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: api::BeckhoffNamespace,
pub motor_driver: StepperVelocityEL70x1,
pub motor_state: MotorState,
}
impl Machine for MotorTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl MotorTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MOTOR_TEST_MACHINE,
};
pub fn emit_state(&mut self) {
let event = api::StateEvent {
motor_enabled: self.motor_state.enabled,
motor_velocity: self.motor_state.target_velocity,
};
self.namespace
.emit(api::BeckhoffEvents::State(event.build()));
}
pub fn turn_motor_on(&mut self) {
self.motor_state.enabled = true;
self.emit_state();
}
pub fn turn_motor_off(&mut self) {
self.motor_state.enabled = false;
self.emit_state();
}
}
+91
View File
@@ -0,0 +1,91 @@
use super::{MotorState, MotorTestMachine, api::BeckhoffNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::{EK1100, EK1100_IDENTITY_A};
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
use ethercat_hal::devices::el7031_0030::{
EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030StepperPort,
};
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
impl MachineNewTrait for MotorTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
println!("[{}::new] Creating new MotorTestMachine", module_path!());
let device_identification = params.device_group.iter().cloned().collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => return Err(anyhow::anyhow!("Hardware is not EtherCAT")),
};
smol::block_on(async {
// Role 0: EK1100 (Koppler)
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
// Role 1: EL7031 (Stepper Motor)
let el7031 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
1,
vec![EL7031_0030_IDENTITY_A],
)
.await?;
let el7031_config = EL7031_0030Configuration {
stm_features: ethercat_hal::devices::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_config)
.await?;
device.0
};
let motor_driver =
StepperVelocityEL70x1::new(el7031.clone(), EL7031_0030StepperPort::STM1);
let (sender, receiver) = smol::channel::unbounded();
Ok(Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: BeckhoffNamespace {
namespace: params.namespace.clone(),
},
motor_driver,
motor_state: MotorState {
enabled: true,
target_velocity: 100,
},
})
})
}
}
+10
View File
@@ -1,7 +1,9 @@
use crate::analog_input_test_machine::AnalogInputTestMachine;
use crate::digital_input_test_machine::DigitalInputTestMachine;
use crate::ip20_test_machine::IP20TestMachine;
use crate::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine;
use crate::wago_ai_test_machine::WagoAiTestMachine;
use crate::wago_do_test_machine::WagoDOTestMachine;
#[cfg(feature = "mock-machine")]
use crate::{
extruder1::mock::ExtruderV2 as ExtruderV2Mock1, extruder2::mock::ExtruderV2 as ExtruderV2Mock2,
@@ -24,6 +26,7 @@ use crate::test_machine::TestMachine;
use lazy_static::lazy_static;
use crate::motor_test_machine::MotorTestMachine;
use anyhow::Error;
use std::{any::TypeId, collections::HashMap};
@@ -132,8 +135,15 @@ lazy_static! {
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
mc.register::<MotorTestMachine>(MotorTestMachine::MACHINE_IDENTIFICATION);
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
mc.register::<WagoDOTestMachine>(WagoDOTestMachine::MACHINE_IDENTIFICATION);
mc.register::<Wago8chDigitalIOTestMachine>(
Wago8chDigitalIOTestMachine::MACHINE_IDENTIFICATION,
);
mc
};
}
@@ -0,0 +1,46 @@
use super::Wago8chDigitalIOTestMachine;
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for Wago8chDigitalIOTestMachine {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_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) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
})
.expect("Failed to send values");
sender.close();
}
}
}
}
@@ -0,0 +1,79 @@
use std::sync::Arc;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine};
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub digital_input: [bool; 8],
pub digital_output: [bool; 8],
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum Wago8chDigitalIOTestMachineEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetOutput { index: usize, value: bool },
}
#[derive(Debug, Clone)]
pub struct Wago8chDigitalIOTestMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<Wago8chDigitalIOTestMachineEvents>
for Wago8chDigitalIOTestMachineNamespace
{
fn emit(&mut self, events: Wago8chDigitalIOTestMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<Wago8chDigitalIOTestMachineEvents> for Wago8chDigitalIOTestMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
Wago8chDigitalIOTestMachineEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for Wago8chDigitalIOTestMachine {
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, value: serde_json::Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(value)?;
match mutation {
Mutation::SetOutput { index, value } => self.set_output(index, value),
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -0,0 +1,80 @@
use std::time::Instant;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::{digital_input::DigitalInput, digital_output::DigitalOutput};
use smol::channel::{Receiver, Sender};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_8CH_IO_TEST_MACHINE,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
wago_8ch_dio_test_machine::api::{
StateEvent, Wago8chDigitalIOTestMachineEvents, Wago8chDigitalIOTestMachineNamespace,
},
};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
pub struct Wago8chDigitalIOTestMachine {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: Wago8chDigitalIOTestMachineNamespace,
pub last_state_emit: Instant,
pub digital_output: [DigitalOutput; 8],
pub digital_input: [DigitalInput; 8],
}
impl Machine for Wago8chDigitalIOTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl Wago8chDigitalIOTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: WAGO_8CH_IO_TEST_MACHINE,
};
pub fn get_state(&self) -> StateEvent {
StateEvent {
digital_input: (0..8)
.map(|i| {
self.digital_input[i]
.get_value()
.expect("digital input value should be available")
})
.collect::<Vec<_>>()
.try_into()
.expect("bool vector into array[8] should work"),
digital_output: [
self.digital_output[0].get(),
self.digital_output[1].get(),
self.digital_output[2].get(),
self.digital_output[3].get(),
self.digital_output[4].get(),
self.digital_output[5].get(),
self.digital_output[6].get(),
self.digital_output[7].get(),
],
}
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace
.emit(Wago8chDigitalIOTestMachineEvents::State(event));
}
pub fn set_output(&mut self, i: usize, value: bool) {
self.digital_output[i].set(value);
}
}
@@ -0,0 +1,105 @@
use std::{sync::Arc, time::Instant};
use anyhow::Error;
use ethercat_hal::{
devices::{
EthercatDevice, downcast_device,
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
wago_modules::wago_750_1506::{
Wago750_1506, Wago750_1506InputPort, Wago750_1506OutputPort,
},
},
io::{digital_input::DigitalInput, digital_output::DigitalOutput},
};
use smol::{block_on, lock::RwLock};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
wago_8ch_dio_test_machine::{
Wago8chDigitalIOTestMachine, api::Wago8chDigitalIOTestMachineNamespace,
},
};
impl MachineNewTrait for Wago8chDigitalIOTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::EtherCATMachine/TestMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
// Example usage of a Wago Coupler and a 750-402 in the first slot, where the Output Port 1,2,3,4 is used
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware,
params,
0,
[WAGO_750_354_IDENTITY_A].to_vec(),
)
.await?;
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
let mut coupler = _wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(_wago_750_354.1);
let dev = coupler.slot_devices.get(0).unwrap().clone().unwrap();
let wago750_1506: Arc<RwLock<Wago750_1506>> =
downcast_device::<Wago750_1506>(dev).await?;
let di1 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI1);
let di2 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI2);
let di3 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI3);
let di4 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI4);
let di5 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI5);
let di6 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI6);
let di7 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI7);
let di8 = DigitalInput::new(wago750_1506.clone(), Wago750_1506InputPort::DI8);
let do1 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO1);
let do2 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO2);
let do3 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO3);
let do4 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO4);
let do5 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO5);
let do6 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO6);
let do7 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO7);
let do8 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO8);
drop(coupler);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: Wago8chDigitalIOTestMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
main_sender: params.main_thread_channel.clone(),
digital_output: [do1, do2, do3, do4, do5, do6, do7, do8],
digital_input: [di1, di2, di3, di4, di5, di6, di7, di8],
};
my_test.emit_state();
Ok(my_test)
})
}
}
+46
View File
@@ -0,0 +1,46 @@
use super::WagoDOTestMachine;
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for WagoDOTestMachine {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_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) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
use super::WagoDOTestMachine;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub led_on: [bool; 8],
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum WagoDOTestMachineEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetLed { index: usize, on: bool },
SetAllLeds { on: bool },
}
#[derive(Debug, Clone)]
pub struct WagoDOTestMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<WagoDOTestMachineEvents> for WagoDOTestMachineNamespace {
fn emit(&mut self, events: WagoDOTestMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<WagoDOTestMachineEvents> for WagoDOTestMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
WagoDOTestMachineEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for WagoDOTestMachine {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetLed { index, on } => self.set_led(index, on),
Mutation::SetAllLeds { on } => self.set_all_leds(on),
}
for (led, &on) in self.douts.iter().zip(self.led_on.iter()) {
led.set(on);
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+69
View File
@@ -0,0 +1,69 @@
use std::time::Instant;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
use smol::channel::{Receiver, Sender};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_DO_TEST_MACHINE,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
wago_do_test_machine::api::{StateEvent, WagoDOTestMachineEvents, WagoDOTestMachineNamespace},
};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
pub struct WagoDOTestMachine {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: WagoDOTestMachineNamespace,
pub last_state_emit: Instant,
pub led_on: [bool; 8],
pub douts: [DigitalOutput; 8],
}
impl Machine for WagoDOTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl WagoDOTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: WAGO_DO_TEST_MACHINE,
};
pub fn get_state(&self) -> StateEvent {
StateEvent {
led_on: self.led_on,
}
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(WagoDOTestMachineEvents::State(event));
}
/// Set the state of a specific digital output
pub fn set_led(&mut self, index: usize, on: bool) {
if index < self.led_on.len() {
self.led_on[index] = on;
self.emit_state();
}
}
/// Set all digital outputs at once
pub fn set_all_leds(&mut self, on: bool) {
self.led_on = [on; 8];
self.emit_state();
}
}
+101
View File
@@ -0,0 +1,101 @@
use crate::wago_do_test_machine::WagoDOTestMachine;
use crate::wago_do_test_machine::api::WagoDOTestMachineNamespace;
use smol::block_on;
use std::time::Instant;
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::devices::wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
use ethercat_hal::devices::wago_modules::wago_750_530::{Wago750_530, Wago750_530Port};
use ethercat_hal::devices::{EthercatDevice, downcast_device};
use ethercat_hal::io::digital_output::DigitalOutput;
use smol::lock::RwLock;
use std::sync::Arc;
impl MachineNewTrait for WagoDOTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/WagoDOTestMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware,
params,
0,
[WAGO_750_354_IDENTITY_A].to_vec(),
)
.await?;
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
let mut coupler = _wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(_wago_750_354.1);
let dev = coupler
.slot_devices
.get(0)
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::MachineNewTrait/WagoDOTestMachine::new] Expected Wago 750-530 module in slot 0, but slot 0 is not configured",
module_path!()
)
})?
.clone()
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::MachineNewTrait/WagoDOTestMachine::new] Expected Wago 750-530 module in slot 0, but slot 0 is empty or no device is present",
module_path!()
)
})?;
let wago750_530: Arc<RwLock<Wago750_530>> = downcast_device::<Wago750_530>(dev).await?;
let do1 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port1);
let do2 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port2);
let do3 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port3);
let do4 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port4);
let do5 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port5);
let do6 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port6);
let do7 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port7);
let do8 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port8);
drop(coupler);
let (sender, receiver) = smol::channel::unbounded();
let mut machine = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: WagoDOTestMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
led_on: [false; 8],
main_sender: params.main_thread_channel.clone(),
douts: [do1, do2, do3, do4, do5, do6, do7, do8],
};
machine.emit_state();
Ok(machine)
})
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-twp+30+hPKoMUojuGyJI1R6dknWWx9i5waqFw2FlHPc=";
npmDepsHash = "sha256-rnaAsSkoTRoBgLCTF8ZFpi7uUQ+8W+u3qoMxsptX6NU=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more