Compare commits

...
Author SHA1 Message Date
Robin Krämer 45c7b24420 fixed xdg autostart (#1180) 2026-02-28 19:25:12 +01:00
Robin Krämer 082d5dc952 Updated documentation for rest-api (#1151)
* updated docs

* updated docs

* Clarify Basic Auth instructions and curl examples

Updated instructions for Basic Auth to clarify the use of the credentials.sh script and added authentication details to example curl requests.

* Update curl example to include -k option

Added note about ignoring self-signed certificate error in curl.

* Update Caddy root certificate instructions for NixOS

Clarified instructions for locating and trusting the Caddy root certificate on NixOS.

* updated api docs

* updated docs
2026-02-27 20:28:41 +01:00
Robin Krämer 55af1120bf removed yield and added small async sleep in async loop (#1176)
* removed yield and added small async sleep in async loop

* fixed linter error
2026-02-27 15:23:47 +01:00
Achraf Jalleli 35864b4de7 1186 target line static (#1162)
* graph: render historical dashed targets as static overlay

* movement improved

* formatting

* more performance optimizations

* formatting
2026-02-25 16:42:37 +01:00
Robin Krämerandgithub-actions[bot] 621cb1913f 1158 update Linux version, newer electron version (#1161)
* updated electron version

* chore: update npmDepsHash to sha256-jW9nogboiuYBzjLNM5rlYsdfp+58OlDf+kxjgNlST70=

* updated flake.lock

* updated to linux 6.18

* flake updated to 25.11

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-25 16:04:13 +01:00
Christian 3e3e3f1c0b Merge pull request #1159 from qitechgmbh/fix-workflow-permission-issues
Fix workflow permission issues
2026-02-25 13:49:57 +01:00
ChristianandCopilot Autofix powered by AI cdf0374020 Potential fix for code scanning alert no. 11: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:24:33 +01:00
ChristianandCopilot Autofix powered by AI ec406889f7 Potential fix for code scanning alert no. 12: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:21:43 +01:00
ChristianandCopilot Autofix powered by AI b731a3bc36 Potential fix for code scanning alert no. 13: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:20:39 +01:00
Christian 27e15820f7 Add pull request template for better contribution guidelines 2026-02-25 12:00:22 +01:00
Robin Krämer bb307bb9d7 Update inverter settings with ExtruderV3 motor constants
Added motor constants for ExtruderV3 to documentation.
2026-02-24 13:25:04 +01:00
entity 39b05b056f Merge pull request #1149 from qitechgmbh/1148-bug-troubleshoot-page-crashes-electron
1148 bug troubleshoot page crashes electron
2026-02-24 13:20:13 +01:00
kraemr 8449045efb removed Terminal from Troubleshoot and added Export Button
fixed linter error, removed globallogstreaming
2026-02-24 11:47:17 +01:00
Christian 8a3de40501 Merge pull request #1123 from qitechgmbh/1119-disable-nozzle-temp-check-on-extruder
1119 Added toggle to disable nozzle heater/temp check for extruder 2 and 3
2026-02-20 19:15:20 +01:00
Achraf Jalleli 853d0a4478 Merge pull request #1050 from qitechgmbh/1003-add-a-ruff-time-estimate-for-the-overall-progress
1003 add a rough time estimate for the overall progress (notify user it takes 5 mins approx)
2026-02-20 15:28:28 +01:00
Robin Krämer d6dccc1b1a 1143 simplify api and add pw protection (#1146)
* added way to generate credentials + config for caddy, added caddy as a reverse proxy
* opened up firewall properly again
2026-02-19 17:45:51 +01:00
Melvin Spieß 5f636a6f15 Merge pull request #1144 from qitechgmbh/1185-target-line-historical
1185 target line historical
2026-02-19 14:50:37 +01:00
JalleliAchraf ac8a8c37ed formatting 2026-02-19 14:11:46 +01:00
Melvin Spieß 03e057f6a9 Merge pull request #1118 from qitechgmbh/889-dre-keep-zoom-level-when-switich-from-live-view-to-historical-view
889 dre keep zoom level when switich from live view to historical view
2026-02-19 14:05:14 +01:00
JalleliAchraf fb195eb7d4 changed speed of the target line 2026-02-19 14:02:07 +01:00
Snacj 874766633a Changed Historical Button to work as before. Only keep scale on gesture
Fixed requested changes

Fixed formatting

Fixed compilation errors

Fixed Formatting

Refresh .gitignore

Refresh .gitignore
2026-02-19 14:00:21 +01:00
Snacj 047b0fbe55 Switch to Historical Mode will now preserve current scale. 2026-02-19 14:00:21 +01:00
JalleliAchraf e3e9412578 Smooth target-line dash flow animation 2026-02-19 13:51:50 +01:00
Melvin Spießandjust_some_entity 92ead0e775 1084 added wago stepper terminal 750 672 (#1138)
* Added base of wago Stepper Controller

Added Debug for testing rx_pdo

* Added Driver Architecture for Stepper Controller and samll testing machine

* Added driver to Minimal Machine

safing progress

* Added infrastructure to control speed

Motor Running

Refactor of Offsets

Refactor Control and Statusbytes into their own Structs.

* Added stepper velocity helper

refactor rxpdo txpdo

Fixed Compilation errors

Motor Running - now move tick() function somewhere else.

* Added Minimal Machine for Wago Stepper Controller

Changed starting Velocity and Acceleration for testing

* Refactor

fix

* adding more debug for testing

Converted 671 Stepper to 672 Stepper

WIP

* Added Statemachine back to driver

WIP

Should be working now

Rebased from Master

* Added Digital Inputs handling to the Driver

* Added Freq Prescaler and Acc Prescaler

Removed 750 671 from this branch

Removed unused file

Added correct productId for Bus coupler

* Fixed Wago Coupler

Fixed Formatting

chore: update npmDepsHash to sha256-N2kdS9IKkEMNOPPnRNQ9GvCenR/k88fVyAFzXMJyxs4=

Fixed naming, formatting

* Added documentation

* Fixed Minor issues

* Fixed Formatting

* Allow dead code

* Removed unused import

---------

Co-authored-by: just_some_entity <entity@jsentity.dev>
2026-02-19 13:14:01 +01:00
JalleliAchraf 06a6c81b55 Record target history only on value change 2026-02-18 12:49:36 +01:00
JalleliAchraf 7b858251ea Optimize dashed target draw to visible window only 2026-02-18 12:31:34 +01:00
JalleliAchraf 04700d690c Render historical dashed targets via custom draw hook 2026-02-18 12:31:34 +01:00
JalleliAchraf e4d950ae9b Add target history tracking on master baseline for extruder/laser/winder 2026-02-18 12:31:34 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 7e03148d88 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-02-17 18:04:50 +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
EinsPommes 8c77ea41fd Update UpdateExecutePage.tsx to clarify reboot message after update completion 2026-02-17 10:02:41 +01:00
ᴏxᴏ.ᴍɪᴋᴀ ef1a2c3ca8 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-02-17 09:57:37 +01:00
Hans Sebastian Pöhlmann 66e30e2085 added confirm dialog for disabling nozzle temp target 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann 6ca15dbebc implemented copilot review suggestions 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann 7e5d44c329 fixed mocks 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann dc287d61cc added toggle to extruder1 backend 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann 9aed1193a3 clearer naming 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann 5fae6b628f hopefully fixing mock build error 2026-02-16 16:50:13 +01:00
Hans Sebastian Pöhlmann 88c6195d6c toggle added to extruder 2 and 3 2026-02-16 16:50:13 +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
ᴏxᴏ.ᴍɪᴋᴀ 1c0e06d4e1 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-02-07 13:17:06 +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
OshgnacknakandCopilot 9a4d41bf48 Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 15:30:06 +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
ᴏxᴏ.ᴍɪᴋᴀ cbffeff52d Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-02-04 08:44:30 +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
ᴏxᴏ.ᴍɪᴋᴀ 9d5e5cc7d0 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-02-03 08:50:47 +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
ᴏxᴏ.ᴍɪᴋᴀ c82d7347e2 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-01-30 16:39:57 +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
ᴏxᴏ.ᴍɪᴋᴀ 06a2284145 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-01-29 17:12:25 +01:00
EinsPommes 1b8b56fd02 refactor(UpdateExecutePage): streamline layout and enhance update time display 2026-01-25 19:48:51 +01:00
EinsPommes fef727983e fix style 2026-01-25 17:41:55 +01:00
EinsPommes 40239e0463 feat(ui): add update time estimate and refine ExecutePage layout 2026-01-25 17:39:21 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 9c579a9fd4 Merge branch 'master' into 1003-add-a-ruff-time-estimate-for-the-overall-progress 2026-01-25 17:19:19 +01:00
EinsPommes a3a343bbb8 Update toast message to confirm update start and clarify average update duration 2026-01-23 16:34:40 +01:00
EinsPommes 373d97d12d remove time estimation logic from UpdateProgressBar; add avg update time toast 2026-01-23 16:34:40 +01:00
EinsPommes e1842fa9d5 Fix formatting 2026-01-23 16:34:40 +01:00
EinsPommes 4fd8ddef51 Add time estimate to update progress bar 2026-01-23 16:34:40 +01:00
144 changed files with 7336 additions and 1260 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.
+14
View File
@@ -0,0 +1,14 @@
## What does this PR do / add / change?
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
## Screenshots
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
## Checklist before opening the pr
- [ ] Tested locally
- [ ] Tested on the machine (if hardware interaction is involved)
- [ ] No format errors (`cargo fmt` / `npm run format`)
- [ ] No build errors (`cargo build` / `npm run build`)
+3
View File
@@ -8,6 +8,9 @@ on:
branches:
- master
permissions:
contents: read
defaults:
run:
working-directory: electron
+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}`);
+3
View File
@@ -8,6 +8,9 @@ on:
branches:
- master
permissions:
contents: read
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
+3
View File
@@ -12,6 +12,9 @@ env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.86"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
+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 -2
View File
@@ -1,11 +1,11 @@
target/
dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
.idea/
.idea/
electron/dist-electron/
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"
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Generate 18 random alphanumeric characters base64 encode It
password=$(head -c 18 /dev/urandom | base64)
echo "Basic Authentication; user=machine password=$password"
echo $password > /tmp/machine_password
caddy_hash=$(caddy hash-password --plaintext $password)
echo "(machine_basic_auth) {
basic_auth {
machine $caddy_hash
}
}" | sudo tee /var/lib/caddy/auth_snippet.conf
# Transfer ownership to caddy user
sudo chown caddy:caddy /var/lib/caddy/auth_snippet.conf
# Tigther permissions (Only readable by root or caddy user)
sudo chmod 600 /var/lib/caddy/auth_snippet.conf
sudo systemctl restart caddy.service
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 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: 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: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

@@ -0,0 +1,931 @@
# Wago Stepper Controller - Developer Documentation
**Models:** 750-671 & 750-672
**Last Updated:** February 2026
## Table of Contents
1. [Overview](#overview)
2. [Hardware Quick Reference](#hardware-quick-reference)
3. [Architecture & Code Organization](#architecture--code-organization)
4. [Control & Status Bytes](#control--status-bytes)
5. [Implementation Details](#implementation-details)
6. [Prescalers & Calculations](#prescalers--calculations)
7. [Common Pitfalls & Debug Tips](#common-pitfalls--debug-tips)
---
## Overview
The Wago Stepper Controllers (750-671 and 750-672) drive stepper motors via EtherCAT. This documentation focuses on **velocity/speed control mode** implementation.
### Key Differences Between Models
| Feature | 750-671 | 750-672 |
| --------------- | ------------------------------------------ | ------------------------------------ |
| Max Voltage | 50 VDC | 70 VDC |
| Max Current | 5 A | 7.5 A |
| Speed Control | Via Application Selector in config | Via Control Byte C1 command |
| Control Byte C1 | Different bit layout (M_Speed_Control bit) | Command field (0x18 = Speed Control) |
**For 750-671:** Must set `Application_Selector = 2` and `PWM_Period = 0` in WAGO I/O CHECK before using speed control.
**For 750-672:** Speed control is selected directly via command in Control Byte C1 (no config needed).
### Process Image
Both models use **12 bytes bidirectional** (96 bits each way):
```
Input (TxPDO - Controller → PLC):
[0] S0 (status)
[1] Reserved
[2-3] Actual Velocity (i16)
[4-5] Reserved
[6-8] Actual Position (24-bit: L, M, H)
[9] S3 (digital inputs, warnings)
[10] S2 (busy, error, on_speed, etc.)
[11] S1 (acknowledgments)
Output (RxPDO - PLC → Controller):
[0] C0 (control)
[1] Reserved
[2-3] Target Velocity (i16, clamped to ±25000)
[4-5] Acceleration (u16)
[6-8] Position (24-bit: L, M, H)
[9] C3 (position/reset control)
[10] C2 (prescalers, error quit)
[11] C1 (main control: enable, start, mode)
```
---
## Hardware Quick Reference
### Connections
| Pin | Name | Purpose |
|-----|------|---------|
| DI1+ | Digital Input 1 | Enable input (connect to 24V or control via software) |
| DI2+ | Digital Input 2 | Referencing input |
| DI- | Ground | Reference potential for DI1/DI2 |
| A1, A2 | Motor Winding A | Connect to motor phase A |
| B1, B2 | Motor Winding B | Connect to motor phase B |
### Status LEDs
- **LED H:** Shows error codes via blink patterns (see Wago manual section 3.3 for error codes)
- Other LEDs show Ready, Enable, Busy states (see manual section 2.1.2.3)
---
## Architecture & Code Organization
### File Structure
```
devices/
wago_modules/
wago_750_672.rs # Device struct, EtherCAT interface
io/
stepper_velocity_wago_750_672.rs # High-level wrapper & byte helpers
```
### Key Components
#### 1. `Wago750_672` Struct (EtherCAT Device)
Located in `devices/wago_modules/wago_750_672.rs`
```rust
pub struct Wago750_672 {
is_used: bool,
tx_bit_offset: usize, // Input data offset
rx_bit_offset: usize, // Output data offset
pub rxpdo: Wago750_672RxPdo, // Data we send
pub txpdo: Wago750_672TxPdo, // Data we receive
module: Option<Module>,
pub state: InitState,
pub initialized: bool,
}
```
**Responsibilities:**
- Implements EtherCAT device traits
- Handles bit-level I/O with process image
- Runs state machine in `input()` method
- Thread-safe (used via `Arc<RwLock<>>`)
#### 2. `StepperVelocityWago750672` Wrapper
Located in `io/stepper_velocity_wago_750_672.rs`
```rust
pub struct StepperVelocityWago750672 {
pub device: Arc<RwLock<Wago750_672>>,
pub state: InitState,
pub target_velocity: i16,
pub target_acceleration: u16,
pub enabled: bool,
pub freq_range_sel: u8,
pub acc_range_sel: u8,
}
```
**Responsibilities:**
- Provides high-level API (`set_velocity()`, `set_enabled()`)
- Manages prescaler settings
- Abstracts away state machine complexity
### Why Two Layers?
1. **`Wago750_672`:** Low-level EtherCAT device that handles cycle-by-cycle I/O
2. **`StepperVelocityWago750672`:** High-level wrapper for application logic
This separation allows:
- EtherCAT device to run in tight real-time loop
- Application code to use simple API without worrying about cycles/acknowledgments
- Easy testing of each layer independently
---
## Control & Status Bytes
### Design Pattern: Type-Safe Byte Construction
Instead of hardcoding hex values like `0x1B`, we use builder pattern with enums:
```rust
// ❌ BAD - magic numbers, error-prone
dev.rxpdo.c1 = 0x1B;
// ✅ GOOD - explicit, self-documenting
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
dev.rxpdo.c1 = c1;
```
### Wago 750-672 Control Bytes
#### Control Byte C1 (Offset 11) - Main Control
```rust
pub struct ControlByteC1(u8);
pub enum C1Flag {
Enable = 0b0000_0001, // Bit 0: Must be 1 to enable
Stop2N = 0b0000_0010, // Bit 1: Must be 1 for normal operation
Start = 0b0000_0100, // Bit 2: Rising edge triggers action
}
pub enum C1Command {
Idle = 0b0000_0000, // Bits 3-7: Operating mode
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000, // ← We use this
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
```
**Implementation:**
```rust
impl ControlByteC1 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
self.0 |= flag as u8; // OR in the bit
self
}
pub const fn with_command(mut self, cmd: C1Command) -> Self {
self.0 = (self.0 & 0b0000_0111) | (cmd as u8); // Mask lower 3 bits, set upper 5
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
```
**Key Points:**
- `const fn` allows compile-time evaluation
- Chainable builder pattern
- Command overwrites bits 3-7 while preserving 0-2
- Flags are additive (multiple can be set)
#### Control Byte C2 (Offset 10) - Prescalers & Error Handling
```rust
pub struct ControlByteC2(u8);
pub enum C2Flag {
FreqRangeSelL = 0b0000_0001, // Bit 0: Freq prescaler LSB
FreqRangeSelH = 0b0000_0010, // Bit 1: Freq prescaler MSB
AccRangeSelL = 0b0000_0100, // Bit 2: Acc prescaler LSB
AccRangeSelH = 0b0000_1000, // Bit 3: Acc prescaler MSB
PreCalc = 0b0100_0000, // Bit 6: Pre-calculation
ErrorQuit = 0b1000_0000, // Bit 7: Acknowledge error
}
impl ControlByteC2 {
pub const fn with_freq_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
self
}
pub const fn with_acc_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
self
}
}
```
**Critical:** Prescalers can only be changed when controller is **disabled** (before Enable state).
#### Control Byte C3 (Offset 9) - Position & Reset
```rust
pub struct ControlByteC3(u8);
pub enum C3Flag {
SetActualPos = 0b0000_0001, // Bit 0: Set current position
DirectionPos = 0b0000_0010, // Bit 2: Force positive direction
DirectionNeg = 0b0000_0100, // Bit 3: Force negative direction
ResetQuit = 0b1000_0000, // Bit 7: Acknowledge reset
}
```
### Status Bytes (Read-Only)
#### Status Byte S1 (Offset 11) - Acknowledgments
```rust
pub struct StatusByteS1(u8);
pub enum S1Flag {
Ready = 0b0000_0001, // Bit 0: Acknowledges Enable
Stop2NAck = 0b0000_0010, // Bit 1: Acknowledges Stop2_N
StartAck = 0b0000_0100, // Bit 2: Acknowledges Start
}
// Bits 3-7: Mirrors C1 command when acknowledged
impl StatusByteS1 {
pub const fn has_flag(self, flag: S1Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
```
**Usage Pattern:**
```rust
// Wait for acknowledgment
if self.txpdo.s1 == c1 {
// All bits acknowledged, proceed
}
```
#### Status Byte S2 (Offset 10) - Operation Status
```rust
pub struct StatusByteS2(u8);
pub enum S2Flag {
OnTarget = 0b0000_0001, // Bit 0: Position reached (positioning mode)
Busy = 0b0000_0010, // Bit 1: Executing command
StandStill = 0b0000_0100, // Bit 2: Motor not moving
OnSpeed = 0b0000_1000, // Bit 3: Target velocity reached
Direction = 0b0001_0000, // Bit 4: Rotation direction (0=neg, 1=pos)
ReferenceOk = 0b0010_0000, // Bit 5: Referencing complete
PreCalcAck = 0b0100_0000, // Bit 6: Pre-calc acknowledged
Error = 0b1000_0000, // Bit 7: Error occurred (check LED H)
}
```
**Critical for velocity control:** Monitor `OnSpeed` bit to know when target velocity is reached.
#### Status Byte S3 (Offset 9) - Digital Inputs & Warnings
```rust
pub struct StatusByteS3(u8);
pub enum S3Flag {
Input1 = 0b0000_0001, // Bits 0-5: Digital input states
Input2 = 0b0000_0010,
Input3 = 0b0000_0100,
Input4 = 0b0000_1000,
Input5 = 0b0001_0000,
Input6 = 0b0010_0000,
Warning = 0b0100_0000, // Bit 6: Warning condition
Reset = 0b1000_0000, // Bit 7: Reset requested
}
```
---
## Implementation Details
### Critical Initialization Sequence
⚠️ **This sequence MUST be followed exactly or the controller won't work:**
1. **Set non-zero acceleration** (before enabling)
2. **Set Enable + Stop2_N** (wait for ack)
3. **Set operating mode** (0x18 for speed control, wait for ack)
4. **Issue Start pulse** (set to 1, wait for ack, set to 0, wait for ack clear)
5. **Running** (repeat step 4 every time velocity changes)
### State Machine (Why It Exists)
The state machine exists because **acknowledgments happen in different EtherCAT cycles**. You can't just set everything in one cycle.
```rust
#[derive(Debug, Clone)]
pub enum InitState {
Off, // Controller disabled
Enable, // Setting Enable + Stop2_N, waiting for ack
SetMode, // Setting speed control mode, waiting for ack
StartPulseStart, // Setting Start bit, waiting for ack
StartPulseEnd, // Clearing Start bit, waiting for ack clear
Running, // Normal operation
ErrorQuit, // Handling error
ResetQuit, // Handling reset
}
```
### State Machine Implementation
Located in `Wago750_672::input()` method (runs every EtherCAT cycle):
```rust
fn input(&mut self, input: &BitSlice) -> Result<(), anyhow::Error> {
// 1. Read process image from controller
let base = self.tx_bit_offset;
let mut b = [0u8; 12];
for i in 0..12 {
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
}
self.txpdo = Wago750_672TxPdo {
s0: b[0],
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
position_l: b[6],
position_m: b[7],
position_h: b[8],
s3: b[9],
s2: b[10],
s1: b[11],
};
// 2. State machine logic
match self.state {
InitState::Off => {
self.initialized = false;
// Do nothing until external enable request
}
InitState::Enable => {
// Build C1 byte with Enable and Stop2_N
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.bits();
self.rxpdo.c1 = c1;
// Wait for acknowledgment
if self.txpdo.s1 == c1 {
self.state = InitState::SetMode;
}
}
InitState::SetMode => {
// Add speed control command
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for mode acknowledgment
if self.txpdo.s1 == c1 {
self.initialized = true;
self.state = InitState::StartPulseStart;
}
}
InitState::StartPulseStart => {
// Set Start bit
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_flag(C1Flag::Start) // ← Rising edge
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for Start acknowledgment
if self.txpdo.s1 == c1 {
self.state = InitState::StartPulseEnd;
}
}
InitState::StartPulseEnd => {
// Clear Start bit
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for acknowledgment to clear
if self.txpdo.s1 == c1 {
self.state = InitState::Running;
}
}
InitState::Running => {
// Monitor for errors and resets
let c2_error_mask = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
let c3_reset_mask = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
if self.txpdo.s2 & c2_error_mask != 0 {
self.state = InitState::ErrorQuit;
} else if self.txpdo.s3 & c3_reset_mask != 0 {
self.state = InitState::ResetQuit;
}
}
InitState::ErrorQuit => {
// Set Error_Quit bit to acknowledge
self.rxpdo.c2 |= ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
tracing::error!("Stepper Controller Errored. Trying to reenable...");
self.state = InitState::Enable; // Restart initialization
}
InitState::ResetQuit => {
// Set Reset_Quit bit to acknowledge
self.rxpdo.c3 |= ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
tracing::error!("Stepper Controller Reset. Trying to reenable...");
self.state = InitState::Enable; // Restart initialization
}
}
Ok(())
}
```
**Key Implementation Notes:**
1. **State changes happen AFTER acknowledgment:** We wait for `self.txpdo.s1 == c1` before moving to next state
2. **Enable and Stop2_N stay high:** They're included in every C1 byte after Enable state
3. **Start pulse is a rising edge:** Must go 1 → 0, not just stay at 1
4. **Error recovery:** Automatically attempts to re-enable on error/reset
### High-Level API Implementation
Located in `StepperVelocityWago750672`:
```rust
pub fn set_enabled(&mut self, enabled: bool) {
// Prevent redundant enables
if self.enabled && enabled {
return;
}
// Safety check: acceleration must be non-zero
if self.get_target_acceleration() == 0 {
return;
}
self.enabled = enabled;
if enabled {
self.change_init_state(InitState::Enable);
} else {
self.change_init_state(InitState::Off);
self.write_control_byte(ControlByte::C1, 0b00000000);
}
}
pub fn set_velocity(&mut self, velocity: i16) {
self.target_velocity = velocity;
let mut dev = block_on(self.device.write());
// Clamp to valid range
dev.rxpdo.velocity = velocity.clamp(-25000, 25000);
// Trigger Start pulse if already initialized
if dev.initialized {
dev.state = InitState::StartPulseStart;
}
}
pub fn set_acceleration(&mut self, acceleration: u16) {
self.target_acceleration = acceleration;
let mut dev = block_on(self.device.write());
dev.rxpdo.acceleration = acceleration;
}
```
**Why the wrapper?**
- Application code doesn't need to know about state machine
- Handles common errors (zero acceleration check)
- Thread-safe access to device
- Clamping and validation in one place
### Changing Velocity at Runtime
⚠️ **Critical:** Must issue a **new Start pulse** for every velocity change:
```rust
// User calls this
stepper.set_velocity(15000);
// Internally, set_velocity() does:
dev.rxpdo.velocity = 15000;
if dev.initialized {
dev.state = InitState::StartPulseStart; // ← Triggers new pulse
}
// State machine then handles:
// StartPulseStart → StartPulseEnd → Running
// (Takes 2-3 EtherCAT cycles)
```
**Common mistake:** Setting velocity without triggering Start pulse - motor won't change speed.
---
## Prescalers & Calculations
Prescalers scale the velocity and acceleration values. They **can only be changed when disabled**.
### Frequency Prescaler (Bits 0-1 of C2)
Controls velocity scaling:
| Freq_Range_Sel | Prescaler | Max Frequency | Resolution |
|----------------|-----------|---------------|------------|
| 00 | 200 (default) | 10 kHz | Configurable |
| 01 | 80 | 25 kHz | 1 Hz |
| 10 | 20 | 100 kHz | 20 Hz |
| 11 | 4 | 500 kHz | 20 Hz |
**Formula:**
```
Output Frequency (fp) = Velocity × 80 / Freq_Prescaler [Hz]
```
**Example:**
```rust
// Freq_Range_Sel = 01 (prescaler = 80)
// Velocity = 10000
// Output: 10000 × 80 / 80 = 10000 Hz
stepper.set_freq_range_sel(1); // Must be done BEFORE enable
stepper.set_velocity(10000);
```
### Acceleration Prescaler (Bits 2-3 of C2)
Controls acceleration scaling:
| Acc_Range_Sel | Multiplier | Time to Max Speed |
|---------------|------------|-------------------|
| 00 | 8 (default) | 7600 ms |
| 01 | 80 | 760 ms |
| 10 | 800 | 76 ms |
| 11 | 8000 | 7.6 ms |
**Formula:**
```
Acceleration (a) = Acceleration_Value × Acc_Multiplier / Freq_Prescaler [Hz/s]
```
**Example:**
```rust
// Acc_Range_Sel = 01 (multiplier = 80)
// Freq_Range_Sel = 01 (prescaler = 80)
// Acceleration_Value = 10000
// Output: 10000 × 80 / 80 = 10000 Hz/s
stepper.set_acc_range_sel(1); // Must be done BEFORE enable
stepper.set_acceleration(10000);
```
### Setting Prescalers (Implementation)
```rust
pub fn set_freq_range_sel(&mut self, factor: u8) {
// Safety checks
if self.enabled || factor > 3 {
return; // Can't change while enabled
}
self.freq_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_freq_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
pub fn set_acc_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.acc_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_acc_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
```
**Usage:**
```rust
let mut stepper = StepperVelocityWago750672::new(device);
// Set prescalers FIRST (before enabling)
stepper.set_freq_range_sel(1);
stepper.set_acc_range_sel(1);
stepper.set_acceleration(10000);
// Then enable
stepper.set_enabled(true);
// Now can set velocity
stepper.set_velocity(15000);
```
---
## Common Pitfalls & Debug Tips
### 1. Controller Won't Enable
**Symptoms:**
- State stuck in `Enable`
- `initialized` never becomes true
- Motor doesn't move
**Debug checklist:**
```rust
// Check acceleration is non-zero
println!("Acceleration: {}", dev.rxpdo.acceleration); // Must be > 0
// Check DI1 if hardware enable is configured
println!("DI1 state: {}", (dev.txpdo.s3 & 0x01) != 0); // Should be 1
// Check status byte acknowledgment
println!("C1: {:#010b}", dev.rxpdo.c1);
println!("S1: {:#010b}", dev.txpdo.s1); // Should match C1
```
**Common causes:**
```rust
// ❌ Forgot to set acceleration
stepper.set_acceleration(0); // Won't enable!
// ✅ Always set non-zero acceleration
stepper.set_acceleration(10000);
stepper.set_enabled(true);
```
### 2. Velocity Won't Change
**Symptoms:**
- Motor running but won't change speed
- `set_velocity()` called but no effect
**Debug:**
```rust
// Check initialized flag
println!("Initialized: {}", dev.initialized);
// Check state after velocity change
println!("State: {:?}", dev.state); // Should become StartPulseStart
// Manually trigger Start pulse (bypass wrapper)
dev.state = InitState::StartPulseStart;
```
**Common causes:**
```rust
// ❌ Setting velocity before initialization
stepper.set_velocity(10000); // Controller not ready yet
// ✅ Check initialized first
if stepper.device.read().initialized {
stepper.set_velocity(10000);
}
```
### 3. Prescaler Not Working
**Symptoms:**
- Velocity calculation seems wrong
- Can't reach expected frequency
**Debug:**
```rust
// Check prescalers were set BEFORE enabling
println!("Freq prescaler: {}", stepper.freq_range_sel);
println!("Acc prescaler: {}", stepper.acc_range_sel);
println!("Enabled: {}", stepper.enabled);
// Check C2 byte
let dev = stepper.device.read();
println!("C2: {:#010b}", dev.rxpdo.c2);
// Bits 0-1: freq, Bits 2-3: acc
```
**Common causes:**
```rust
// ❌ Setting prescaler after enable
stepper.set_enabled(true);
stepper.set_freq_range_sel(1); // Too late! Ignored.
// ✅ Set prescalers FIRST
stepper.set_freq_range_sel(1);
stepper.set_acc_range_sel(1);
stepper.set_enabled(true);
```
### 4. State Machine Stuck
**Symptoms:**
- State doesn't progress
- Acknowledgments not received
**Debug:**
```rust
// Add logging to state machine
match self.state {
InitState::Enable => {
println!("Enable state: C1={:#010b}, S1={:#010b}", self.rxpdo.c1, self.txpdo.s1);
// ...
}
}
// Check EtherCAT communication
// - Is cycle running?
// - Are other devices working?
// - Check bus topology
```
### 5. Error Bit Set
**Symptoms:**
- Error bit (S2 bit 7) is 1
- LED H blinking
- Controller stops
**Debug:**
```rust
// Check error state
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
if s2.has_flag(S2Flag::Error) {
println!("ERROR DETECTED");
// Count LED H blinks for error code
// See Wago manual section 3.3
// Check other status flags for clues
println!("Busy: {}", s2.has_flag(S2Flag::Busy));
println!("StandStill: {}", s2.has_flag(S2Flag::StandStill));
}
```
**Common error codes:**
- **2 blinks:** Overcurrent
- **3 blinks:** Position error
- **4 blinks:** Encoder fault
- See Wago manual for complete list
---
### Supporting Wago 750-671
The 750-671 uses different C1 bit layout:
1. **Create separate enum:**
```rust
// For 750-671
pub enum C1Flag671 {
Enable = 0b0000_0001, // Bit 0
Stop2N = 0b0000_0010, // Bit 1
Start = 0b0000_0100, // Bit 2
MSpeedControl = 0b0000_1000, // Bit 3 (instead of command field)
MProgram = 0b0001_0000, // Bit 4
MReference = 0b0010_0000, // Bit 5
MJog = 0b0100_0000, // Bit 6
MDriveByMBX = 0b1000_0000, // Bit 7
}
```
---
## Quick Reference Card
### Initialization Checklist
```rust
// 1. Create and configure
let stepper = StepperVelocityWago750672::new(device);
// 2. Set prescalers (optional, before enable)
stepper.set_freq_range_sel(1); // 0-3
stepper.set_acc_range_sel(1); // 0-3
// 3. Set non-zero acceleration (REQUIRED)
stepper.set_acceleration(10000);
// 4. Enable
stepper.set_enabled(true);
// 5. Set velocity (after initialized)
stepper.set_velocity(15000); // ±25000 max
```
### Key Constants
```rust
const SPEED_CONTROL_CMD: u8 = 0x18; // C1 command for 750-672
const MAX_VELOCITY: i16 = 25000;
const MIN_VELOCITY: i16 = -25000;
const DEFAULT_ACCELERATION: u16 = 10000;
```
### Critical Rules
1. ⚠️ **Never enable with zero acceleration**
2. ⚠️ **Always wait for acknowledgments**
3. ⚠️ **Issue Start pulse for every velocity change**
4. ⚠️ **Prescalers can only be changed when disabled**
5. ⚠️ **Keep Enable and Stop2_N high during operation**
### Common Bit Patterns
```rust
// Enable + Stop2_N
0b0000_0011
// Enable + Stop2_N + Speed Control
0b0001_1011
// Enable + Stop2_N + Speed Control + Start
0b0001_1111
```
### Debug Commands
```bash
# View byte values
println!("C1: {:#010b}", dev.rxpdo.c1);
println!("S1: {:#010b}", dev.txpdo.s1);
# Check specific flags
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
println!("Error: {}", s2.has_flag(S2Flag::Error));
println!("OnSpeed: {}", s2.has_flag(S2Flag::OnSpeed));
# Monitor state
println!("State: {:?}", dev.state);
println!("Initialized: {}", dev.initialized);
```
---
## Additional Resources
- **Wago Manual:** Section 2.1.2.3 (LEDs), 3.3 (Error Codes)
- **WAGO I/O CHECK:** Configuration software for 750-671 application selector
- **EtherCAT Specification:** www.ethercat.org
- **Code Location:**
- Device: `devices/wago_modules/wago_750_672.rs`
- Wrapper: `io/stepper_velocity_wago_750_672.rs`
---
## Changelog
| Version | Date | Changes |
| ------- | ------- | ------------------------------------------- |
| 1.0 | 2025 | Initial implementation with state machine |
| 2.0 | 2026-02 | Added comprehensive developer documentation |
---
**Need help?** Check the troubleshooting section or add debug logging using the template above.
+17 -1
View File
@@ -136,7 +136,7 @@ A slave recognizes message data as one message when a **3.5 character long no-da
# Inverter settings
## Motor Constants
## Motor Constants for ExtruderV2:
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
@@ -151,6 +151,22 @@ The following is a table that shows which Registers need to be set on the Invert
| `3` | Base frequency | 5000 (50 Hz) |
| `19` | Base frequency voltage | 400 (Volt) |
## Motor Constants for ExtruderV3:
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
| Parameter number (Pr) | Name | Value |
| --------------------- | ---------------------------- | ------------- |
| `71` | Applied Motor | 0 |
| `80` | Motor Capacity | 22 (2.2 kw) |
| `96` | Auto tuning setting/status | 1 |
| `1` | Max Frequency | 5000 |
| `2` | Min Frequency | 0 |
| `9` | Electronic Thermal O/L Relay | 4.40 (Ampere) |
| `3` | Base frequency | 5000 (50 Hz) |
| `19` | Base frequency voltage | 400 (Volt) |
## All Settings needed for the Inverter Communication/Operation with Extruder
The Following settings are required to get the inverter Communication and Operation working properly
+52 -12
View File
@@ -1,8 +1,8 @@
# REST API `/api/v2`
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
The panel behaves like any other NetworkManager-managed linux and is able to either receive an IP-Address over DHCP or a statically defined one. Where the static IP is most likely the easiest to work with.
- **Base URL:** `http://10.10.10.1:3001`
- **Base URL:** `https://IP_ADDRESS`
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
@@ -10,17 +10,57 @@ The Qitech Control Panel exposes a small HTTP interface for discovering machines
## Authentication
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
The panel performs Basic-Auth authentication.
For EVERY request made over HTTPS, Basic Auth is required.
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
To set the password on the Panel for Basic Auth, do the following:
1. Open a Terminal
2. cd /home/qitech/control
3. ./credentials.sh
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
The credentials.sh script sets and prints the username and password for the api ONCE in the console.
Afterwards its accessible in machine_password in /tmp/ until the machine shuts down.
If you ever forget your password you can simply run ./credentials.sh again, resetting the password used, however the username will always be machine
<p align="center">
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
</p>
Example request with curl:
```
# username=machine
# password=VoMZKZK4K4ZIvON+OU2ZcCXy
# With -k you can ignore curl's self signed certificate error
curl https://192.168.130.195/api/v2/machine -k -u machine:VoMZKZK4K4ZIvON+OU2ZcCXy
```
## Handling Self signed Certificate Warnings
For HTTPS we use self-signed certificates.
These are untrusted by default in browsers,tools like curl and https libraries.
For most programs you can either ignore this warning or you can manually trust the certificate on your system.
How these are installed depends on your system.
On the panel pc, the root Certificate is located at: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
You need to copy this file to an usb drive or copy it some other way and add it as a trusted root certificate in your OS.
Please refer to your OS' documentation or search the Internet about how to install a trusted SSL certificate.
## Bypassing HTTPS and Authentication
If you have an isolated network and dont wish to perform Authentication, then you can achieve this by:
1. Forking our repository
2. Changing the Open Ports in nixos/os/configuration.nix:
```
# All the way at the Bottom
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
# Change this
networking.firewall.allowedTCPPorts = [ 443 ];
# To this
networking.firewall.allowedTCPPorts = [ 3001 ];
```
Again, you should only ever do this if you have your own reverse proxy with authentication or you have an isolated network.
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
---
@@ -38,7 +78,7 @@ Each machine also includes a `legacy_id` to support older **v1** workflows. If a
### Example request
```bash
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
curl -X GET "https://IP_ADDRESS/api/v2/machine" -u machine:YOUR_PASSWORD
```
### Example response
@@ -119,7 +159,7 @@ To receive continuous updates (via WebSockets), subscribe to the machine namespa
### Example request (mock machine)
```bash
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
curl -X GET "https://IP_ADDRESS/api/v2/machine/mock/57922" -u machine:YOUR_PASSWORD
```
### Example response
@@ -174,7 +214,7 @@ The API does **not** return the newly-applied state in the POST response. The pa
curl -X POST \
-d \
-H "Content-Type: application/json" \
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
"https://IP_ADDRESS/api/v1/machine/mock/57922" -u machine:YOUR_PASSWORD
```
### Example response
+148 -359
View File
File diff suppressed because it is too large. Load diff
+8 -8
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": {
@@ -34,9 +34,9 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron": "^40.6.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"
}
}
-4
View File
@@ -8,15 +8,11 @@ import { router } from "./routes/router";
import { RouterProvider } from "@tanstack/react-router";
import { Toaster } from "./components/ui/sonner";
import { enableMapSet } from "immer";
import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming";
import { GlobalLaserToastManager } from "./setup/GlobalLaserToastManager";
export default function App() {
const { i18n } = useTranslation();
// Start global log streaming
useGlobalLogStreaming();
useEffect(() => {
syncThemeWithLocal();
updateAppLanguage(i18n);
+12 -3
View File
@@ -20,6 +20,7 @@ import { TouchButton } from "../touch/TouchButton";
import { seriesToUPlotData } from "@/lib/timeseries";
import { ControlCard } from "@/control/ControlCard";
import { getAllTimeSeries } from "./createChart";
import { TargetDashOverlay } from "./TargetDashOverlay";
// Collects lines connected to visible series for rendering
const getVisibleLines = (data: any, visibleSeries: boolean[]) => {
@@ -595,10 +596,18 @@ export function BigGraph({
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
<div
ref={containerRef}
className={`h-full w-full overflow-hidden transition-opacity duration-100`}
className={`relative h-full w-full overflow-hidden transition-opacity duration-100`}
style={{ backgroundColor: colors.background }}
/>
>
<div ref={containerRef} className="h-full w-full overflow-hidden" />
<TargetDashOverlay
uplotRef={uplotRef}
newData={newData}
config={enhancedConfig}
selectedTimeWindow={selectedTimeWindow}
isLiveMode={isLiveMode}
/>
</div>
</div>
</div>
</div>
@@ -80,7 +80,7 @@ export function GraphControls({
)}
<TouchButton
onClick={onSwitchToHistorical}
onClick={() => onSwitchToHistorical("button")}
variant="outline"
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
!isLiveMode
@@ -197,7 +197,7 @@ export function FloatingControlPanel({
)}
<TouchButton
onClick={onSwitchToHistorical}
onClick={() => onSwitchToHistorical("button")}
variant="outline"
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
!isLiveMode
@@ -0,0 +1,411 @@
import React, { useEffect, useMemo, useState, useId } from "react";
import uPlot from "uplot";
import { BigGraphProps, GraphConfig } from "./types";
import { getAllTimeSeries } from "./createChart";
type OverlayLine = {
key: string;
color: string;
width: number;
dash: number[];
d: string;
};
type ClipRect = {
x: number;
y: number;
width: number;
height: number;
};
function findFirstGe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = arr.length;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] >= value) {
ans = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
}
function findLastLe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] <= value) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
function buildSteppedPath(
u: uPlot,
xData: number[],
yData: Array<number | null>,
): string {
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xData.length < 2 || xMin === undefined || xMax === undefined) {
return "";
}
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
const lastVisibleIdx = Math.min(
xData.length - 1,
findLastLe(xData, xMax) + 1,
);
if (lastVisibleIdx < firstVisibleIdx) {
return "";
}
const parts: string[] = [];
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = firstVisibleIdx; i <= lastVisibleIdx; 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 (!started) {
parts.push(`M ${x} ${y}`);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
parts.push(`L ${x} ${prevY}`);
}
if (y !== prevY) {
parts.push(`L ${x} ${y}`);
}
prevX = x;
prevY = y;
}
return parts.join(" ");
}
function buildFlatLatestValuePath(
u: uPlot,
yData: Array<number | null>,
): string {
let latestValue: number | null = null;
for (let i = yData.length - 1; i >= 0; i--) {
const value = yData[i];
if (value !== null && value !== undefined) {
latestValue = value;
break;
}
}
if (latestValue === null) {
return "";
}
const y = u.valToPos(latestValue, "y", true);
const left = u.bbox.left;
const right = u.bbox.left + u.bbox.width;
return `M ${left} ${y} L ${right} ${y}`;
}
function getHistoricalDashTargets(
data: BigGraphProps["newData"],
config: GraphConfig,
): Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> {
const allOriginalSeries = getAllTimeSeries(data);
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
let visibleLineIndex = 0;
const targets: Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> = [];
config.lines?.forEach((line) => {
if (line.show === false) return;
const dash = line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
targets.push({
dataIndex: firstConfigLineDataIndex + visibleLineIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
visibleLineIndex++;
});
return targets;
}
function areDashArraysEqual(a: number[], b: number[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function areOverlayLinesEqual(a: OverlayLine[], b: OverlayLine[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (
a[i].key !== b[i].key ||
a[i].color !== b[i].color ||
a[i].width !== b[i].width ||
a[i].d !== b[i].d ||
!areDashArraysEqual(a[i].dash, b[i].dash)
) {
return false;
}
}
return true;
}
function isSameClipRect(a: ClipRect | null, b: ClipRect): boolean {
return (
!!a &&
a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height
);
}
export function TargetDashOverlay({
uplotRef,
newData,
config,
selectedTimeWindow,
isLiveMode,
}: {
uplotRef: React.RefObject<uPlot | null>;
newData: BigGraphProps["newData"];
config: GraphConfig;
selectedTimeWindow: number | "all";
isLiveMode: boolean;
}) {
const targetMeta = useMemo(
() => getHistoricalDashTargets(newData, config),
[newData, config],
);
const clipPathId = useId().replace(/:/g, "");
const [lines, setLines] = useState<OverlayLine[]>([]);
const [clipRect, setClipRect] = useState<ClipRect | null>(null);
useEffect(() => {
if (targetMeta.length === 0) {
setLines([]);
setClipRect(null);
}
}, [targetMeta]);
useEffect(() => {
let rafId: number | null = null;
let bootstrapRafId: number | null = null;
let recalcScheduled = false;
let hooksAttached = false;
const removeHookFns: Array<() => void> = [];
let resizeObserver: ResizeObserver | null = null;
const recalc = () => {
recalcScheduled = false;
const u = uplotRef.current;
if (!u || targetMeta.length === 0) {
return;
}
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length < 2) {
return;
}
const nextLines = targetMeta
.map((meta, index) => {
const yData = u.data[meta.dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length < 2) return null;
const useFlatShortWindowPath =
isLiveMode &&
selectedTimeWindow !== "all" &&
selectedTimeWindow <= 60_000;
const d = useFlatShortWindowPath
? buildFlatLatestValuePath(u, yData)
: buildSteppedPath(u, xData, yData);
if (!d) return null;
return {
key: `${meta.dataIndex}-${index}`,
color: meta.color,
width: meta.width,
dash: meta.dash,
d,
} as OverlayLine;
})
.filter((line): line is OverlayLine => !!line);
if (nextLines.length > 0) {
setLines((prev) =>
areOverlayLinesEqual(prev, nextLines) ? prev : nextLines,
);
const nextClipRect: ClipRect = {
x: u.bbox.left,
y: u.bbox.top,
width: u.bbox.width,
height: u.bbox.height,
};
setClipRect((prev) =>
isSameClipRect(prev, nextClipRect) ? prev : nextClipRect,
);
}
};
const scheduleRecalc = () => {
if (recalcScheduled) return;
recalcScheduled = true;
rafId = window.requestAnimationFrame(recalc);
};
scheduleRecalc();
if (targetMeta.length === 0) {
return;
}
const addHook = (u: any, hookName: string, fn: () => void) => {
const hooks = u.hooks?.[hookName];
if (!Array.isArray(hooks)) return;
hooks.push(fn);
removeHookFns.push(() => {
const idx = hooks.indexOf(fn);
if (idx >= 0) hooks.splice(idx, 1);
});
};
const onWindowResize = () => scheduleRecalc();
window.addEventListener("resize", onWindowResize);
const attachHooks = (): boolean => {
if (hooksAttached) return true;
const u = uplotRef.current as any;
if (!u) return false;
addHook(u, "draw", scheduleRecalc);
addHook(u, "setScale", scheduleRecalc);
addHook(u, "setData", scheduleRecalc);
const rootElement = u.root as HTMLElement | undefined;
if (rootElement && typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(() => scheduleRecalc());
resizeObserver.observe(rootElement);
}
hooksAttached = true;
scheduleRecalc();
return true;
};
if (!attachHooks()) {
const tryAttach = () => {
if (attachHooks()) return;
bootstrapRafId = window.requestAnimationFrame(tryAttach);
};
bootstrapRafId = window.requestAnimationFrame(tryAttach);
}
return () => {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
if (bootstrapRafId !== null) {
window.cancelAnimationFrame(bootstrapRafId);
}
removeHookFns.forEach((fn) => fn());
window.removeEventListener("resize", onWindowResize);
resizeObserver?.disconnect();
};
}, [uplotRef, targetMeta, isLiveMode, selectedTimeWindow]);
if (lines.length === 0) {
return null;
}
return (
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
aria-hidden
>
{clipRect && (
<defs>
<clipPath id={clipPathId}>
<rect
x={clipRect.x}
y={clipRect.y}
width={clipRect.width}
height={clipRect.height}
/>
</clipPath>
</defs>
)}
<g clipPath={clipRect ? `url(#${clipPathId})` : undefined}>
{lines.map((line) => {
return (
<path
key={line.key}
d={line.d}
fill="none"
stroke={line.color}
strokeWidth={line.width}
strokeLinecap="butt"
strokeDasharray={line.dash.join(" ")}
strokeDashoffset={0}
/>
);
})}
</g>
</svg>
);
}
+16 -3
View File
@@ -5,8 +5,10 @@ import {
AnimationRefs,
AnimationState,
BigGraphProps,
GraphLine,
SeriesData,
} from "./types";
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
export function useAnimationRefs(): AnimationRefs {
const animationFrameRef = useRef<number | null>(null);
@@ -50,7 +52,9 @@ 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[][],
): uPlot.AlignedData {
const uData: uPlot.AlignedData = [timestamps];
@@ -72,7 +76,14 @@ export function buildUPlotData(
// Add config lines
config.lines?.forEach((line) => {
if (line.show !== false) {
uData.push(timestamps.map(() => line.value));
const lineData = line.targetSeries
? alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
)
: timestamps.map(() => line.value);
uData.push(lineData);
}
});
@@ -88,7 +99,9 @@ export function animateNewPoint(
viewMode: string,
selectedTimeWindow: number | "all",
startTimeRef: React.RefObject<number | null>,
config: { lines?: Array<{ show?: boolean; value: number }> },
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
getAllSeriesData?: () => number[][],
): void {
+22 -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,
@@ -59,7 +63,13 @@ export function createChart({
// Add config lines data
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = new Array(timestamps.length).fill(line.value);
const lineData = line.targetSeries
? alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
)
: new Array(timestamps.length).fill(line.value);
uPlotData.push(lineData as any);
}
});
@@ -169,11 +179,16 @@ export function createChart({
// Add config lines
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
seriesConfig.push({
label: line.label,
stroke: line.color,
// Historical dashed targets are rendered in an SVG overlay.
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 },
});
@@ -512,7 +527,9 @@ export function createChart({
values: [...primaryValues],
};
return cleanup;
return () => {
cleanup();
};
}
// Helper function to get all valid TimeSeries from DataSeries
@@ -3,7 +3,12 @@ import { useRef, useCallback } from "react";
import uPlot from "uplot";
import { seriesToUPlotData } from "@/lib/timeseries";
import { getPrimarySeriesData, stopAnimations } from "./animation";
import { BigGraphProps, HistoricalModeHandlers, AnimationRefs } from "./types";
import {
BigGraphProps,
HistoricalModeHandlers,
AnimationRefs,
SwitchOrigin,
} from "./types";
export function useHistoricalMode({
newData,
@@ -120,12 +125,43 @@ export function useHistoricalMode({
);
// Switch to historical mode
const switchToHistoricalMode = useCallback(() => {
isInHistoricalModeRef.current = true;
captureHistoricalFreezeTimestamp();
stopAnimations(animationRefs);
lastProcessedCountRef.current = 0; // Reset processed count
}, [captureHistoricalFreezeTimestamp, animationRefs, lastProcessedCountRef]);
const switchToHistoricalMode = useCallback(
(origin?: SwitchOrigin) => {
isInHistoricalModeRef.current = true;
// Preserve current scale
if (origin === "gesture") {
try {
if (uplotRef.current) {
const xScale = uplotRef.current.scales.x;
const yScale = uplotRef.current.scales.y;
manualScaleRef.current = {
x: {
min: xScale?.min ?? 0,
max: xScale?.max ?? 0,
},
y: {
min: yScale?.min ?? 0,
max: yScale?.max ?? 1,
},
};
}
} catch (err) {
console.warn(
"Error preserving chart scales when switching to historical mode:",
err,
);
}
} else {
manualScaleRef.current = null;
}
captureHistoricalFreezeTimestamp();
stopAnimations(animationRefs);
lastProcessedCountRef.current = 0; // Reset processed count
},
[captureHistoricalFreezeTimestamp, animationRefs, lastProcessedCountRef],
);
// Switch back to live mode
const switchToLiveMode = useCallback(() => {
+6 -2
View File
@@ -3,6 +3,8 @@ import { Unit } from "@/control/units";
import { TimeSeries } from "@/lib/timeseries";
import { RefObject } from "react";
export type SwitchOrigin = "button" | "gesture";
// Prop-based sync types
export type PropGraphSync = {
timeWindow: number | "all";
@@ -11,6 +13,7 @@ export type PropGraphSync = {
xRange?: { min: number; max: number };
historicalFreezeTimestamp?: number | null;
showFromTimestamp?: number | null;
historicalSwitchOrigin: SwitchOrigin;
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
onViewModeChange?: (
graphId: string,
@@ -27,6 +30,7 @@ export type PropGraphSync = {
export type GraphLine = {
type: "threshold" | "target";
value: number;
targetSeries?: TimeSeries;
color: string;
label?: string;
width?: number;
@@ -83,7 +87,7 @@ export type ControlProps = {
isLiveMode: boolean;
onTimeWindowChange: (timeWindow: number | "all") => void;
onSwitchToLive: () => void;
onSwitchToHistorical: () => void;
onSwitchToHistorical: (origin: SwitchOrigin) => void;
onExport?: () => void;
timeWindowOptions?: TimeWindowOption[];
showFromTimestamp?: number | null;
@@ -100,7 +104,7 @@ export interface HistoricalModeHandlers {
captureHistoricalFreezeTimestamp: () => number;
getHistoricalEndTimestamp: () => number;
handleHistoricalTimeWindow: (timeWindow: number | "all") => void;
switchToHistoricalMode: () => void;
switchToHistoricalMode: (origin?: SwitchOrigin) => void;
switchToLiveMode: () => void;
}
@@ -175,7 +175,7 @@ export function useBigGraphEffects({
const newIsLiveMode = syncGraph.isLiveMode;
if (lastState.isLiveMode && !newIsLiveMode) {
historicalMode.switchToHistoricalMode();
historicalMode.switchToHistoricalMode(syncGraph.historicalSwitchOrigin);
} else if (!lastState.isLiveMode && newIsLiveMode) {
historicalMode.switchToLiveMode();
+10 -1
View File
@@ -1,5 +1,5 @@
import { useState, useCallback, useRef } from "react";
import { PropGraphSync } from "./types";
import { PropGraphSync, SwitchOrigin } from "./types";
import { GraphExportData, exportGraphsToExcel } from "./excelExport";
import { useGraphSettingsStore } from "@/stores/graphSettingsStore";
@@ -19,6 +19,7 @@ export function useGraphSync(exportGroupId?: string) {
null,
);
const historicalSwitchOriginRef = useRef<SwitchOrigin>("gesture");
const historicalFreezeTimestampRef = useRef<number | null>(null);
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
new Map(),
@@ -65,6 +66,7 @@ export function useGraphSync(exportGroupId?: string) {
showFromTimestamp?: number | null;
clearHistoricalFreeze?: boolean;
setHistoricalFreeze?: boolean;
historicalSwitchOrigin?: SwitchOrigin;
},
requestId?: number,
) => {
@@ -104,6 +106,10 @@ export function useGraphSync(exportGroupId?: string) {
historicalFreezeTimestampRef.current = Date.now();
}
if (updates.historicalSwitchOrigin !== undefined) {
historicalSwitchOriginRef.current = updates.historicalSwitchOrigin;
}
clearChangeSource();
});
},
@@ -164,6 +170,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode: "manual",
isLiveMode: false,
setHistoricalFreeze: historicalFreezeTimestampRef.current === null,
historicalSwitchOrigin: "gesture",
});
},
[updateSyncState],
@@ -210,6 +217,7 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode: false,
viewMode: "manual",
setHistoricalFreeze: true,
historicalSwitchOrigin: "button",
});
}, [updateSyncState]);
@@ -242,6 +250,7 @@ export function useGraphSync(exportGroupId?: string) {
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
historicalSwitchOrigin: historicalSwitchOriginRef.current,
onTimeWindowChange: handleTimeWindowChange,
onViewModeChange: handleViewModeChange,
onZoomChange: handleZoomChangeThrottled,
@@ -1,5 +1,3 @@
export const TROUBLESHOOT_REBOOT_HMI = "troubleshoot:reboot-hmi";
export const TROUBLESHOOT_RESTART_BACKEND = "troubleshoot:restart-backend";
export const TROUBLESHOOT_START_LOG_STREAM = "troubleshoot:start-log-stream";
export const TROUBLESHOOT_STOP_LOG_STREAM = "troubleshoot:stop-log-stream";
export const TROUBLESHOOT_LOG_DATA = "troubleshoot:log-data";
export const TROUBLESHOOT_EXPORT_LOGS = "troubleshoot:export-logs";
@@ -1,9 +1,7 @@
import {
TROUBLESHOOT_REBOOT_HMI,
TROUBLESHOOT_RESTART_BACKEND,
TROUBLESHOOT_START_LOG_STREAM,
TROUBLESHOOT_STOP_LOG_STREAM,
TROUBLESHOOT_LOG_DATA,
TROUBLESHOOT_EXPORT_LOGS,
} from "./troubleshoot-channels";
export function exposeTroubleshootContext() {
@@ -11,11 +9,6 @@ export function exposeTroubleshootContext() {
contextBridge.exposeInMainWorld("troubleshoot", {
rebootHmi: () => ipcRenderer.invoke(TROUBLESHOOT_REBOOT_HMI),
restartBackend: () => ipcRenderer.invoke(TROUBLESHOOT_RESTART_BACKEND),
startLogStream: () => ipcRenderer.invoke(TROUBLESHOOT_START_LOG_STREAM),
stopLogStream: () => ipcRenderer.invoke(TROUBLESHOOT_STOP_LOG_STREAM),
onLogData: (callback: (log: string) => void) =>
ipcRenderer.on(TROUBLESHOOT_LOG_DATA, (_event, log: string) => {
callback(log);
}),
exportLogs: () => ipcRenderer.invoke(TROUBLESHOOT_EXPORT_LOGS),
});
}
@@ -1,14 +1,13 @@
import { ipcMain } from "electron";
import { spawn, ChildProcess } from "child_process";
import { ipcMain, dialog } from "electron";
import { spawn, exec, ChildProcess } from "child_process";
import {
TROUBLESHOOT_REBOOT_HMI,
TROUBLESHOOT_RESTART_BACKEND,
TROUBLESHOOT_START_LOG_STREAM,
TROUBLESHOOT_STOP_LOG_STREAM,
TROUBLESHOOT_LOG_DATA,
TROUBLESHOOT_EXPORT_LOGS,
} from "./troubleshoot-channels";
let logStreamProcess: ChildProcess | null = null;
import fs from "fs";
import path from "path";
export function addTroubleshootEventListeners() {
ipcMain.handle(TROUBLESHOOT_REBOOT_HMI, async () => {
@@ -60,62 +59,45 @@ export function addTroubleshootEventListeners() {
}
});
ipcMain.handle(TROUBLESHOOT_START_LOG_STREAM, async (event) => {
ipcMain.handle(TROUBLESHOOT_EXPORT_LOGS, async () => {
try {
// Stop any existing log stream
if (logStreamProcess) {
logStreamProcess.kill();
logStreamProcess = null;
const now = new Date();
const datePart = now.toISOString().split("T")[0]; // YYYY-MM-DD
const timePart = now.toTimeString().split(" ")[0].replace(/:/g, "-"); // HH-mm-ss
const fileName = `journal_${datePart}_${timePart}.log`;
// 1. Open a Save Dialog so the user can choose the location
const { canceled, filePath } = await dialog.showSaveDialog({
title: "Export System Logs",
defaultPath: fileName,
filters: [{ name: "Log Files", extensions: ["log"] }],
});
if (canceled || !filePath) {
return { success: false, error: "Export cancelled by user" };
}
logStreamProcess = spawn(
"journalctl",
["-u", "qitech-control-server", "-n", "10000", "-f"],
{
shell: true,
// 2. Wrap the exec in a typed Promise to match the backend restart pattern
// This resolves the TS2794 error by explicitly defining the return type
return await new Promise<{ success: boolean; error?: string }>(
(resolve) => {
// Note: journalctl -xb usually requires sudo or journal group membership
exec(`journalctl -xb > "${filePath}"`, (error, stdout, stderr) => {
if (error) {
console.error("Exec error:", error);
resolve({
success: false,
error: error instanceof Error ? error.message : String(error),
});
return;
}
resolve({ success: true });
});
},
);
logStreamProcess.stdout?.on("data", (data) => {
const logLine = data.toString();
event.sender.send(TROUBLESHOOT_LOG_DATA, logLine);
});
logStreamProcess.stderr?.on("data", (data) => {
const logLine = data.toString();
event.sender.send(TROUBLESHOOT_LOG_DATA, logLine);
});
logStreamProcess.on("close", (code) => {
console.log(`Log stream process exited with code ${code}`);
logStreamProcess = null;
});
logStreamProcess.on("error", (error) => {
console.error("Log stream process error:", error);
logStreamProcess = null;
});
return { success: true };
} catch (error) {
console.error("Failed to start log stream:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
});
ipcMain.handle(TROUBLESHOOT_STOP_LOG_STREAM, async () => {
try {
if (logStreamProcess) {
logStreamProcess.kill();
logStreamProcess = null;
console.log("Log stream stopped");
}
return { success: true };
} catch (error) {
console.error("Failed to stop log stream:", error);
console.error("Failed to export logs: ", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
+3 -32
View File
@@ -39,12 +39,12 @@ export async function restartBackend(): Promise<{
}
}
export async function startLogStream(): Promise<{
export async function exportLogs(): Promise<{
success: boolean;
error?: string;
}> {
try {
return await window.troubleshoot.startLogStream();
return await window.troubleshoot.exportLogs();
} catch (error) {
return {
success: false,
@@ -53,35 +53,6 @@ export async function startLogStream(): Promise<{
}
}
export async function stopLogStream(): Promise<{
success: boolean;
error?: string;
}> {
try {
return await window.troubleshoot.stopLogStream();
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Setup log data streaming with callback
* @param onLogData Callback function to handle incoming log data
*/
export function setupLogDataListener(onLogData: (log: string) => void): void {
window.troubleshoot.onLogData(onLogData);
}
/**
* Cleanup log data listener
*/
export function cleanupLogDataListener(): void {
window.troubleshoot.onLogData(() => {});
}
/**
* Combined function to restart backend and show appropriate toast messages
*/
@@ -99,10 +70,10 @@ export async function restartBackendWithToast(): Promise<boolean> {
/**
* Combined function to reboot HMI and show appropriate toast messages
* ?????? How would you see this?
*/
export async function rebootHmiWithToast(): Promise<boolean> {
const result = await rebootHmi();
if (result.success) {
return true;
} else {
@@ -1,78 +0,0 @@
import { useEffect } from "react";
import { useLogsStore } from "@/stores/logsStore";
import {
startLogStream,
stopLogStream,
setupLogDataListener,
isTroubleshootAvailable,
} from "@/helpers/troubleshoot_helpers";
/**
* Hook to manage global log streaming
* This should be used once at the app level to start log streaming
*/
export function useGlobalLogStreaming() {
const { isStreaming, addLogEntry, setStreaming } = useLogsStore();
useEffect(() => {
// Only proceed if troubleshoot context is available
if (!isTroubleshootAvailable()) {
return;
}
if (!isStreaming) {
startLogStreamHelper();
}
// Setup log data listener
const handleLogData = (logData: string) => {
// Parse systemd journal log format
const lines = logData.split("\n").filter((line) => line.trim());
lines.forEach((line) => {
if (line.trim()) {
// Simple log level detection
let level: "info" | "warn" | "error" | "debug" = "info";
if (line.includes("ERROR") || line.includes("error")) level = "error";
else if (line.includes("WARN") || line.includes("warn"))
level = "warn";
else if (line.includes("DEBUG") || line.includes("debug"))
level = "debug";
addLogEntry({
source: "qitech-control-server",
level,
message: line.trim(),
raw: line,
});
}
});
};
// Setup log data listener using helper
setupLogDataListener(handleLogData);
return () => {
// Cleanup on unmount
if (isStreaming && isTroubleshootAvailable()) {
stopLogStream();
setStreaming(false);
}
};
}, [isStreaming, addLogEntry, setStreaming]);
const startLogStreamHelper = async () => {
try {
const result = await startLogStream();
if (result.success) {
setStreaming(true);
} else {
console.warn("Failed to start log stream:", result.error);
}
} catch (error) {
console.warn("Failed to start log stream:", error);
}
};
return { isStreaming };
}
+43
View File
@@ -304,3 +304,46 @@ 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).
*/
export function alignTargetSeriesToTimestamps(
targetSeries: TimeSeries,
dataTimestamps: number[],
fallbackValue: number,
): number[] {
if (dataTimestamps.length === 0) {
return [];
}
const [targetTimestamps, targetValues] = extractDataFromSeries(
targetSeries.long,
);
if (targetTimestamps.length === 0) {
return new Array(dataTimestamps.length).fill(fallbackValue);
}
const result: number[] = [];
let targetIndex = 0;
for (const dataTs of dataTimestamps) {
while (
targetIndex < targetTimestamps.length - 1 &&
targetTimestamps[targetIndex + 1] <= dataTs
) {
targetIndex++;
}
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",
+17 -14
View File
@@ -16,6 +16,7 @@ type Props = {
min: number;
max: number;
onChangeTargetTemp?: (temperature: number) => void;
targetTemperatureEnabled: boolean;
};
export function HeatingZone({
title,
@@ -25,6 +26,7 @@ export function HeatingZone({
heatingTimeSeries,
heatingPower,
onChangeTargetTemp,
targetTemperatureEnabled,
}: Props) {
const targetTemperature = heatingState?.target_temperature ?? 150;
@@ -41,19 +43,20 @@ export function HeatingZone({
timeseries={heatingTimeSeries}
/>
</div>
<Label label="Target Temperature">
<EditValue
value={targetTemperature}
defaultValue={150}
min={min}
max={max}
unit="C"
title="Target Temperature"
renderValue={(value) => roundToDecimals(value, 1)}
onChange={onChangeTargetTemp}
/>
</Label>
{targetTemperatureEnabled && (
<Label label="Target Temperature">
<EditValue
value={targetTemperature}
defaultValue={150}
min={min}
max={max}
unit="C"
title="Target Temperature"
renderValue={(value) => roundToDecimals(value, 1)}
onChange={onChangeTargetTemp}
/>
</Label>
)}
</div>
<TimeSeriesValueNumeric
@@ -65,7 +68,7 @@ export function HeatingZone({
timeseries={heatingPower}
/>
{heatingState?.wiring_error && (
{heatingState?.wiring_error && targetTemperatureEnabled && (
<StatusBadge variant="error">
Cant Read Temperature! Check Temperature Sensor Wiring!
</StatusBadge>
@@ -68,10 +68,12 @@ export function Extruder2ControlPage() {
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
const nozzleReady =
!state?.extruder_settings_state.nozzle_temperature_target_enabled ||
isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
@@ -87,6 +89,7 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Middle"}
@@ -96,6 +99,7 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Back"}
@@ -105,6 +109,7 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -114,6 +119,10 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
@@ -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,
},
@@ -20,6 +20,7 @@ export function Extruder2SettingsPage() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder2();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -68,6 +69,16 @@ export function Extruder2SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setTemperatureTargetEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
@@ -109,6 +109,7 @@ export const heatingStatesSchema = z.object({
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
});
/**
@@ -221,6 +222,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 +262,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 +302,17 @@ export function extruder2MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
const nextTargetScrewRpm = stateEvent.data.screw_state.target_rpm;
const nextTargetNozzleTemperature =
stateEvent.data.heating_states.nozzle.target_temperature;
const nextTargetFrontTemperature =
stateEvent.data.heating_states.front.target_temperature;
const nextTargetMiddleTemperature =
stateEvent.data.heating_states.middle.target_temperature;
const nextTargetBackTemperature =
stateEvent.data.heating_states.back.target_temperature;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +320,53 @@ export function extruder2MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure:
state.targetPressure.current?.value === nextTargetPressure
? state.targetPressure
: addTargetPressure(state.targetPressure, {
value: nextTargetPressure,
timestamp,
}),
targetScrewRpm:
state.targetScrewRpm.current?.value === nextTargetScrewRpm
? state.targetScrewRpm
: addTargetScrewRpm(state.targetScrewRpm, {
value: nextTargetScrewRpm,
timestamp,
}),
targetNozzleTemperature:
state.targetNozzleTemperature.current?.value ===
nextTargetNozzleTemperature
? state.targetNozzleTemperature
: addTargetNozzleTemperature(state.targetNozzleTemperature, {
value: nextTargetNozzleTemperature,
timestamp,
}),
targetFrontTemperature:
state.targetFrontTemperature.current?.value ===
nextTargetFrontTemperature
? state.targetFrontTemperature
: addTargetFrontTemperature(state.targetFrontTemperature, {
value: nextTargetFrontTemperature,
timestamp,
}),
targetMiddleTemperature:
state.targetMiddleTemperature.current?.value ===
nextTargetMiddleTemperature
? state.targetMiddleTemperature
: addTargetMiddleTemperature(state.targetMiddleTemperature, {
value: nextTargetMiddleTemperature,
timestamp,
}),
targetBackTemperature:
state.targetBackTemperature.current?.value ===
nextTargetBackTemperature
? state.targetBackTemperature
: addTargetBackTemperature(state.targetBackTemperature, {
value: nextTargetBackTemperature,
timestamp,
}),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +467,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
@@ -224,6 +232,20 @@ export function useExtruder2() {
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
machine_identification_unique: machineIdentification,
data: { SetNozzleTemperatureTargetEnabled: enabled },
}),
);
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
@@ -373,6 +395,10 @@ export function useExtruder2() {
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestNozzleTemperatureTargetEnabled } = useMachineMutation(
z.object({ SetNozzleTemperatureTargetEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
@@ -423,6 +449,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,
@@ -443,6 +477,7 @@ export function useExtruder2() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
resetInverter,
};
}
@@ -68,10 +68,12 @@ export function Extruder3ControlPage() {
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
const nozzleReady =
!state?.extruder_settings_state.nozzle_temperature_target_enabled ||
isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
@@ -87,6 +89,7 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Middle"}
@@ -96,6 +99,7 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Back"}
@@ -105,6 +109,7 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -114,6 +119,10 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
@@ -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,
},
@@ -20,6 +20,7 @@ export function Extruder3SettingsPage() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -68,6 +69,16 @@ export function Extruder3SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setTemperatureTargetEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
@@ -109,6 +109,7 @@ export const heatingStatesSchema = z.object({
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
});
/**
@@ -221,6 +222,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 +262,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 +302,17 @@ export function extruder3MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
const nextTargetScrewRpm = stateEvent.data.screw_state.target_rpm;
const nextTargetNozzleTemperature =
stateEvent.data.heating_states.nozzle.target_temperature;
const nextTargetFrontTemperature =
stateEvent.data.heating_states.front.target_temperature;
const nextTargetMiddleTemperature =
stateEvent.data.heating_states.middle.target_temperature;
const nextTargetBackTemperature =
stateEvent.data.heating_states.back.target_temperature;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +320,53 @@ export function extruder3MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure:
state.targetPressure.current?.value === nextTargetPressure
? state.targetPressure
: addTargetPressure(state.targetPressure, {
value: nextTargetPressure,
timestamp,
}),
targetScrewRpm:
state.targetScrewRpm.current?.value === nextTargetScrewRpm
? state.targetScrewRpm
: addTargetScrewRpm(state.targetScrewRpm, {
value: nextTargetScrewRpm,
timestamp,
}),
targetNozzleTemperature:
state.targetNozzleTemperature.current?.value ===
nextTargetNozzleTemperature
? state.targetNozzleTemperature
: addTargetNozzleTemperature(state.targetNozzleTemperature, {
value: nextTargetNozzleTemperature,
timestamp,
}),
targetFrontTemperature:
state.targetFrontTemperature.current?.value ===
nextTargetFrontTemperature
? state.targetFrontTemperature
: addTargetFrontTemperature(state.targetFrontTemperature, {
value: nextTargetFrontTemperature,
timestamp,
}),
targetMiddleTemperature:
state.targetMiddleTemperature.current?.value ===
nextTargetMiddleTemperature
? state.targetMiddleTemperature
: addTargetMiddleTemperature(state.targetMiddleTemperature, {
value: nextTargetMiddleTemperature,
timestamp,
}),
targetBackTemperature:
state.targetBackTemperature.current?.value ===
nextTargetBackTemperature
? state.targetBackTemperature
: addTargetBackTemperature(state.targetBackTemperature, {
value: nextTargetBackTemperature,
timestamp,
}),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +467,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
@@ -224,6 +232,28 @@ export function useExtruder3() {
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
const res = enabled
? true
: confirm(
"This will disable the ability to set a temperature target for the nozzle heating element and will any wiring error message. Only proceed if you understand the risks.",
);
if (res) {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
machine_identification_unique: machineIdentification,
data: { SetNozzleTemperatureTargetEnabled: enabled },
}),
);
}
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
@@ -373,6 +403,10 @@ export function useExtruder3() {
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestNozzleTemperatureTargetEnabled } = useMachineMutation(
z.object({ SetNozzleTemperatureTargetEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
@@ -423,6 +457,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,
@@ -439,6 +481,7 @@ export function useExtruder3() {
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
@@ -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,
},
@@ -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,
};
});
@@ -114,9 +118,17 @@ export function laser1MessageHandler(
// Apply appropriate caching strategy based on event type
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
const nextTargetDiameter = stateEvent.data.laser_state.target_diameter;
updateStore((state) => ({
...state,
state: stateEvent,
targetDiameter:
state.targetDiameter.current?.value === nextTargetDiameter
? state.targetDiameter
: addTargetDiameter(state.targetDiameter, {
value: nextTargetDiameter,
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,
+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",
@@ -579,6 +608,29 @@ export const TestMotor: MachineProperties = {
],
};
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",
@@ -603,6 +655,41 @@ export const wagoAiTestMachine: MachineProperties = {
],
};
export const testmachinestepper: MachineProperties = {
name: "TestMachineStepper",
version: "V1",
slug: "testmachinestepper",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0037,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x00000021,
product_id: 0x07500354,
revision: 0x2,
},
],
},
{
role: 1,
role_label: "Stepper Motor",
allowed_devices: [
{
vendor_id: 0x00000021,
product_id: 0x67214e8,
revision: 0x2,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
@@ -616,8 +703,11 @@ export const machineProperties: MachineProperties[] = [
analogInputTestMachine,
wagoAiTestMachine,
digitalInputTestMachine,
wago8chDioTestMachine,
ip20TestMachine,
wagoDoTestMachine,
TestMotor,
testmachinestepper,
];
export const getMachineProperties = (
@@ -0,0 +1,130 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { useTestMachineStepper } from "./useTestMachineStepper";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { TouchButton } from "@/components/touch/TouchButton";
export function TestMachineStepperControlPage() {
const { state, setTargetSpeed, setEnabled, setFreq, setAccFreq } =
useTestMachineStepper();
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Stepper Speed">
<Label label="Target Speed">
<EditValue
value={state?.target_speed}
unit="rpm"
title="Target Speed"
defaultValue={0}
min={-25000}
max={25000}
step={1}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setTargetSpeed}
/>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(true)}
disabled={false}
isLoading={false}
>
Enable
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(false)}
disabled={false}
isLoading={false}
>
Disable
</TouchButton>
</Label>
</ControlCard>
<ControlCard height={2} title="Frequency Prescaler">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
</ControlCard>
<ControlCard height={2} title="Acceleration Factor">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachineStepperPage() {
const { serial } = testMachineStepperSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachinestepper/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,81 @@
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({
target_speed: z.number(),
enabled: z.boolean(),
freq: z.number(),
acc_freq: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMachineStepperNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineStepperNamespaceStore =
(): StoreApi<TestMachineStepperNamespaceStore> =>
create<TestMachineStepperNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineStepperMessageHandler(
store: StoreApi<TestMachineStepperNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineStepperNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: TestMachineStepperNamespaceStore,
) => TestMachineStepperNamespaceStore,
) => 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 useTestMachineStepperNamespaceImplementation =
createNamespaceHookImplementation<TestMachineStepperNamespaceStore>({
createStore: createTestMachineStepperNamespaceStore,
createEventHandler: testMachineStepperMessageHandler,
});
export function useTestMachineStepperNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineStepperNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineStepperNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,126 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useTestMachineStepperNamespace,
StateEvent,
} from "./testMachineStepperNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachinestepper } from "@/machines/properties";
import { z } from "zod";
export function useTestMachineStepper() {
const { serial: serialString } = testMachineStepperSerialRoute.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: testmachinestepper.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineStepperNamespace(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 setTargetSpeed = (target: number) => {
updateStateOptimistically(
(current) => {
current.target_speed = target;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetTargetSpeed", value: { target } },
}),
);
};
const setEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.enabled = enabled;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetEnabled", value: { enabled } },
}),
);
};
const setFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetFreq", value: { factor } },
}),
);
};
const setAccFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.acc_freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAccFreq", value: { factor } },
}),
);
};
return {
state: stateOptimistic.value,
setTargetSpeed,
setEnabled,
setFreq,
setAccFreq,
};
}
@@ -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,
@@ -273,10 +277,18 @@ export function winder2MessageHandler(
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
const nextTargetPullerSpeed = stateEvent.data.puller_state.target_speed;
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed:
state.targetPullerSpeed.current?.value === nextTargetPullerSpeed
? state.targetPullerSpeed
: addTargetPullerSpeed(state.targetPullerSpeed, {
value: nextTargetPullerSpeed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
+53
View File
@@ -57,6 +57,8 @@ import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Se
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
@@ -75,6 +77,10 @@ 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
@@ -89,6 +95,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,
@@ -156,6 +174,19 @@ export const testMotorControlRoute = createRoute({
component: () => <TestMotorControlPage />,
});
export const testMachineStepperSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachinestepper/$serial",
component: () => <TestMachineStepperPage />,
});
export const testMachineStepperControlRoute = createRoute({
getParentRoute: () => testMachineStepperSerialRoute,
path: "control",
component: () => <TestMachineStepperControlPage />,
});
// Leaf route: control page
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -383,6 +414,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",
@@ -487,10 +530,18 @@ export const rootTree = RootRoute.addChildren([
testMachineSerialRoute.addChildren([testMachineControlRoute]),
testMachineStepperSerialRoute.addChildren([
testMachineStepperControlRoute,
]),
analogInputTestMachineSerialRoute.addChildren([
analogInputTestMachineControlRoute,
]),
wago8chDioTestMachineRoute.addChildren([
wago8chDioTestMachineControlRoute,
]),
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
digitalInputTestMachineSerialRoute.addChildren([
@@ -541,6 +592,8 @@ export const rootTree = RootRoute.addChildren([
wagoPower1SerialRoute.addChildren([wagoPower1ControlRoute]),
buffer1SerialRoute.addChildren([buffer1ControlRoute]),
wagoDoTestMachineSerialRoute.addChildren([wagoDoTestMachineControlRoute]),
]),
]),
]);
+38 -25
View File
@@ -1,25 +1,22 @@
import { Alert } from "@/components/Alert";
import { Page } from "@/components/Page";
import { SectionTitle } from "@/components/SectionTitle";
import { Terminal } from "@/components/Terminal";
import { TouchButton } from "@/components/touch/TouchButton";
import { useLogsStore } from "@/stores/logsStore";
import { rebootHmi, restartBackend } from "@/helpers/troubleshoot_helpers";
import {
rebootHmi,
restartBackend,
exportLogs,
} from "@/helpers/troubleshoot_helpers";
import React, { useState } from "react";
import { toast } from "sonner";
export function TroubleshootPage() {
const [isRebootLoading, setIsRebootLoading] = useState(false);
const [isRestartLoading, setIsRestartLoading] = useState(false);
const { getLogsBySource, clearLogs } = useLogsStore();
const [isExportLoading, setIsExportLoading] = useState(false);
// Perhaps we just need to clear the logs ?
// Get backend logs for display
const backendLogs = getLogsBySource("qitech-control-server");
const logLines = backendLogs.map((log) => log.raw);
const handleRebootHmi = async () => {
setIsRebootLoading(true);
try {
const result = await rebootHmi();
if (result.success) {
@@ -50,10 +47,32 @@ export function TroubleshootPage() {
}
};
const handleExportLogs = async () => {
setIsExportLoading(true);
try {
const result = await exportLogs();
if (result.success) {
toast.success("Export Logs initiated");
} else {
toast.error(`Failed to export Logs: ${result.error}`);
}
} catch (error) {
toast.error(`Failed to export Logs: ${error}`);
} finally {
setIsExportLoading(false);
}
};
return (
<Page>
<SectionTitle title="System Troubleshoot" />
<Alert title="Troubleshoot Actions Info" variant="warning">
These actions will temporarily interrupt system operations. The HMI
reboot will restart the entire panel, while the backend restart will
only restart the control service. Use with caution during production.
</Alert>
<div className="flex gap-4">
<TouchButton
variant="destructive"
@@ -74,23 +93,17 @@ export function TroubleshootPage() {
>
Restart Backend Process
</TouchButton>
<TouchButton
variant="outline"
icon="lu:Power"
isLoading={isExportLoading}
onClick={exportLogs}
className="w-max"
>
Export Backend Service Logs
</TouchButton>
</div>
<Alert title="Troubleshoot Actions Info" variant="warning">
These actions will temporarily interrupt system operations. The HMI
reboot will restart the entire panel, while the backend restart will
only restart the control service. Use with caution during production.
</Alert>
<h2 className="text-lg font-semibold">Backend Service Logs</h2>
<Terminal
lines={logLines}
autoScroll={true}
className="h-160"
title="qitech-control-server"
exportPrefix="qitech_control_server_journald"
/>
</Page>
);
}
+11 -3
View File
@@ -4,6 +4,7 @@ import { SectionTitle } from "@/components/SectionTitle";
import { Terminal } from "@/components/Terminal";
import { TouchButton } from "@/components/touch/TouchButton";
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
import { Icon } from "@/components/Icon";
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
import { useUpdateStore } from "@/stores/updateStore";
import { useSearch } from "@tanstack/react-router";
@@ -88,7 +89,7 @@ export function UpdateExecutePage() {
<Page>
<SectionTitle title="Apply Update" />
<div className="flex flex-row gap-4">
<div className="flex flex-row items-center gap-4">
<TouchButton
className="w-max"
icon="lu:CircleFadingArrowUp"
@@ -108,6 +109,14 @@ export function UpdateExecutePage() {
Cancel Update
</TouchButton>
)}
{isUpdating && (
<div className="ml-auto flex items-center gap-2 rounded-xl border border-blue-400 bg-blue-600 px-4 py-2.5 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
<Icon name="lu:Info" className="h-5 w-5 text-blue-100" />
<span className="text-base leading-snug text-blue-50">
Updates typically take approximately <strong>5 minutes</strong>
</span>
</div>
)}
</div>
{currentUpdateInfo && (
<Alert title="Update Information" variant="info">
@@ -136,8 +145,7 @@ export function UpdateExecutePage() {
</div>
<div className="text-sm">
Please stay connected to the internet and leave the power on. The
update procedure takes a couple of minutes and reboots the machine
afterwards.
machine will reboot after the update completes.
</div>
</div>
</Alert>
+1 -3
View File
@@ -58,9 +58,7 @@ interface UpdateContext {
interface TroubleshootContext {
rebootHmi: () => Promise<{ success: boolean; error?: string }>;
restartBackend: () => Promise<{ success: boolean; error?: string }>;
startLogStream: () => Promise<{ success: boolean; error?: string }>;
stopLogStream: () => Promise<{ success: boolean; error?: string }>;
onLogData: (callback: (log: string) => void) => void;
exportLogs: () => Promise<{ success: boolean; error?: string }>;
}
interface NixOSGeneration {
+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?;
+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,
+134 -54
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::{
@@ -12,6 +15,7 @@ use crate::{
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
wago_750_672::{WAGO_750_672_MODULE_IDENT, WAGO_750_672_PRODUCT_ID},
wago_750_1506::{WAGO_750_1506_MODULE_IDENT, WAGO_750_1506_PRODUCT_ID},
},
},
@@ -20,24 +24,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,
}
@@ -58,7 +63,6 @@ impl EthercatDevice for Wago750_354 {
None => break,
}
}
Ok(())
}
@@ -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,84 @@ 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,
false => 0x7000,
};
let pdo_index = (pdo_mapping & 0xFFFF0000) >> 16;
if pdo_index < start_index {
// Treat as coupler/module 0
return 0;
}
let index_in_hex = pdo_index - start_index;
if index_in_hex < 16 {
0
} else {
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 +271,7 @@ impl Wago750_354 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -229,26 +279,42 @@ 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();
}
WAGO_750_672_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-672".to_string();
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
@@ -266,17 +332,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 +393,12 @@ 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()))
}
WAGO_750_672_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_672::Wago750_672::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -312,27 +409,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,7 +1,9 @@
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;
pub mod wago_750_652;
pub mod wago_750_672;
@@ -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,
},
})
}
@@ -143,7 +143,7 @@ pub struct Wago750_1506 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
// Should always have on but not when calling constructor
// Should always have one but not when calling constructor
module: Option<Module>,
rx_pdo: Wago750_1506RxPdo,
tx_pdo: Wago750_1506TxPdo,
@@ -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(())
}
@@ -0,0 +1,387 @@
/*
* Wago Stepper Controller 750-672
* 70 VDC / 7.5 A
*/
use anyhow::Ok;
use bitvec::field::BitField;
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
io::{
digital_input::{DigitalInputDevice, DigitalInputInput},
stepper_velocity_wago_750_672::{
C1Command, C1Flag, C2Flag, C3Flag, ControlByteC1, ControlByteC2, ControlByteC3, S3Flag,
StatusByteS3,
},
},
};
#[derive(Clone)]
pub struct Wago750_672 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub rxpdo: Wago750_672RxPdo,
pub txpdo: Wago750_672TxPdo,
module: Option<Module>,
pub state: InitState,
pub initialized: bool,
}
// unfortunately we need to track and set bits over multiple cycles
// and wait for their acknowledgement so we have to use a state machine
// inside of our loop.
#[derive(Debug, Clone)]
pub enum InitState {
Off,
Enable,
SetMode,
StartPulseStart,
StartPulseEnd,
Running,
ErrorQuit,
ResetQuit,
}
/*
* Wago has two different kind of applications when it comes to
* running the Stepper Controller.
* - Positioning
* - Frequency/Speed Control
* !!!! IMPORTANT !!!!
* It seems like this is only true for the 750 671.
* The 750 672 has differnt commands inside of ControlByteC1
* where w can choose the Speed control mode - just like that
* we always want to use the latter one so the following
* process images are for that usecase.
*/
#[derive(Clone, Debug, Default)]
pub struct Wago750_672RxPdo {
pub control_byte: u8, // C0
pub velocity: i16, // D0/D1
pub acceleration: u16, // D2/D3
pub control_byte3: u8, // C3
pub control_byte2: u8, // C2
pub control_byte1: u8, // C1
}
#[derive(Clone, Debug, Default)]
pub struct Wago750_672TxPdo {
pub status_byte0: u8, // S0
pub actual_velocity: i16, // D0/D1
pub position_l: u8, // D4
pub position_m: u8, // D5
pub position_h: u8, // D6
pub status_byte3: u8, // S3
pub status_byte2: u8, // S2
pub status_byte1: u8, // S1
}
/// Digital input port enumeration for the 6 inputs
#[derive(Debug, Clone, Copy)]
pub enum Wago750_672InputPort {
DI1,
DI2,
DI3,
DI4,
DI5,
DI6,
}
// Get the Digital Inputs from the Status Byte S3
impl DigitalInputDevice<Wago750_672InputPort> for Wago750_672 {
fn get_input(&self, port: Wago750_672InputPort) -> Result<DigitalInputInput, anyhow::Error> {
let s3 = StatusByteS3::from_bits(self.txpdo.status_byte3);
Ok(DigitalInputInput {
value: match port {
Wago750_672InputPort::DI1 => s3.has_flag(S3Flag::Input1),
Wago750_672InputPort::DI2 => s3.has_flag(S3Flag::Input2),
Wago750_672InputPort::DI3 => s3.has_flag(S3Flag::Input3),
Wago750_672InputPort::DI4 => s3.has_flag(S3Flag::Input4),
Wago750_672InputPort::DI5 => s3.has_flag(S3Flag::Input5),
Wago750_672InputPort::DI6 => s3.has_flag(S3Flag::Input6),
},
})
}
}
impl EthercatDeviceUsed for Wago750_672 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_672 {}
impl EthercatDynamicPDO for Wago750_672 {
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 EthercatDeviceProcessing for Wago750_672 {
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl EthercatDevice for Wago750_672 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let mut b = [0u8; 12];
for i in 0..12 {
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
}
self.txpdo = Wago750_672TxPdo {
status_byte0: b[0],
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
position_l: b[6],
position_m: b[7],
position_h: b[8],
status_byte3: b[9],
status_byte2: b[10],
status_byte1: b[11],
};
// Yes this statemachine is unfortunately needed in here to make sure
// bits are correctly set and reset in the correct cycle.
match self.state {
InitState::Off => {
// Do nothing
self.initialized = false;
}
InitState::Enable => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state if ENABLE and STOP2_N is acknowledged
if self.txpdo.status_byte1 == c1 {
self.state = InitState::SetMode;
}
}
InitState::SetMode => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state if SPEED MODE is acknowledged
if self.txpdo.status_byte1 == c1 {
self.initialized = true;
self.state = InitState::StartPulseStart;
}
}
InitState::StartPulseStart => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_flag(C1Flag::Start)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state after StartPulse is acknowledged
if self.txpdo.status_byte1 == c1 {
self.state = InitState::StartPulseEnd;
}
}
InitState::StartPulseEnd => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state after StartPulse is over
if self.txpdo.status_byte1 == c1 {
self.state = InitState::Running;
}
}
InitState::Running => {
let c2 = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
let c3 = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
// Check for Error
if self.txpdo.status_byte2 & c2 != 0 {
self.state = InitState::ErrorQuit;
} else {
self.rxpdo.control_byte2 |= c2;
}
// Check for Reset
if self.txpdo.status_byte3 & c3 != 0 {
self.state = InitState::ResetQuit;
} else {
self.rxpdo.control_byte3 |= c3;
}
}
InitState::ErrorQuit => {
self.rxpdo.control_byte2 |=
ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
tracing::error!("Stepper Controller Errored. Trying to reenable...");
self.state = InitState::Enable;
}
InitState::ResetQuit => {
self.rxpdo.control_byte3 |=
ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
tracing::error!("Stepper Controller Reset not Quit. Trying to reenable...");
self.state = InitState::Enable;
}
}
Ok(())
}
fn input_len(&self) -> usize {
12 * 8
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.rx_bit_offset;
let output_bytes = [
self.rxpdo.control_byte,
0,
self.rxpdo.velocity.to_le_bytes()[0],
self.rxpdo.velocity.to_le_bytes()[1],
self.rxpdo.acceleration.to_le_bytes()[0],
self.rxpdo.acceleration.to_le_bytes()[1],
0,
0,
0,
self.rxpdo.control_byte3,
self.rxpdo.control_byte2,
self.rxpdo.control_byte1,
];
for i in 0..12 {
output[base + i * 8..base + (i + 1) * 8].store_le(output_bytes[i]);
}
Ok(())
}
fn output_len(&self) -> usize {
12 * 8
}
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> {
let expected = self.input_len();
let actual = input.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::input_checked] Input length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
fn output_checked(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let expected = self.output_len();
let actual = output.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::output_checked] Output length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
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 NewEthercatDevice for Wago750_672 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rxpdo: Wago750_672RxPdo::default(),
txpdo: Wago750_672TxPdo::default(),
state: InitState::Off,
initialized: false,
}
}
}
impl std::fmt::Debug for Wago750_672 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_672")
}
}
pub const WAGO_750_672_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_672_PRODUCT_ID: u32 = 108139752;
pub const WAGO_750_672_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_672_VENDOR_ID, WAGO_750_672_PRODUCT_ID);
@@ -5,7 +5,7 @@ const U16_MAX: i128 = std::u16::MAX as i128;
/// We convert the overflows and underflows to an i128 value for easier calculations.
///
/// When overriding the counter we don't set the valeu direclty but schedula it wiht the [`push_override`] so it can be synced with [`pop_override`] to an EtherCAT device.
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct CounterWrapperU16U128 {
counter: i128,
last_counter: u16,
+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(())
}
+1
View File
@@ -7,4 +7,5 @@ pub mod encoder_input;
pub mod pulse_train_output;
pub mod serial_interface;
pub mod stepper_velocity_el70x1;
pub mod stepper_velocity_wago_750_672;
pub mod temperature_input;
@@ -0,0 +1,376 @@
use std::sync::Arc;
use smol::{block_on, lock::RwLock};
use crate::devices::wago_modules::wago_750_672::{InitState, Wago750_672};
/*
* Wago Stepper Velocity Wrapper around Stepper Controller
*
*/
#[derive(Debug)]
pub struct StepperVelocityWago750672 {
pub device: Arc<RwLock<Wago750_672>>,
pub state: InitState,
pub target_velocity: i16,
pub target_acceleration: u16,
pub enabled: bool,
pub freq_range_sel: u8,
pub acc_range_sel: u8,
}
impl StepperVelocityWago750672 {
pub fn new(device: Arc<RwLock<Wago750_672>>) -> Self {
Self {
device,
state: InitState::Off,
target_velocity: 0,
target_acceleration: 10000,
enabled: false,
freq_range_sel: 0,
acc_range_sel: 0,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
if self.enabled && enabled {
return;
}
// this needs to be set before the stepper controller
// is enabled because it will generate an error otherwise
self.set_acceleration(self.target_acceleration);
self.enabled = enabled;
if enabled {
self.change_init_state(InitState::Enable);
} else {
self.change_init_state(InitState::Off);
self.write_control_byte(ControlByte::C1, 0b00000000);
}
}
pub fn set_velocity(&mut self, velocity: i16) {
self.target_velocity = velocity;
let mut dev = block_on(self.device.write());
// clamp velocity to -25000 - 25000 :: because this is
// the min max for the controller
dev.rxpdo.velocity = velocity.clamp(-25000, 25000);
if dev.initialized {
dev.state = InitState::StartPulseStart;
}
}
pub fn set_acceleration(&mut self, acceleration: u16) {
self.target_acceleration = acceleration;
let mut dev = block_on(self.device.write());
dev.rxpdo.acceleration = acceleration;
}
#[allow(dead_code)]
fn get_actual_velocity(&self) -> i16 {
let dev = block_on(self.device.read());
dev.txpdo.actual_velocity
}
#[allow(dead_code)]
fn get_target_acceleration(&self) -> u16 {
let dev = block_on(self.device.read());
dev.rxpdo.acceleration
}
pub fn set_freq_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.freq_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.control_byte2)
.with_freq_range(factor)
.bits();
dev.rxpdo.control_byte2 = c2;
}
pub fn set_acc_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.acc_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.control_byte2)
.with_acc_range(factor)
.bits();
dev.rxpdo.control_byte2 = c2;
}
fn change_init_state(&mut self, state: InitState) {
self.state = state.clone();
let mut dev = block_on(self.device.write());
dev.state = state;
}
fn write_control_byte(&self, control_byte: ControlByte, value: u8) {
let mut dev = block_on(self.device.write());
match control_byte {
ControlByte::C0 => dev.rxpdo.control_byte = value,
ControlByte::C1 => dev.rxpdo.control_byte1 = value,
ControlByte::C2 => dev.rxpdo.control_byte2 = value,
ControlByte::C3 => dev.rxpdo.control_byte3 = value,
}
}
#[allow(dead_code)]
fn read_status_byte(&self, status_byte: StatusByte) -> u8 {
let dev = block_on(self.device.read());
match status_byte {
StatusByte::S0 => dev.txpdo.status_byte0,
StatusByte::S1 => dev.txpdo.status_byte1,
StatusByte::S2 => dev.txpdo.status_byte2,
StatusByte::S3 => dev.txpdo.status_byte3,
}
}
}
// The Different Control Bytes set control the stepper controller
// by setting and resetting specific bits.
//
// There are some construction functions provided to create the
// control bytes correctly.
/// Control Byte C1
#[derive(Clone, Copy, Default)]
pub struct ControlByteC1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Flag {
Enable = 0b0000_0001,
Stop2N = 0b0000_0010,
Start = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Command {
Idle = 0b0000_0000,
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000,
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
impl ControlByteC1 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn with_command(mut self, cmd: C1Command) -> Self {
self.0 = (self.0 & 0b0000_0111) | (cmd as u8);
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C2
#[derive(Clone, Copy, Default)]
pub struct ControlByteC2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C2Flag {
FreqRangeSelL = 0b0000_0001,
FreqRangeSelH = 0b0000_0010,
AccRangeSelL = 0b0000_0100,
AccRangeSelH = 0b0000_1000,
PreCalc = 0b0100_0000,
ErrorQuit = 0b1000_0000,
}
impl ControlByteC2 {
pub const fn new() -> Self {
Self(0)
}
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn with_freq_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
self
}
pub const fn with_acc_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
self
}
pub const fn with_flag(mut self, flag: C2Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C3
#[derive(Clone, Copy, Default)]
pub struct ControlByteC3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C3Flag {
SetActualPos = 0b0000_0001,
DirectionPos = 0b0000_0010,
DirectionNeg = 0b0000_0100,
ResetQuit = 0b1000_0000,
}
impl ControlByteC3 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C3Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
// The status bytes are similar to the control bytes but are only readable
// and most of the time provide corresponding acknoledgements to the diffrent
// Control bytes.
//
// There are also helper functions to construct the diffrent Status bytes for
// compoarison.
/// Status Byte S1
#[derive(Clone, Copy, Default)]
pub struct StatusByteS1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1Flag {
Ready = 0b0000_0001,
Stop2NAck = 0b0000_0010,
StartAck = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1CommandAck {
Idle = 0b0000_0000,
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000,
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
impl StatusByteS1 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S1Flag) -> bool {
(self.0 & flag as u8) != 0
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Status Byte S2
#[derive(Clone, Copy, Default)]
pub struct StatusByteS2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S2Flag {
OnTarget = 0b0000_0001,
Busy = 0b0000_0010,
StandStill = 0b0000_0100,
OnSpeed = 0b0000_1000,
Direction = 0b0001_0000,
ReferenceOk = 0b0010_0000,
PreCalcAck = 0b0100_0000,
Error = 0b1000_0000,
}
impl StatusByteS2 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S2Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
/// Status Byte S3
#[derive(Clone, Copy, Default)]
pub struct StatusByteS3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S3Flag {
Input1 = 0b0000_0001,
Input2 = 0b0000_0010,
Input3 = 0b0000_0100,
Input4 = 0b0000_1000,
Input5 = 0b0001_0000,
Input6 = 0b0010_0000,
Warning = 0b0100_0000,
Reset = 0b1000_0000,
}
impl StatusByteS3 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S3Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
#[derive(Debug, Clone, Copy)]
pub enum ControlByte {
C0,
C1,
C2,
C3,
}
#[derive(Debug, Clone, Copy)]
pub enum StatusByte {
S0,
S1,
S2,
S3,
}
Generated
+111 -12
View File
@@ -1,6 +1,21 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1771796463,
"narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=",
"owner": "ipetkov",
"repo": "crane",
"rev": "3d3de3313e263e04894f284ac18177bd26169bad",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -15,7 +30,7 @@
"type": "github"
}
},
"crane_2": {
"crane_3": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -102,12 +117,51 @@
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1771851181,
"narHash": "sha256-gFgE6mGUftwseV3DUENMb0k0EiHd739lZexPo5O/sdQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "9a4b494b1aa1b93d8edf167f46dc8e0c0011280c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750973805,
"narHash": "sha256-BZXgag7I0rnL/HMHAsBz3tQrfKAibpY2vovexl2lS+Y=",
@@ -122,9 +176,10 @@
"type": "github"
}
},
"home-manager_2": {
"home-manager_3": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
]
@@ -143,9 +198,10 @@
"type": "github"
}
},
"home-manager_3": {
"home-manager_4": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -167,11 +223,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1750969886,
"narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=",
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a676066377a2fe7457369dd37c31fd2263b662f4",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
@@ -207,6 +263,31 @@
],
"qitech-control": "qitech-control_2"
},
"locked": {
"lastModified": 1772023797,
"narHash": "sha256-RipiUlH5UKiofTRquREluUFOwEfNjLFEvyDol/VWDRg=",
"owner": "qitechgmbh",
"repo": "control",
"rev": "3e3e3f1c0bc2b26fe0fc1e7d49b65e3fd3d7f368",
"type": "github"
},
"original": {
"owner": "qitechgmbh",
"repo": "control",
"type": "github"
}
},
"qitech-control_2": {
"inputs": {
"crane": "crane_3",
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"nixpkgs": [
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_3"
},
"locked": {
"lastModified": 1751047510,
"narHash": "sha256-SZ+DxQvFJKJOro1AVUwWltLQO1L9TIc6AI4tnM9V4tk=",
@@ -221,15 +302,16 @@
"type": "github"
}
},
"qitech-control_2": {
"qitech-control_3": {
"inputs": {
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"flake-utils": "flake-utils_4",
"home-manager": "home-manager_4",
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_3",
"qitech-control": "qitech-control_4",
"rust-overlay": "rust-overlay_2"
},
"locked": {
@@ -246,10 +328,11 @@
"type": "github"
}
},
"qitech-control_3": {
"qitech-control_4": {
"inputs": {
"flake-utils": "flake-utils_4",
"flake-utils": "flake-utils_5",
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -300,6 +383,7 @@
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -378,6 +462,21 @@
"repo": "default",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+1 -1
View File
@@ -2,7 +2,7 @@
description = "QiTech Control";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
# Crane for Rust builds with dependency caching
crane = { url = "github:ipetkov/crane"; };
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more