Compare commits

...
Author SHA1 Message Date
rob 0022d86035 Revert "Merge pull request #914 from qitechgmbh/bin-directory"
This reverts commit aecabfde43, reversing
changes made to b8168c6b43.
2025-12-19 14:10:40 +01:00
Hans Sebastian Pöhlmann dc711a820f Analog input test machine (#989)
Adding AnalogInputTestMachine, a minimal demo setup with EK1100 and EL3021. Adding setup instructions to readme.
2025-12-18 16:45:28 +00:00
Robin Krämer 59cf2cea2a Merge pull request #849 from qitechgmbh/769-add-an-alert-or-warning-in-the-frontend-for-filament-out-of-spec
769 add an alert or warning in the frontend for filament out of spec
2025-12-18 12:00:52 +01:00
Snacj da69ad1c3e Removed unused import 2025-12-18 11:22:22 +01:00
Snacj 6ff434fb47 Fixed Formatting 2025-12-18 11:22:22 +01:00
Snacj a63c57a0b9 Global error on Filament out of tolerance. 2025-12-18 11:22:22 +01:00
Snacj 21c43adcb4 Rebase and error fixing 2025-12-18 11:22:22 +01:00
snacj afa23666e2 Resolved small writing and logic errors. 2025-12-18 11:22:22 +01:00
snacj 245ad64426 Fixed Formatting 2025-12-18 11:22:22 +01:00
snacj 9665971150 Fixed 0mm threshold for Laser Diameter Warning 2025-12-18 11:22:22 +01:00
snacj 3b056c8f26 Changed Styling of Checkbox. 2025-12-18 11:22:22 +01:00
snacj 62b35e0237 Added AutoStopWinderOnOutOfTolerance to Laser Presets 2025-12-18 11:22:22 +01:00
snacj 11bc62f40e formatting 2025-12-18 11:22:22 +01:00
snacj 116c76d218 Added AutoStopWinderOnOutOfTolerance 2025-12-18 11:22:22 +01:00
snacj 4dcedb537d Changed Styling of Warning. 2025-12-18 11:22:22 +01:00
snacj 09022c9c86 Global Laser Warning addded 2025-12-18 11:22:22 +01:00
snacj 80cf98b11d Laser Warning works across all laser pages 2025-12-18 11:22:22 +01:00
snacj 644e750353 Added global Laser Alert 2025-12-18 11:22:22 +01:00
Robin Krämer aecabfde43 Merge pull request #914 from qitechgmbh/bin-directory
Move scripts to bin directory with better names
2025-12-18 11:20:28 +01:00
Oshgnacknak 686678a1fe Rename install script in electron app 2025-12-18 11:16:39 +01:00
Oshgnacknak 7ac0b788c2 Move scripts to bin directory with better names 2025-12-18 11:16:39 +01:00
Robin Krämer b8168c6b43 Merge pull request #976 from qitechgmbh/aquapath-fix-port-selection
Aquapath fix port selection
2025-12-17 10:12:30 +01:00
rob 6026d2a658 ports are now correctly selected for the aquapath 2025-12-17 10:10:18 +01:00
Achraf Jalleli a301f3e228 Merge pull request #979 from qitechgmbh/932-add-metrics
Add compile and runtime metrics
2025-12-16 22:45:54 +01:00
JalleliAchraf 0b6bd68a24 frontend UI improvements + compile metrics script fixes 2025-12-16 17:43:33 +01:00
Christian e7fd390263 Merge pull request #969 from qitechgmbh/968-Add-mock-machines-to-pr-checks
968 add mock machines to pr checks and fix mocks and dev build
2025-12-16 16:09:17 +01:00
JalleliAchraf eab675795a Add compile and runtime metrics 2025-12-16 14:07:03 +01:00
Christian 8c2dc9a507 Merge branch 'master' into 968-Add-mock-machines-to-pr-checks 2025-12-16 10:49:14 +01:00
Robin Krämer f6c0744efa Merge pull request #937 from qitechgmbh/860-wago-coupler-750-354-cant-enter-op
860 wago coupler 750 354 cant enter op
2025-12-16 10:45:25 +01:00
rob 8c7e810359 wago modules detected and wrapped as an EtherCatDevice 2025-12-16 08:25:19 +01:00
Christian f0fd9e8ce6 fix: clean up import formatting in registry.rs 2025-12-15 11:00:49 +01:00
Christian c5579a4471 fix mock machines in dev build 2025-12-15 10:58:07 +01:00
Christian de7a65bf12 feat: add mock machine build step to CI workflow 2025-12-15 10:43:52 +01:00
rob e4d0b3c5be added logic for initializing wago modules on a 750_354 2025-12-12 15:24:59 +01:00
Robin Krämer dad029ab0d Merge pull request #924 from qitechgmbh/905-change-the-laser-graph-roundness-to-display-in-percentage-instead-of-mm
display laser roundness as percentage
2025-12-07 12:00:46 +01:00
EinsPommes 9c780799d1 fix: prettier formatting 2025-12-07 11:59:15 +01:00
EinsPommes 27a33f9def transform roundness to percentage and update related components 2025-12-07 11:59:15 +01:00
Robin Krämer f44cd029f9 Merge pull request #925 from qitechgmbh/891-switch-to-setup-page-when-selected-machine-disconnected
891 switch to setup page when selected machine disconnected
2025-12-07 11:58:58 +01:00
EinsPommes 9abb7629fb fix: prettier formatting 2025-12-07 11:56:41 +01:00
EinsPommes a161d85a63 fix: clarify machine existence check in SidebarLayout 2025-12-07 11:56:41 +01:00
EinsPommes 9df9e6df4a remove console.log statement 2025-12-07 11:56:41 +01:00
EinsPommes 29d465c13e fix: handle machine disconnects properly
The app now detects when a machine disconnects and redirects you
to the setup page. Before, it would just stay on the machine page
even though the machine was gone.

Fixed both backend (USB detection) and frontend (auto-navigation).
Added error handling for parseInt and pathMatch array access.
2025-12-07 11:56:41 +01:00
EinsPommes 622c94f2d9 update: redirect to setup when machine disconnects 2025-12-07 11:56:41 +01:00
EinsPommes f31a2789ca auto-redirect to setup when machine disconnects 2025-12-07 11:56:41 +01:00
Robin Krämer e7632e8d1b Merge pull request #941 from qitechgmbh/936
936-documentation for minimal example
2025-12-07 11:56:25 +01:00
Achraf Jalleli 2f4f3de32a Update schematic asset path
Removed placeholder text for schematic asset path.
2025-12-06 21:23:01 +01:00
Achraf Jalleli c9f49198c7 updated image reference 2025-12-06 19:01:06 +01:00
Achraf Jalleli e2c12a0168 Refinements
Updated instructions for connecting and configuring the EK1100 and EL2004.
2025-12-06 18:59:43 +01:00
Achraf Jalleli cb6e066512 Fix formatting of Acknowledgements section header 2025-12-06 18:50:35 +01:00
Achraf Jalleli 1dd0421f96 documentation for minimal example 2025-12-06 18:48:49 +01:00
Achraf Jalleli 64855f2e7a Minimal Example Documentation added 2025-12-06 18:42:17 +01:00
Achraf Jalleli 855ae7b3cf minimal example docu assets 2025-12-06 18:20:54 +01:00
Robin Krämer 5f9a8605c9 Merge pull request #930 from qitechgmbh/923-create-extruder-v3
923 create extruder v3
2025-12-04 13:55:20 +01:00
rob 567730a8d4 fixed typos in machines 2025-12-04 13:25:07 +01:00
rob 6b2164b88b added frontend for extruder3 and fixed rpm calculation 2025-12-04 11:30:53 +01:00
rob 930ea10409 added extruder2 2025-12-04 11:26:17 +01:00
Robin Krämer 24b703e262 Merge pull request #931 from qitechgmbh/895-small-sample-machine-for-testing
small test machine
2025-12-04 11:23:42 +01:00
JalleliAchraf 152dd3efa0 small test machine 2025-12-04 11:17:24 +01:00
Milan von dem Bussche ebc5bae55f added Minimal Example README.md
Minimal example to explain the Code
2025-12-03 16:19:13 +01:00
Robin Krämer 88214c46a8 Merge pull request #911 from qitechgmbh/909-implement-ecma-error-messages
909 implement ecmy error messages
2025-11-30 00:41:49 +01:00
rob c9485a92e9 fixed formatting 2025-11-30 00:38:34 +01:00
rob 9df42c4d13 fixed server setup 2025-11-30 00:38:34 +01:00
rob fc9a439ae8 removed unused variables 2025-11-30 00:38:34 +01:00
rob e451153406 removed unused variables 2025-11-30 00:38:34 +01:00
rob 500e2fe481 fixed parsing etc 2025-11-30 00:38:34 +01:00
rob a5884d93e7 test 2025-11-30 00:38:34 +01:00
rob d2e32cdd78 added comment 2025-11-30 00:38:34 +01:00
rob f66c6d4f65 Note entirely sure it works ... 2025-11-30 00:38:34 +01:00
Oshgnacknak fb85cc6db7 Merge pull request #896 from qitechgmbh/877-remove-dependencies-that-are-not-necessary-or-bloated
Remove futures crate
2025-11-24 23:13:55 +01:00
Robin Krämer 4e128ded36 Merge pull request #885 from qitechgmbh/699-add-min-and-max-diameter-on-laser-page2
699 add min and max diameter on laser page
2025-11-24 13:02:35 +01:00
Oshgnacknak f50388b141 Just restart failed tasks 2025-11-24 12:54:40 +01:00
ChristianandCopilot 4025a68fe1 Update electron/src/control/MinMaxValue.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-23 17:58:07 +01:00
ChristianandCopilot 7a05fa206f Update electron/src/control/MinMaxValue.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-23 17:58:07 +01:00
Christian 8594b06c3b Reorganize shared timeframe selector for clarity and improve layout for diameter range display 2025-11-23 17:58:07 +01:00
Christian 17523574d8 Add shared timeframe selector to Laser1ControlPage and expose TIMEFRAME_OPTIONS 2025-11-23 17:58:07 +01:00
Christian 7971797178 Refactor diameter range display for 2-axis lasers and improve layout 2025-11-23 17:58:07 +01:00
Christian a8b0837e58 Add MinMaxValue component for displaying diameter ranges 2025-11-23 17:58:07 +01:00
Oshgnacknak e419974844 Refactor init logic to work without futures crate 2025-11-22 17:30:37 +01:00
Oshgnacknak 773d0f6c15 Remove tonic 2025-11-22 16:15:48 +01:00
Oshgnacknak afc4e0d8b4 Completely remove opentelemetry and update dependencies 2025-11-22 16:15:47 +01:00
Robin Krämer 5e86988ac4 Merge pull request #907 from qitechgmbh/882-add-nozzle-docs-for-extruder
882 add nozzle docs for extruder
2025-11-19 11:38:07 +01:00
snacj 2d28c18b1a Added Heading for new Mechanical Diagram 2025-11-18 14:04:32 +01:00
snacj b19764281a Added Nozzle docs 2025-11-18 13:58:51 +01:00
snacj c25d63f781 Added Nozzle docs 2025-11-18 13:58:51 +01:00
Robin Krämer d79efc412f Merge pull request #904 from qitechgmbh/898-mock-machine-doesnt-work
898 mock machine doesnt work
2025-11-18 12:53:24 +01:00
rob 7ad74cf089 fixed formatting 2025-11-17 18:00:43 +01:00
rob 6073d37892 fixed remaining compile errors 2025-11-17 18:00:43 +01:00
rob c04937553f machines show up but dont get subscribed to 2025-11-17 18:00:43 +01:00
rob e08bdfa3c7 removed unnecessary and outdated docs 2025-11-17 18:00:43 +01:00
rob d282678b19 fixed compile errors for mock-machines 2025-11-17 18:00:43 +01:00
Robin Krämer 9ff4243350 Merge pull request #902 from qitechgmbh/2.12-releasenotes
updated Changelog
2025-11-17 16:38:48 +01:00
rob 6c79b33752 updated Changelog 2025-11-17 16:36:41 +01:00
rob 861b5da7a3 updated Changelog 2025-11-17 16:35:28 +01:00
Robin Krämer 7dd3d50653 Merge pull request #899 from qitechgmbh/add-debugging-utils-folder
added debugging utils and fixed memory leak
2025-11-14 23:30:55 +01:00
kraemr ba8e04c641 removed unused imports 2025-11-14 23:28:05 +01:00
kraemr cece245a55 removed one_hour caching on backend for apis 2025-11-14 21:26:38 +01:00
kraemr fab4d2810e fixed memory leak and added debugging utilities 2025-11-14 21:18:51 +01:00
Robin Krämer 5bff3ca929 Merge pull request #897 from qitechgmbh/fix-async-yield-timeout
Fix async yield timeout
2025-11-14 16:45:59 +01:00
kraemr afe1d3b80e fixed display of time taken on failing loop 2025-11-14 14:06:32 +01:00
kraemr 6c8005dba9 log cycle time on failure 2025-11-14 13:36:50 +01:00
kraemr 9f399ca6f3 fixed compile error 2025-11-14 13:25:44 +01:00
kraemr a7574dd348 better error handling for copy ethercat outputs and inputs 2025-11-14 13:22:27 +01:00
kraemr b39f3597c8 fixed loop 2025-11-14 12:20:53 +01:00
Robin Krämer bb9a86d52e Merge pull request #893 from qitechgmbh/fix-serial-hotplugging
fixed laser hotplug issue
2025-11-13 13:57:27 +01:00
kraemr e5329e40c6 fixed ci error 2025-11-13 13:55:44 +01:00
kraemr 319611f69d fixed laser hotplug issue 2025-11-13 13:55:44 +01:00
Robin Krämer 4b16253c8c Merge pull request #887 from qitechgmbh/841-remove-all-locking-and-as-much-async-code-as-possible
841 remove all locking and as much async code as possible
2025-11-13 10:01:47 +01:00
kraemr b60423fde6 removed unnecessary variable from shared_state and fixed serial hotplug 2025-11-13 09:33:40 +01:00
kraemr 5d3399c72e fixed formatting electron 2025-11-13 01:03:10 +01:00
kraemr d247bafcbb fixed formatting 2025-11-13 01:00:28 +01:00
kraemr 5227aad4c6 removed all warnings 2025-11-13 00:56:59 +01:00
kraemr da4c54a99e fixed compile errors 2025-11-13 00:04:15 +01:00
kraemr 8d42bd93f7 started refactoring so machines use channels instead of locks
backend works flawlessly now

added machine_connection and a main_thread actor

added back write_machine_device_ident

error handling for machines restored

correctly handles machine connections/disconnections now

removed unused deps
2025-11-12 17:54:11 +01:00
kraemr 37e467272b removed potential "deadlock" 2025-11-12 17:41:03 +01:00
Oshgnacknak ed0b9cd5ae Merge pull request #859 from qitechgmbh/789-create-minimal-set-of-uom-units
Use minimal set of units instead of full uom::si module
2025-11-11 15:47:45 +01:00
Oshgnacknak 8045f799f0 Use minimal set of units instead of full uom::si module 2025-11-10 17:15:51 +01:00
Robin Krämer ca6ea16f89 Merge pull request #881 from qitechgmbh/880-add-nozzle-pid-for-temperature-at-extruder
880 add nozzle pid for temperature at extruder
2025-11-06 12:16:58 +01:00
snacj 452614abf1 Fixed Frontend Formatting 2025-11-06 11:59:52 +01:00
snacj f63c797cd1 Fixed Formatting 2025-11-06 11:57:56 +01:00
snacj de791f6ea8 Added PID for extruder nozzle. 2025-11-06 11:57:34 +01:00
Robin Krämer 024f751ef1 Merge pull request #873 from qitechgmbh/872-add-cycle-target-time
872 add cycle target time
2025-11-04 16:19:15 +01:00
kraemr d4c82bfd7d refactored ethercat input ouput into funcs, and added spin_sleep
with default target time of 300us
2025-11-04 15:23:19 +01:00
Robin Krämer a49f19133d Merge pull request #874 from qitechgmbh/855-decouple-serial-devices-from-ethercat-logic-in-looprs-mainrs
855 decouple serial devices from ethercat logic in looprs mainrs
2025-11-04 15:08:49 +01:00
snacj a94009514c Changed Error message of moved code 2025-11-04 12:46:03 +01:00
Snacj d1d3724f52 Fixed Formatting 2025-11-03 18:49:51 +01:00
Snacj 9f4177aff9 Fixed implementation errors 2025-11-03 18:48:01 +01:00
Snacj e323fd76f8 Moved loop thread out of main loop 2025-11-03 18:45:08 +01:00
Snacj b0d24d8502 fixed mock-machine init after app_state 2025-11-03 18:29:27 +01:00
Snacj 8ae20c2aae Added init_mock back to main.rs and fixed serial devices relying on ethercat interface 2025-11-03 18:27:28 +01:00
Snacj 89843514b8 debugging main.rs 2025-11-03 17:41:26 +01:00
Robin Krämer 1a9e39a49e Merge pull request #817 from qitechgmbh/811-add-a-simple-updating-tab
811 add a simple updating progress bar
2025-11-03 14:23:36 +01:00
Christian f4d5e74c59 Add update step tracking and progress bar component 2025-11-02 14:47:05 +01:00
Oshgnacknak eb1301df99 Merge pull request #650 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-forge/plugin-fuses-7.8.3
Bump @electron-forge/plugin-fuses from 7.8.2 to 7.10.2 in /electron
2025-10-31 19:02:46 +01:00
Oshgnacknak 2885fa91cc Bump electron fuses to 17.10.2 2025-10-31 18:41:38 +01:00
dependabot[bot] d89e4681db Bump @electron-forge/plugin-fuses from 7.8.2 to 7.8.3 in /electron
Bumps [@electron-forge/plugin-fuses](https://github.com/electron/forge) from 7.8.2 to 7.8.3.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.8.2...v7.8.3)

---
updated-dependencies:
- dependency-name: "@electron-forge/plugin-fuses"
  dependency-version: 7.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 17:26:00 +00:00
Oshgnacknak dedcd76241 Merge pull request #761 from qitechgmbh/dependabot/npm_and_yarn/electron/hookform/resolvers-5.2.2
Bump @hookform/resolvers from 4.1.3 to 5.2.2 in /electron
2025-10-31 18:23:33 +01:00
Oshgnacknak e987dd07f9 Fix resolver type argument 2025-10-31 18:22:03 +01:00
dependabot[bot] 6747433100 Bump @hookform/resolvers from 4.1.3 to 5.2.2 in /electron
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 4.1.3 to 5.2.2.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v4.1.3...v5.2.2)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.2.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 18:22:02 +01:00
Robin Krämer 3ffd67918c Merge pull request #854 from qitechgmbh/853-add-laser-dre-manual-md-to-docs
853 add laser dre manual md to docs
2025-10-31 10:39:33 +01:00
Snacj 3fe6556037 Added Settings options to DRE md 2025-10-31 10:34:42 +01:00
snacj 5ccc65241d Added Markdown for laser DRE 2025-10-31 10:34:42 +01:00
Robin Krämer be92fd0665 Merge pull request #857 from qitechgmbh/856-dre-laser-registers-are-read-wrong-in-modbus-mode
Fixed Register Vec to work with 2 Axis Laser in Modbus mode
2025-10-29 21:32:01 +01:00
Snacj 8a6d4d1385 Fixed Register Vec to work with 2 Axis Laser in Modbus mode 2025-10-29 18:10:52 +01:00
Robin Krämer 13117a5d94 Merge pull request #845 from qitechgmbh/844-add-winder-mock-machine
added Winder mock
2025-10-28 17:58:25 +01:00
kraemr 6b74c7ec7a removed default mock-machine 2025-10-28 17:53:04 +01:00
kraemr e8b561f722 added Winder mock 2025-10-28 17:53:04 +01:00
Robin Krämer 75ee5af180 Merge pull request #852 from qitechgmbh/851-improve-caching
851 improve caching
2025-10-28 17:46:21 +01:00
kraemr 29f924f6cc removed socketioManager 2025-10-27 16:42:03 +01:00
kraemr 6e058c8e95 Keep namespaces alive 2025-10-27 15:11:30 +01:00
Robin Krämer 6822da80cf Merge pull request #838 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-a50d7f32cf
Bump playwright and @playwright/test in /electron
2025-10-27 11:24:43 +01:00
dependabot[bot] 37e3302f61 Bump playwright and @playwright/test in /electron
Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together.


Updates `playwright` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

Updates `@playwright/test` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.56.1
  dependency-type: indirect
- dependency-name: "@playwright/test"
  dependency-version: 1.56.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:21:25 +01:00
Robin Krämer 9b369d71ee Merge pull request #839 from qitechgmbh/dependabot/npm_and_yarn/electron/vite-6.4.1
Bump vite from 6.3.6 to 6.4.1 in /electron
2025-10-27 11:21:10 +01:00
dependabot[bot] cf58f0db6f Bump vite from 6.3.6 to 6.4.1 in /electron
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:17:46 +01:00
Robin Krämer 299f23c796 Merge pull request #847 from qitechgmbh/846-refactor-initialization-to-be-based-around-futures
started refactoring initialization to use futures instead
2025-10-27 11:14:18 +01:00
kraemr b58e4333a0 removed traces 2025-10-27 11:08:21 +01:00
kraemr f76cb9c381 Fixed Loop async code 2025-10-27 11:01:04 +01:00
kraemr e93e10d14d fixed all warnings and restored original ethercat discovery behaviour 2025-10-23 20:13:48 +02:00
kraemr 025464ebff all init functions look sane now
fixed compilation errors

fixed compile errors

refactored serial device detection successfully

added serial_detection functions

fixed  Killed error, and improved serial_detection

fixed all remaining issues

fix

fix
2025-10-23 20:13:33 +02:00
kraemr ae8f77f070 started refactoring initialization to use futures instead 2025-10-22 17:56:52 +02:00
Robin Krämer 97811a12d1 Merge pull request #842 from qitechgmbh/changelog-2.11.0
updated Changelog
2025-10-21 13:11:26 +02:00
kraemr 1f7e01820a updated Changelog 2025-10-21 13:10:37 +02:00
Robin Krämer 2f73771bbd Merge pull request #827 from qitechgmbh/825-add-button-to-change-the-spool-motor-direction-of-the-winder
825 add button to change the spool motor direction of the winder
2025-10-21 13:01:25 +02:00
Christian 503ac18e7b Ensure spool RPM is always displayed as a positive value 2025-10-21 12:37:34 +02:00
Christian 6a99b21c15 Fix spool speed rendering to display absolute values 2025-10-21 12:37:34 +02:00
Christian 170667f2b4 Add spool direction control to Winder2
- Introduced a new setting for spool direction (Forward/Reverse) in Winder2PresetsPage and Winder2Settings.
- Updated the state management to include forward direction in useWinder.
- Enhanced the API to handle SetSpoolForward mutation.
- Adjusted the spool speed controller to apply direction based on the forward setting.
2025-10-21 12:37:34 +02:00
Robin Krämer 314b7f2484 Merge pull request #840 from qitechgmbh/828-add-xl-winder-button-to-the-winder-config
828 add xl winder button to the winder config
2025-10-21 12:28:22 +02:00
Christian 9353b12707 Fix traverse limit reset logic in handleXlModeChange function 2025-10-21 11:51:02 +02:00
Christian 063810f906 Add gotoTraverseHome call when switching from XL to normal mode 2025-10-21 11:51:02 +02:00
Christian ac7ce126e6 Refactor handleXlModeChange to reset traverse limits when switching from XL mode 2025-10-21 11:51:02 +02:00
Christian a920830dcf Update WINDER2_TRAVERSE_MAX_XL to 385 mm for improved configuration 2025-10-21 11:51:02 +02:00
Christian 15d74fcee0 Add configuration for Winder2 settings and implement XL mode functionality 2025-10-21 11:51:02 +02:00
Robin Krämer c598e3fc9f Merge pull request #826 from qitechgmbh/818-smooth-fluctuating-temperature-extruder
818 smooth fluctuating temperature extruder
2025-10-20 18:16:08 +02:00
snacj 0dba0f2774 Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj e2cdb6a94c Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj 5dd2011e78 removed file 2025-10-20 18:12:06 +02:00
snacj 7bc142dc83 Fixed error in Extruder mock and removed zone from UpdateStateOptimistically 2025-10-20 18:12:06 +02:00
snacj 1931f00806 Changed rounding and step size to 0.001 for temperature PID 2025-10-20 18:12:06 +02:00
snacj 253336b02c Refactored frontend code of new PID settings 2025-10-20 18:12:06 +02:00
snacj b0c3a97f7f Fixed 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
snacj 22887d4f7a Added 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
Robin Krämer 00dbc90c85 Merge pull request #834 from qitechgmbh/lag-spike-fix
removed async sleep from control loop
2025-10-20 18:07:21 +02:00
kraemr 9ade1d8993 removed async sleep from control loop 2025-10-20 18:04:52 +02:00
Robin Krämer b2fb803033 Merge pull request #829 from qitechgmbh/808-lock-kernel-in-nix-to-last-working-rt-kernel
Newest kernel with realtime preemption
2025-10-20 13:42:38 +02:00
nixos-install.sh 988ed5b959 Newest kernel with realtime preemption 2025-10-17 17:36:14 +02:00
Oshgnacknak 2e62f99724 Merge pull request #806 from qitechgmbh/fix-caps
Fix caps in cargo run Linux
2025-10-17 15:01:53 +02:00
Oshgnacknak 530327b1e8 Fix caps in cargo run Linux 2025-10-17 14:55:44 +02:00
Robin Krämer 2f75b31fa9 Merge pull request #816 from qitechgmbh/812-improve-interface-discovery-to-only-use-ethernet-devices
fixed the atrocious interface_discovery
2025-10-17 12:42:31 +02:00
kraemr ce8f0a8c4b fixed the atrocious interface_discovery 2025-10-17 12:40:18 +02:00
Robin Krämer 321aae5b86 Merge pull request #820 from qitechgmbh/changelog-210
updated Changelog
2025-10-16 15:16:05 +02:00
kraemr 7712ea2d1f updated Changelog 2025-10-16 15:15:36 +02:00
Robin Krämer 175f64f784 Merge pull request #804 from qitechgmbh/803-add-winder-gearbox-conversion
803 add winder gearbox conversion
2025-10-16 12:39:14 +02:00
ChristianandCopilot 9357960463 Update electron/src/machines/winder/winder2/useWinder.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 12:22:54 +02:00
Christian 4d043906bc Remove unnecessary whitespace in puller speed calculation 2025-10-16 12:22:54 +02:00
Christian b99b7f796b Reset target speed to 0 when setting gear ratio and adjust puller speed calculation based on gear ratio 2025-10-16 12:22:54 +02:00
Christian a7b0414613 Add gear ratio calculation and display in Winder2 control and presets 2025-10-16 12:22:54 +02:00
Christian 13c608b11a Add gear ratio functionality to puller speed controller
- Introduced gear ratio enum with options for 1:5 and 1:10.
- Updated puller speed controller to include gear ratio state.
- Implemented gear ratio selection in Winder2Settings component.
- Added corresponding mutation and state management in useWinder2 hook.
2025-10-16 12:22:54 +02:00
Oshgnacknak d14fb91736 Simpliy panic handling and fix backtrace (#809)
* Hide memory locking behind feature flag

* Simplify panic handling
2025-10-15 21:09:05 +02:00
Oshgnacknak 6beaa444af Simplify panic handling 2025-10-15 17:08:37 +02:00
Oshgnacknak 3b552558d4 Hide memory locking behind feature flag 2025-10-15 17:08:37 +02:00
Robin Krämer 9cc63bf8fb Merge pull request #805 from qitechgmbh/743-electron-doesnt-use-gpu-for-hw-acceleration
Remove bad hardware acceleration flag
2025-10-15 10:31:04 +02:00
Oshgnacknak 70ce7625b2 Remove bad hardware acceleration flag 2025-10-13 15:00:21 +02:00
Robin Krämer 9936764125 Merge pull request #717 from qitechgmbh/700-add-a-show-from-option-for-graphs
700 add a show from option for graphs
2025-10-13 12:17:26 +02:00
Christian a0f09e1671 Improve formatting and adjust future time validation messages in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 1100b880f9 Add validation to prevent future time selection in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 6224f33ba5 fix eslint 2025-10-13 12:14:09 +02:00
Christian b0a9b5a6d2 Fix styling 2025-10-13 12:14:09 +02:00
ChristianandCopilot ac7247c87a Update electron/src/components/time/TimeInput.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 12:14:09 +02:00
Christian 2ea459f3be Refactor button styles and improve formatting in GraphControls and TimeInput components 2025-10-13 12:14:09 +02:00
Christian 25250f640f Add showFromTimestamp functionality to graph components and implement TimeInput for time selection 2025-10-13 12:14:09 +02:00
Robin Krämer 57a0cc8af4 Merge pull request #776 from qitechgmbh/775-add-explanation-of-padding-to-the-winder-manual
Add padding parameter explanation to manual for filament winding
2025-10-13 12:13:53 +02:00
Christian 5ebb978eae Add padding example image to the winder docs 2025-10-13 12:09:20 +02:00
Christian d647ee6ebe fix format 2025-10-13 12:09:20 +02:00
Christian fea203ae7a Add padding parameter explanation to manual for filament winding 2025-10-13 12:09:20 +02:00
Robin Krämer b5eb413d37 Merge pull request #791 from qitechgmbh/788-fix-remote-setting-reset-and-update-label-in-update-tab
Fix remote setting reset and update label to "Repository Owner" in Update tab
2025-10-13 12:08:43 +02:00
Christian aa1e5c4f39 Fix typos in GitHub repository owner label and description 2025-10-13 12:05:31 +02:00
Christian ee91934bf7 Refactor Github source management: integrate condition store and update UI labels 2025-10-13 12:05:31 +02:00
Milan von dem Bussche 1fa04eae9a Update README.md
Added YouTube Explainer Video about QiTech Control
2025-10-10 18:41:46 +02:00
Robin Krämer 98e1269f6f Merge pull request #800 from qitechgmbh/changelog-2.9.1
updated Changelog
2025-10-10 15:07:51 +02:00
kraemr 8f640fcb21 updated Changelog 2025-10-10 15:06:30 +02:00
Robin Krämer 6c120b7fe9 Merge pull request #796 from qitechgmbh/795-winding-when-puller-reverse-doesnt-work-in-adaptive
adaptive spool speed now takes negative speeds into account,
2025-10-08 18:12:23 +02:00
kraemr 033b93b9da puller speed now is always positive in frontend 2025-10-08 17:49:55 +02:00
kraemr 6092358e87 adaptive spool speed now takes negative speeds into account,
aswell as Auto Stop Spool
2025-10-08 17:40:50 +02:00
Robin Krämer 71de39e871 Merge pull request #793 from qitechgmbh/790-refactor-aquapath-newrs-to-match-others
790 refactor aquapath newrs to match others
2025-10-08 16:29:49 +02:00
joe 54ffa193c3 fixed fmt 2025-10-08 16:23:40 +02:00
joe 03164f1ceb refactor(aquapath1): cleanup and fix device initialization and configuration 2025-10-08 15:45:58 +02:00
Robin Krämer f67a46ab4c Merge pull request #787 from qitechgmbh/786-add-memory-locking
added CAP_IPC_LOCK and added memory locking
2025-10-07 13:54:02 +02:00
kraemr 610790a778 added CAP_IPC_LOCK and memory locking 2025-10-07 13:04:49 +02:00
Robin Krämer 6eaf451ad4 Merge pull request #692 from JanHBade/master
Update README.md
2025-10-07 12:57:27 +02:00
JanHBade 022e0adae3 Update README.md
Minor typo in EtherCAT
2025-10-07 12:57:16 +02:00
Robin Krämer 6e6d4b31e7 Merge pull request #746 from qitechgmbh/640-improve-scroll-bar-styling-for-manuals-to-resemble-macos-style
Update scrollbar styling to be similar to macos
2025-10-07 12:45:12 +02:00
Christian 0dafad3fb5 fix code stye 2025-10-07 12:40:01 +02:00
Christian 534de55239 Refactor scrollbar styles to apply only to html and body elements 2025-10-07 12:40:01 +02:00
Christian d9f92bcdf2 Refactor scrollbar styles for Markdown and Terminal components to be similar to MacOS styling 2025-10-07 12:40:01 +02:00
Robin Krämer 1570299e38 Merge pull request #782 from qitechgmbh/781-refactor-winderv2-emit-functions-to-emitrs
refactored winder2's emit methods to emit.rs
2025-10-06 17:01:38 +02:00
kraemr c95f6f3bd9 refactored winder2's emit methods to emit.rs 2025-10-06 16:36:59 +02:00
Robin Krämer 9fcde2a234 Merge pull request #780 from qitechgmbh/772-remove-dependencies-that-are-not-necessary
decreased compile time by removing jemalloc, and using only f64 uom
2025-10-06 16:05:08 +02:00
kraemr a2ffc41e05 decreased compile time by removing jemalloc, and using only f64 uom 2025-10-06 16:02:29 +02:00
Robin Krämer ff13baf257 Merge pull request #779 from qitechgmbh/changelog-2.9.0
updated Changelog
2025-10-06 14:25:39 +02:00
kraemr 8f5df07c19 updated Changelog 2025-10-06 14:24:43 +02:00
Robin Krämer 299b38515a Merge pull request #778 from qitechgmbh/777-increase-max-step-size
Increased Winder Step Size
2025-10-06 14:12:24 +02:00
kraemr 2b073aeb76 Increased Winder Step Size 2025-10-06 13:56:20 +02:00
Robin Krämer 7a6f92f8a4 Merge pull request #771 from qitechgmbh/770-update-machine-implementation-status-in-the-readme
Update machine documentation to reflect current implementation status
2025-10-02 17:07:13 +02:00
Christian 2e283ea95f Update machine documentation to reflect current implementation status 2025-10-02 17:03:55 +02:00
Robin Krämer 829bb5ea1c Merge pull request #625 from qitechgmbh/improved-disconnect-handling
Introduce machine connection
2025-10-02 14:26:07 +02:00
Oshgnacknak 0be18aaed0 Derive machines from macro again
This time we also add get_machine_identification_unique.

Also fix import warnings
2025-10-02 13:58:54 +02:00
Oshgnacknak ffe0f09061 Clean up machine event creation 2025-10-01 16:50:04 +02:00
nixos-install.sh 730d61a18d Don't clear ethercat slots, we want to keep the namespaces 2025-10-01 15:57:03 +02:00
Oshgnacknak fc07525976 Don't remove, only disconnect serial machines
This allows use to keep the namespaces
2025-10-01 15:57:03 +02:00
Oshgnacknak c48953aa6b Introduce machine connection
This is to hold on to the namespace of
a machine even if the devices of that
machine has disconnected and to allow
reattaching to the namespace once the
devices reconnects.

Additionally to the new struct,
a wast refactor took place cleaning up
connections to machines, cross connections
between machines the way we downcast
machines downcasts.
2025-10-01 15:57:03 +02:00
Melvin Spieß 0c948b436d Merge pull request #764 from qitechgmbh/dependabot/cargo/axum-0.8.6
Bump axum from 0.8.4 to 0.8.6
2025-10-01 12:05:48 +02:00
Robin Krämer 1b3f92dd1c Merge pull request #725 from qitechgmbh/425-fix-terminal-component-issues-v2
425 fix terminal component issues v2
2025-10-01 11:58:55 +02:00
dependabot[bot] c29c549a1c Bump axum from 0.8.4 to 0.8.6
Bumps [axum](https://github.com/tokio-rs/axum) from 0.8.4 to 0.8.6.
- [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.4...axum-v0.8.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:57:52 +00:00
kraemr 17622cacde set log level to "info" for rust 2025-10-01 11:56:29 +02:00
kraemr 0b423454ec fixed line duplication in Terminal Component on Update Page 2025-10-01 11:56:29 +02:00
kraemr 81ec98f670 dont clear log, causes react errors 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com 1aed425544 eslint bypass for control character 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com da080ace68 try using clear functions before interactions 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com ded8dd2d2d added react-virtualized 2025-10-01 11:56:29 +02:00
93094049+kraemr@users.noreply.github.com edd83ba559 added virtualized list 2025-10-01 11:56:29 +02:00
Melvin Spieß 7d1d8559a5 Merge pull request #672 from qitechgmbh/671-refactor-machine-creation
reduced code duplication in new.rs of machines
2025-10-01 11:56:17 +02:00
kraemr f0bb9bfed1 fixed lifetimes of get_ethercat_device and refactored buffer 2025-10-01 11:52:13 +02:00
93094049+kraemr@users.noreply.github.com c6ea057736 renamed helper function 2025-10-01 11:52:13 +02:00
93094049+kraemr@users.noreply.github.com 90a00198a7 reduced code duplication in new.rs of machines 2025-10-01 11:52:12 +02:00
Melvin Spieß f8241547fe Merge pull request #762 from qitechgmbh/dependabot/cargo/erased-serde-0.4.8
Bump erased-serde from 0.4.6 to 0.4.8
2025-10-01 11:50:54 +02:00
Melvin Spieß 1cb5d51c13 Merge pull request #760 from qitechgmbh/dependabot/npm_and_yarn/electron/tanstack/react-router-1.130.17
Bump @tanstack/react-router from 1.130.12 to 1.130.17 in /electron
2025-10-01 11:38:53 +02:00
Melvin Spieß c42cdf8db1 Merge pull request #759 from qitechgmbh/dependabot/npm_and_yarn/electron/radix-ui/react-navigation-menu-1.2.14
Bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 in /electron
2025-10-01 11:38:22 +02:00
Melvin Spieß 69014ef8c6 Merge pull request #758 from qitechgmbh/dependabot/npm_and_yarn/electron/types/react-19.1.16
Bump @types/react from 19.1.9 to 19.1.16 in /electron
2025-10-01 11:35:53 +02:00
Melvin Spieß 5faab29a2b Merge pull request #763 from qitechgmbh/dependabot/cargo/libc-0.2.176
Bump libc from 0.2.175 to 0.2.176
2025-10-01 11:35:31 +02:00
dependabot[bot] 9a8b16ca69 Bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 in /electron
Bumps [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:32:49 +00:00
Melvin Spieß 53f3c66214 Merge pull request #765 from qitechgmbh/dependabot/cargo/regex-1.11.3
Bump regex from 1.11.2 to 1.11.3
2025-10-01 11:30:59 +02:00
dependabot[bot] 432bf0d6ba Bump libc from 0.2.175 to 0.2.176
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.175 to 0.2.176.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.176
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:30:49 +00:00
dependabot[bot] f3990f35f1 Bump erased-serde from 0.4.6 to 0.4.8
Bumps [erased-serde](https://github.com/dtolnay/erased-serde) from 0.4.6 to 0.4.8.
- [Release notes](https://github.com/dtolnay/erased-serde/releases)
- [Commits](https://github.com/dtolnay/erased-serde/compare/0.4.6...0.4.8)

---
updated-dependencies:
- dependency-name: erased-serde
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:30:40 +00:00
dependabot[bot] 6f17412363 Bump regex from 1.11.2 to 1.11.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.2 to 1.11.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.2...1.11.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:28:49 +00:00
dependabot[bot] 68907b9750 Bump @types/react from 19.1.9 to 19.1.16 in /electron
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.9 to 19.1.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:26:34 +00:00
dependabot[bot] e7007e1980 Bump @tanstack/react-router from 1.130.12 to 1.130.17 in /electron
Bumps [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) from 1.130.12 to 1.130.17.
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.130.17/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.130.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:26:27 +00:00
Melvin Spieß 3bcd96d73c Merge pull request #766 from qitechgmbh/dependabot/cargo/anyhow-1.0.100
Bump anyhow from 1.0.99 to 1.0.100
2025-10-01 11:09:51 +02:00
dependabot[bot] 8a77c62a5e Bump anyhow from 1.0.99 to 1.0.100
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.99 to 1.0.100.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.100
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:24:32 +00:00
kraemr 93add82ab5 updated Changelog 2025-09-30 15:29:27 +02:00
Robin Krämer 690b1f7ab3 Merge pull request #757 from qitechgmbh/revert-ethercat-setup
fixed ethercat setup timeouts and retry
2025-09-29 17:19:33 +02:00
kraemr 3c07b94e3a fixed ethercat setup timeouts and retry 2025-09-29 17:18:49 +02:00
Robin Krämer 3b63b55859 Merge pull request #756 from qitechgmbh/fix-winder-regression
fixed winder regression due to refactor
2025-09-29 17:10:33 +02:00
kraemr f87982c465 fixed winder regression due to refactor 2025-09-29 17:01:04 +02:00
Robin Krämer b03cedb55c Merge pull request #447 from qitechgmbh/fix/438
Implementation AquaPath
2025-09-29 15:42:32 +02:00
kraemr f5934efa6c restored mock-machine functionality 2025-09-29 15:39:25 +02:00
kraemr 186619cd48 fixed compilation error 2025-09-29 15:33:25 +02:00
Darmenov Alisher 583d4c1a4d resolved comment 2025-09-29 15:33:25 +02:00
Anatomik5 63bee54948 AquapathV1 2025-09-29 15:33:25 +02:00
Robin Krämer 1523d64ed7 Merge pull request #749 from qitechgmbh/738-add-mock-machines
738 add mock machines
2025-09-29 11:32:39 +02:00
kraemr 48fdde61dc further improved readability by moving feature flags to mod.rs 2025-09-29 11:30:54 +02:00
kraemr 7bc700ac5f when selecting mock-machine feature extruder now appears as well 2025-09-29 11:30:54 +02:00
93094049+kraemr@users.noreply.github.com bdd6147f92 rewrote extruder and winder, also added mock-machine for extruder 2025-09-29 11:30:54 +02:00
93094049+kraemr@users.noreply.github.com f7f65bdaaa refactored all emit functions out of mod.rs for clarity for extruder 2025-09-29 11:30:54 +02:00
Robin Krämer 232b0782ca Merge pull request #754 from qitechgmbh/748-add-feature-flag-or-env-var-for-setting-loop-timeout
748 add feature flag or env var for setting loop timeout
2025-09-29 11:30:19 +02:00
kraemr c3c36a26b6 also added development-build flag to irq affinity code 2025-09-25 17:11:18 +02:00
kraemr 7f22bdfbf2 added feature development-build to set timeout 2025-09-25 17:06:42 +02:00
Robin Krämer ff4c579f7b Merge pull request #673 from qitechgmbh/632-add-documentation-for-creating-a-new-machine
added documentation for adding a machine
2025-09-25 15:33:41 +02:00
348 changed files with 24968 additions and 9150 deletions

No files matched your search

+3
View File
@@ -46,3 +46,6 @@ jobs:
- name: Run tests
run: cargo test --verbose
- name: Mock Machine
run: cargo build --features development-build --features mock-machine
+5
View File
@@ -3,3 +3,8 @@ dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
+83 -2
View File
@@ -1,3 +1,84 @@
# `2.12.0`
_17.11.2025_
## General
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
## Laser
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
## Extruder V2
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
# `2.11.0`
_21.10.2025_
## General
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
## Winder V2
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
## Extruder V2
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
# `2.10.0`
_16.10.2025_
## Winder V2
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
## General
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
# `2.9.1`
_10.10.2025_
## Winder V2
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
## General
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
# `2.9.0`
_06.10.2025_
## Winder V2
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
## General
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
# `2.8.0`
_30.09.2025_
## Laser V1
- [#741](https://github.com/qitechgmbh/control/pull/741) Added roundness measurement to two-axis Laser
## Winder V2
- [#745](https://github.com/qitechgmbh/control/pull/745), [#756](https://github.com/qitechgmbh/control/pull/756) improved Winder Frontend
## General
- [#447](https://github.com/qitechgmbh/control/pull/447) added AquaPath Machine
# `2.7.0`
_17.09.2025_
@@ -10,9 +91,9 @@ _17.09.2025_
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
## Laser V1/V2
## Laser V1
- [#731](https://github.com/qitechgmbh/control/pull/731) Added LaserV2 with two-axis measurements
- [#731](https://github.com/qitechgmbh/control/pull/731) Added Laser with two-axis measurements
## General
Generated
+524 -1100
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -4,8 +4,11 @@ members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"ethercat-eeprom-dump",
"machines",
#"ethercat-eeprom-dump",
"control-core-derive",
"units",
"utils",
]
exclude = []
resolver = "2"
+413 -7
View File
@@ -7,6 +7,9 @@ QiTech Control is an industrial control panel software for the next generation o
[![](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.*
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
# Repo Structure
Frontend
@@ -32,7 +35,7 @@ Other
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
@@ -64,14 +67,16 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
# More Docs
# More Docs docs
/assets
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -149,3 +154,404 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
## Minimal Example — LED Control on the EL2004
A complete hardware + software walkthrough
---
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
4. [Software Setup](#4-software-setup)
5. [Demo](#5-demo)
6. [References](#6-references)
7. [Acknowledgements](#7-acknowledgements)
---
# 1. Introduction
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a **Beckhoff EL2004 EtherCAT terminal** using the QiTech machine framework.
It represents the simplest possible hardware interaction in the system:
**toggling LED outputs using the QiTech Control Dashboard.**
---
# 2. Requirements
## Hardware
- Beckhoff **EL2004 EtherCAT Terminal** (4-channel digital output)
- Beckhoff **EK1100 EtherCAT Coupler**
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
- Jumper / bridge wires (0.5–1.5 mm² recommended)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
## Software
*(Installation steps in Section 4)*
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
---
# 3. Hardware Setup
## 3.1 Schematic
![](./docs/assets/schematic.png)
---
## 3.2 EK1100 Wiring
This wiring configuration powers the EL2004 and prepares it for LED control.
It is not the only possible wiring but is the **simplest functional setup**.
### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
---
## 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](./docs/assets/wiring.png)
---
## 3.2.2 Wiring Steps (Used in This Example)
We supply power using a **DC hollow-plug adapter**, like this one:
https://www.amazon.de/dp/B093FTFZ8Q
Perform the following wiring on the EK1100:
1. Red wire **(+24 V)** → Terminal **2**
2. Black wire **(0 V)** → Terminal **3**
3. Jumper wire from **Terminal 1 → Terminal 6**
4. Jumper wire from **Terminal 5 → Terminal 7**
After wiring, your module should look like **Figure 1**.
---
### **Figure 1 — EK1100 Minimal Wiring**
<img src="./docs/assets/ek1100.jpeg" width="400">
---
## 3.3 EL2004 Integration
Slide the EL2004 onto the **right side of the EK1100** until it locks.
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
### **Figure 2 — EL2004 Terminal**
<img src="./docs/assets/el2004.jpeg" width="400">
---
## 3.4 Final Assembled Setup
### **Figure 3 — EK1100 + EL2004 Connected**
<img src="./docs/assets/complete.jpeg" width="400">
---
## 3.5 Power & Ethernet
### Power
Connect the 24 V adapter to the hollow plug used earlier.
**Example AC/DC Adapter (Figure 4):**
<img src="./docs/assets/adapter.jpeg" width="400">
### Ethernet
Use a standard LAN cable to connect your PC → EK1100.
The final powered up and connected setup should look like this:
<img src="./docs/assets/power.jpeg" width="400">
---
# 4. Software Setup
## 4.1 Installing on Ubuntu/Debian
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
## 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
Ensure the EK1100 is connected.
## 4.3 Running the Frontend
```bash
cd electron
npm run start
```
This launches the QiTech Control dashboard.
# 5. Demo
## 5.1 Assigning Devices in the Dashboard
Once the backend + frontend are running, you should see:
EK1100 Coupler
EL2004 Digital Output Terminal
![](./docs/assets/discovery.png)
Steps:
- Click Assign on the EK1100
- Select TestMachine V1
![](./docs/assets/setmachine.png)
- Enter a serial number (use the same for EK1100 + EL2004)
![](./docs/assets/serserial.png)
- Click Write
- Repeat for the EL2004
## 5.2 Testing LED Control
Navigate to:
Machines → TestMachine
![](./docs/assets/machinedetected.png)
You will see this interface:
![](./docs/assets/machinecontrol.png)
**You can now toggle the four digital outputs of the EL2004.**
# 6. References
This guide incorporates information from official Beckhoff documentation.
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
Referenced Manuals
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
[Beckhoff EL2004 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el20xx_el2124_de.pdf)
# 7. Acknowledgements
This tutorial is inspired by the clarity and educational quality of Beckhoff manuals.
All wiring illustrations and hardware descriptions in this guide are provided for demonstration purposes only and do not replace official Beckhoff installation guidelines.
Special thanks to the QiTech engineering team for providing the backend architecture, EtherCAT HAL abstraction, and the TestMachine framework that makes this example possible.
---
Minimal Example - Analog Input EL3021
===
### Table of Contents
1. Introduction
2. Requirements
3. Hardware Setup
4. Software Setup
5. Demo
6. References
### 1. Introduction
The EL3021 Analog Input example demonstrates how to measure analog current on a **Beckhoff EL3021 EtherCAT teminal** using the QiTech machine framework.
### 2. Requirements
## Hardware
- Beckhoff **EL3021 EtherCAT Terminal** (1-channel analog input)
- Beckhoff **EK1100 EtherCAT Coupler**
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V )
- Jumper / bridge wires (0.5–1.5 mm² recommended)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
### 3. Hardware Setup
#### 3.1 Schematic
![](./docs/assets/EL3021_schematic.png)
The 24V supply powers the EL2004, while the adjustable power supply generates the analog input signal for the EL3021.
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
#### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](./docs/assets/wiring.png)
---
#### 3.2.2 Wiring Steps (Used in This Example)
We supply power using a **DC hollow-plug adapter**, like this one:
https://www.amazon.de/dp/B093FTFZ8Q
Perform the following wiring on the EK1100:
1. Red wire **(+24 V)** → Terminal **2**
2. Black wire **(0 V)** → Terminal **3**
3. Jumper wire from **Terminal 1 → Terminal 6**
4. Jumper wire from **Terminal 5 → Terminal 7**
After wiring, your module should look like **Figure 1**.
---
#### **Figure 1 — EK1100 Minimal Wiring**
<img src="./docs/assets/ek1100.jpeg" width="400">
---
#### 3.3 EL3021 Integration
Slide the EL3021 onto the **right side of the EK1100** until it locks.
The EtherCAT E-Bus and power contacts connect automatically.
Then you need to wire the adjustable power supply to the EL3021.
#### **Figure 2 — EL3021 Terminal**
<img src="./docs/assets/EL3021.jpg" width="400">
1. Wire ⊕ to **Terminal 1**
2. Wire ⊖ to **Terminal 2**
---
#### 3.4 Final Assembled Setup
#### **Figure 3 — EK1100 + EL3021 Connected**
<img src="./docs/assets/EK1100+EL3021.jpg" width="400">
---
### 3.5 Power & Ethernet
#### 3.5.1 Power
Connect the 24 V adapter to the hollow plug used earlier.
#### Example AC/DC Adapter (Figure 4):
<img src="./docs/assets/adapter.jpeg" width="400">
#### 3.5.2 Ethernet
Use a standard LAN cable to connect your PC → EK1100.
The final powered up and connected setup should look like this:
<img src="./docs/assets/EL3021_power+eth.jpg" width="400">
---
### 4. Software Setup
#### 4.1 Installing on Ubuntu/Debian
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
#### 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
Ensure the EK1100 is connected.
#### 4.3 Running the Frontend
```bash
cd electron
npm run start
```
This launches the QiTech Control dashboard.
### 5. Demo
#### 5.1 Assigning Devices in the Dashboard
Once both backend and frontend are running, the Setup -> EtherCat page should look something like this:
![](./docs/assets/EL3021_overview.png)
Listing the EK1100 and the EL3021.
To assign the two devices to a machine, click "Assign" on both devices. This should open the following pop-up modal:
**EK1100**
![](./docs/assets/EL3021_assign.png)
1. Select "AnalogTest V1" under "Maschine"
2. Choose a serial number to uniquely identify the machine (1 in this example)
3. Under "Device Role" select "0 - Bus Coupler"
**EL3021**
![](./docs/assets/EL3021_assign2.png)
1. Select "AnalogTest V1" under "Maschine"
2. Use the same serial number you chose for the EK1100
3. Under "Device Role" select "1 - EL3021"
---
The machine is now configured.
A new Tab "AnalogTest" should appear in the right sidebar. Clicking that tab should show the machine control interface:
![](./docs/assets/EL3021_control.png)
Results should show the current measured by the analog input - the current set on the adjustable power supply.
### 6. References
This guide incorporates information from official Beckhoff documentation.
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
Referenced Manuals
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
[Beckhoff EL3021 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el30xx_de.pdf)
+6 -4
View File
@@ -15,16 +15,18 @@ if [[ "$1" == "release" ]]; then
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE
cargo build $FEATURE --features development-build
fi
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
echo "Setting permissions for serial ports"
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
if compgen -G "/dev/ttyUSB*" > /dev/null; then
echo "Setting permissions for serial ports"
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
fi
# run
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
BUILD_DIR="${BUILD_DIR:-target}"
CSV_FILE="${CSV_FILE:-compile_metrics.csv}"
DEFAULT_PKG="server"
if [ "$#" -eq 0 ]; then
set -- cargo build
fi
if [ "${1:-}" = "cargo" ] && [ "${2:-}" = "build" ]; then
cargo clean
BUILD_CMD=(cargo rustc -p "$DEFAULT_PKG")
shift 2
BUILD_CMD+=("$@")
BUILD_CMD+=(-- --emit=llvm-ir -C debuginfo=0)
else
BUILD_CMD=("$@")
fi
compile_start_ns=$(date +%s%N)
"${BUILD_CMD[@]}"
compile_end_ns=$(date +%s%N)
compile_ms=$(( (compile_end_ns - compile_start_ns) / 1000000 ))
if [ -d "$BUILD_DIR" ]; then
llvm_ir_lines=$(
find "$BUILD_DIR" -type f -name '*.ll' -print0 \
| xargs -0 cat \
| wc -l
)
else
llvm_ir_lines=0
fi
bin_path=""
if [ -n "${BINARY_PATH:-}" ] && [ -f "$BINARY_PATH" ]; then
bin_path="$BINARY_PATH"
elif [ -f "./target/release/server" ]; then
bin_path="./target/release/server"
elif [ -f "./target/debug/server" ]; then
bin_path="./target/debug/server"
elif [ -f "./server/target/release/server" ]; then
bin_path="./server/target/release/server"
elif [ -f "./server/target/debug/server" ]; then
bin_path="./server/target/debug/server"
fi
if [ -n "$bin_path" ]; then
binary_size_bytes=$(stat -c '%s' "$bin_path")
else
binary_size_bytes=0
fi
finished_timestamp_ms=$(($(date +%s%N) / 1000000))
finished_datetime=$(date -Iseconds)
if [ ! -f "$CSV_FILE" ]; then
echo "datetime,finished_timestamp_ms,compile_ms,llvm_ir_lines,binary_size_bytes" > "$CSV_FILE"
fi
echo "${finished_datetime},${finished_timestamp_ms},${compile_ms},${llvm_ir_lines},${binary_size_bytes}" \
>> "$CSV_FILE"
echo "Metrics recorded in $CSV_FILE"
+6 -1
View File
@@ -40,10 +40,15 @@ fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {}
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
}
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
fn as_any(&self) -> &dyn std::any::Any {
+5 -7
View File
@@ -7,31 +7,29 @@ edition = "2024"
workspace = true
[dependencies]
anyhow = "1.0.99"
anyhow = "1.0.100"
axum = "0.8.4"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.2"
uom = "0.36.0"
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.6"
erased-serde = "0.4.8"
core_affinity = "0.8.3"
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
tokio = { version = "1.45.1", features = ["sync"], optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.175"
libc = "0.2.176"
[dev-dependencies]
approx = "0.5.1"
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularVelocity},
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Velocity},
velocity::meter_per_second,
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
@@ -199,8 +199,8 @@ impl AngularAccelerationPositionController {
/// # Example
/// ```ignore
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
/// use uom::si::f64::Angle;
/// use uom::si::angle::degree;
/// use units::f64::Angle;
/// use units::angle::degree;
///
/// let mut controller = AngularAccelerationPositionController::new_simple(
/// Some(Angle::new::<degree>(180.0)),
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angular_acceleration::radian_per_second_squared,
angular_jerk::radian_per_second_cubed,
angular_velocity::radian_per_second,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -65,7 +65,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -127,7 +127,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -185,7 +185,7 @@ impl AngularJerkSpeedController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
///
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Length, Velocity},
length::meter,
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(10.0);
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
@@ -101,8 +101,8 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
@@ -150,7 +150,7 @@ impl LinearAccelerationPositionController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{length::meter, f64::Length};
/// use units::{length::meter, f64::Length};
///
/// let mut controller = LinearAccelerationPositionController::new_simple(
/// Some(Length::new::<meter>(10.0)),
@@ -375,7 +375,7 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
///
/// let new_position = Length::new::<meter>(0.0);
///
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Jerk, Velocity},
jerk::meter_per_second_cubed,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
+14 -32
View File
@@ -1,4 +1,4 @@
use uom::si::angle::radian;
use units::{angle::radian, f64::Angle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
@@ -63,7 +63,7 @@ impl AngleConverter {
}
/// Convert angle from mathematical system to this system (f32)
pub fn degrees_encode(&self, math_angle_degrees: f32) -> f32 {
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(math_angle_degrees);
let mut result = normalized_input;
@@ -85,7 +85,7 @@ impl AngleConverter {
}
/// Convert angle from this system to mathematical system (f32)
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(system_angle_degrees);
let mut result = normalized_input;
@@ -107,14 +107,14 @@ impl AngleConverter {
}
/// Convert angle in radians from mathematical system to this system (f32)
pub fn radians_encode(&self, math_angle_radians: f32) -> f32 {
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode(degrees);
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f32)
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode(degrees);
converted_degrees.to_radians()
@@ -179,7 +179,7 @@ impl AngleConverter {
}
/// Normalize angle to [0, 360) range (f32)
fn normalize_angle(&self, angle: f32) -> f32 {
fn normalize_angle(&self, angle: f64) -> f64 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
@@ -207,30 +207,12 @@ impl AngleConverterUom {
Self { angle_converter }
}
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_encode(angle.get::<radian>()),
)
pub fn encode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
}
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_decode(angle.get::<radian>()),
)
}
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_encode_f64(angle.get::<radian>()),
)
}
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_decode_f64(angle.get::<radian>()),
)
pub fn decode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
}
}
@@ -297,8 +279,8 @@ mod tests {
let converter = AngleConverter::screen();
// Test radians_encode
let result = converter.radians_encode(PI / 2.0); // 90° in radians
let expected = 270.0 * PI / 180.0; // 270° in radians
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
assert!((result - expected).abs() < 1e-6);
}
@@ -306,10 +288,10 @@ mod tests {
fn test_roundtrip_conversion() {
let converter = AngleConverter::new(true, false, true); // X-flip + CW
let original = 75.0_f32;
let original = 75.0_f64;
let converted = converter.degrees_encode(original);
let roundtrip = converter.degrees_decode(converted);
assert!((original - roundtrip).abs() < 1e-6);
assert!((original - roundtrip as f64).abs() < 1e-6);
}
}
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::revolution,
angular_acceleration::radian_per_second_squared,
angular_velocity::revolution_per_second,
@@ -90,7 +90,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
use units::{
angle::degree, angular_acceleration::degree_per_second_squared,
angular_velocity::degree_per_second,
};
@@ -14,14 +14,14 @@
//!
//! ```rust
//! use control_core::converters::circular_converter::CircularConverter;
//! use uom::si::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//! use units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//!
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
//! let velocity = Velocity::new::<meter_per_second>(1.0);
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
//! ```
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
angle::radian,
angular_acceleration::radian_per_second_squared,
@@ -199,7 +199,7 @@ impl CircularConverter {
#[cfg(test)]
mod tests {
use super::*;
use uom::si::length::centimeter;
use units::length::centimeter;
#[test]
fn test_constructors() {
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::revolution,
angular_velocity::revolution_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
@@ -251,7 +251,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
use units::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
+36
View File
@@ -0,0 +1,36 @@
use anyhow::anyhow;
use smol::lock::Mutex;
use std::any::Any;
use std::sync::Arc;
pub trait Downcast<T> {
fn downcast(&self) -> Result<T, anyhow::Error>;
}
pub trait DowncastRef<T> {
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
}
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let lock = self.lock_blocking();
// Check if the inner type can be downcasted to T
let any: &dyn Any = &*lock;
if any.is::<T>() {
// Clone the Arc and return it as the desired type
let cloned: Arc<Mutex<dyn Any>> = self.clone();
// Transmute the Arc to the desired type
unsafe {
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
Ok(arc)
}
} else {
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
}
}
}
@@ -1,10 +1,9 @@
use std::{sync::Arc, time::Duration};
use ethercrab::{
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
std::{ethercat_now, tx_rx_task},
};
use interfaces::Interface;
use std::{process::Command, sync::Arc, time::Duration};
// Constants for EtherCAT configuration
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
@@ -12,14 +11,46 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
/// Finds an ethernet interface that is suitable for EtherCAT communication.
///
/// ```ignore
/// match discover_ethercat_interface().await {
/// Ok(interface) => println!("Found working interface: {}", interface),
/// Err(_) => println!("No working interface found"),
/// }
/// ```
/// Sets a network interface to unmanaged by NetworkManager.
/// Returns true if the command succeeded.
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
let managed_str = match managed {
true => "yes",
false => "no",
};
tracing::info!(
"set_interface_managed for {} managed was set to: {}",
interface,
managed_str
);
let status = Command::new("nmcli")
.args(["dev", "set", interface, "managed", managed_str])
.status();
matches!(status, Ok(s) if s.success())
}
/// Returns true if the given network interface is Ethernet.
/// Prevents testing wlan,loopback and other non ethernet devices
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
#[cfg(target_os = "linux")]
{
let base_path = format!("/sys/class/net/{}", interface);
let type_path = std::path::Path::new(&base_path).join("type");
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
// 1 means Ethernet
let reports_as_ethernet = iface_type == "1";
// Double-check that it's not a wireless interface
let uevent_path = std::path::Path::new(&base_path).join("uevent");
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
// If "DEVTYPE=wlan" appears, it's lying
let actually_wifi = uevent.contains("DEVTYPE=wlan");
return Ok(reports_as_ethernet && !actually_wifi);
}
#[cfg(not(target_os = "linux"))]
return Ok(true);
}
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
tracing::info!("Discovering EtherCAT interface...");
@@ -37,7 +68,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
} else {
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
}
// Don't call the default hook, which would print the backtrace
}));
// Get eligible interfaces
@@ -48,6 +78,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
iface.is_up()
&& iface.is_running()
&& !iface.is_loopback()
&& is_ethernet(&iface.name).unwrap_or(false)
&& !iface.name.starts_with("bridge")
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
@@ -56,57 +87,39 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
})
.collect::<Vec<_>>();
// Sort interfaces by name
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
let mut interface: Option<&str> = None;
// Try all interfaces concurrently
let tasks = interfaces
.iter()
.map(|iface| {
let name = iface.name.clone();
std::thread::Builder::new()
.name(format!("ethercat-test-{}", name))
.spawn(move || {
std::panic::catch_unwind(|| {
smol::block_on(async {
match test_interface(&name) {
Ok(_) => {
tracing::info!("Found working EtherCAT interface: {}", name);
Some(name.clone())
}
Err(_) => {
tracing::debug!("Interface {} failed", name);
None
}
}
})
})
.unwrap_or(None)
})
.expect("Failed to spawn thread")
})
.collect::<Vec<_>>();
for i in 0..interfaces.len() {
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
interface = Some(&interfaces[i].name);
break;
}
Err(_) => (),
}
}
// Wait for all tasks to complete and collect successful results
let successful_interfaces: Vec<String> = tasks
.into_iter()
.filter_map(|task| task.join().expect("Should join thread"))
.collect();
// Return the first successful interface (they're sorted by name)
let result = successful_interfaces.into_iter().next();
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
set_interface_managed(&interfaces[i].name, false);
} else {
set_interface_managed(&interfaces[i].name, true);
}
}
// Restore the default panic hook
std::panic::set_hook(default_hook);
match result {
Some(name) => Ok(name),
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
match interface {
Some(interface) => return Ok(interface.to_string()),
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
}
}
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
tracing::trace!("Testing interface: {}", interface);
tracing::info!("Testing interface: {}", interface);
let pdu_storage = Box::leak(Box::new(PduStorage::<
IFACE_DISCOVERY_MAX_FRAMES,
@@ -144,7 +157,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
// Default 10000
dc_static_sync_iterations: 10000,
// Default None
retry_behaviour: ethercrab::RetryBehaviour::None,
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
},
));
-1
View File
@@ -1,2 +1 @@
pub mod eeprom_identification;
pub mod interface_discovery;
+1 -6
View File
@@ -1,19 +1,14 @@
pub mod controllers;
pub mod converters;
pub mod downcast;
pub mod ethercat;
pub mod helpers;
pub mod irq_handling;
pub mod machines;
pub mod modbus;
pub mod realtime;
pub mod rest;
pub mod serial;
pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
-25
View File
@@ -1,25 +0,0 @@
use serde_json::Value;
use crate::socketio::namespace::Namespace;
pub trait MachineApi {
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> &mut Namespace;
/// Returns a list of available video stream identifiers for this machine
#[cfg(feature = "video-streaming")]
fn api_video_streams(&self) -> Vec<String> {
Vec::new()
}
/// Get a video stream receiver for the specified stream ID
/// Returns None if the stream doesn't exist
#[cfg(feature = "video-streaming")]
fn api_get_video_stream(
&mut self,
stream_id: &str,
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
let _ = stream_id;
None
}
}
-113
View File
@@ -1,113 +0,0 @@
use std::fmt::Display;
use serde::Deserialize;
use serde::Serialize;
/// Identifies a spacifi machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentificationUnique {
pub machine_identification: MachineIdentification,
pub serial: u16,
}
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
pub vendor: u16,
pub machine: u16,
}
impl MachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
}
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
.machine_identification
.machine
!= 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
type Error = anyhow::Error;
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
let device_machine_identification =
value.device_machine_identification.ok_or(anyhow::anyhow!(
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
module_path!()
))?;
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
-275
View File
@@ -1,275 +0,0 @@
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use crate::{
machines::{
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
},
serial::SerialDevice,
socketio::event::GenericEvent,
};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
pub serial_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl Default for MachineManager {
fn default() -> Self {
Self::new()
}
}
impl MachineManager {
pub fn new() -> Self {
Self {
ethercat_machines: HashMap::new(),
serial_machines: HashMap::new(),
}
}
pub fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: usize>(
&mut self,
device_identifications: &Vec<DeviceIdentification>,
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
// empty ethercat machines
self.ethercat_machines.clear();
// group devices by machine device identification
let device_grouping_result = group_devices_by_identification(device_identifications);
tracing::info!(
"Device Groups {}",
device_grouping_result.device_groups.len()
);
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
// iterate over all identified device groups but ignore unaffected machines
for device_group in device_grouping_result.device_groups.iter() {
// get the machine identification
let machine_identification = match device_group.first() {
Some(device_identification) => &device_identification.device_machine_identification,
None => continue, // Skip this group if empty
};
// create the machine
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group,
hardware: &machine_new_hardware,
socket_queue_tx: socket_queue_tx.clone(),
machine_manager: machine_manager.clone(),
});
// insert the machine into the ethercat machines map
self.ethercat_machines.insert(
machine_identification.machine_identification_unique.clone(),
new_machine,
);
}
}
pub fn get(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
self.ethercat_machines
.get(machine_identification)
.or_else(|| self.serial_machines.get(machine_identification))
}
pub fn add_serial_device(
&mut self,
device_identification: &DeviceIdentification,
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
let hardware = MachineNewHardwareSerial { device };
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group: &vec![device_identification_identified.clone()],
hardware: &MachineNewHardware::Serial(&hardware),
socket_queue_tx,
machine_manager,
});
tracing::info!("Adding serial machine {:?}", new_machine);
self.serial_machines.insert(
device_identification_identified
.device_machine_identification
.machine_identification_unique,
new_machine,
);
}
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
tracing::info!(
"Removing serial machine {:?}",
device_identification_identified
);
self.serial_machines.remove(
&device_identification_identified
.device_machine_identification
.machine_identification_unique,
);
}
pub fn get_machine_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.ethercat_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
Some(machine_weak)
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.serial_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
Some(machine_weak)
}
}
/// Groups devices by machine identification
///
/// Returns a DeviceGroupingResult containing:
/// - device_groups: a vector of devices grouped by machine identification
/// - unidentified_devices: a vector of devices that could not be identified
pub fn group_devices_by_identification(
device_identifications: &Vec<DeviceIdentification>,
) -> DeviceGroupingResult {
let mut device_groups: Vec<Vec<DeviceIdentificationIdentified>> = Vec::new();
let mut unidentified_devices: Vec<DeviceIdentification> = Vec::new();
for device_identification in device_identifications {
// if vendor or serial or machine is 0, it is not a valid machine device
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
if !device_machine_identification.is_valid() {
unidentified_devices.push(device_identification.clone());
continue;
}
} else {
unidentified_devices.push(device_identification.clone());
continue;
}
// scan over all deice groups
// get the first DeviceMachineIdentification
// compare and append to the group
let mut found = false;
for check_group in device_groups.iter_mut() {
// get first device in group
let first_device = check_group.first().expect("group to not be empty");
let first_device_machine_identification = &first_device
.device_machine_identification
.machine_identification_unique;
// chek if it has machine identification
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
// compare with the current device
if first_device_machine_identification
== &device_machine_identification.machine_identification_unique
{
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
check_group.push(device_identification_identified);
found = true;
break;
}
}
}
if !found {
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
device_groups.push(vec![device_identification_identified]);
}
}
DeviceGroupingResult {
device_groups,
unidentified_devices,
}
}
/// Structure to hold the result of grouping devices by identification
#[derive(Debug)]
pub struct DeviceGroupingResult {
/// Devices grouped by machine identification
pub device_groups: Vec<Vec<DeviceIdentificationIdentified>>,
/// Devices that could not be identified
pub unidentified_devices: Vec<DeviceIdentification>,
}
-82
View File
@@ -1,82 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use smol::lock::Mutex;
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
pub enum MachineManagerIter<'a> {
EthercatMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
Done,
}
pub struct MachineManagerIterator<'a> {
iter: MachineManagerIter<'a>,
serial_machines:
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl<'a> Iterator for MachineManagerIterator<'a> {
type Item = (
&'a MachineIdentificationUnique,
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
);
fn next(&mut self) -> Option<Self::Item> {
loop {
match &mut self.iter {
MachineManagerIter::EthercatMachines { iter } => {
// Try to get the next item from ethercat_machines
if let Some(item) = iter.next() {
return Some(item);
} else {
// If ethercat_machines is exhausted, move to serial_machines
self.iter = MachineManagerIter::SerialMachines {
iter: self.serial_machines.iter(),
};
// Continue to the next iteration of the loop
continue;
}
}
MachineManagerIter::SerialMachines { iter } => {
// Return the next item from serial_machines or None if exhausted
if let Some(item) = iter.next() {
return Some(item);
} else {
// If serial_machines is also exhausted, we're done
self.iter = MachineManagerIter::Done;
return None;
}
}
MachineManagerIter::Done => {
// We've exhausted both collections
return None;
}
}
}
}
}
impl MachineManager {
// Returns an iterator over all machines (both ethercat and serial)
pub fn iter(&'_ self) -> MachineManagerIterator<'_> {
MachineManagerIterator {
iter: MachineManagerIter::EthercatMachines {
iter: self.ethercat_machines.iter(),
},
serial_machines: &self.serial_machines,
}
}
}
-83
View File
@@ -1,83 +0,0 @@
use anyhow::anyhow;
use api::MachineApi;
use new::MachineNewTrait;
use serde::Serialize;
use smol::lock::Mutex;
use std::any::Any;
use std::fmt::Debug;
use std::sync::{Arc, Weak};
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::new::MachineAct;
pub mod api;
pub mod identification;
pub mod manager;
pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + AnyGetters + Debug + Send {}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let read_lock = machine.lock().await;
// Check if the inner type can be downcasted to T
if read_lock.as_any().is::<T>() {
// Clone the Arc and return it as the desired type
let cloned_machine = Arc::clone(&machine);
// Transmute the Arc to the desired type
unsafe {
Ok(Arc::from_raw(
Arc::into_raw(cloned_machine) as *const Mutex<T>
))
}
} else {
Err(anyhow!(
"[{}::downcast_machine] Downcast failed",
module_path!()
))
}
}
/// Trait to get strong count to see if Weak has reference
pub trait GetStrongCount {
fn get_strong_count(&self) -> usize;
}
impl<T> GetStrongCount for Weak<Mutex<T>> {
fn get_strong_count(&self) -> usize {
self.strong_count()
}
}
#[derive(Debug)]
pub struct ConnectedMachine<T: GetStrongCount> {
pub machine_identification_unique: MachineIdentificationUnique,
pub machine: T,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ConnectedMachineData {
pub machine_identification_unique: MachineIdentificationUnique,
pub is_available: bool,
}
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
where
T: GetStrongCount,
{
fn from(value: &ConnectedMachine<T>) -> Self {
Self {
machine_identification_unique: value.machine_identification_unique.clone(),
is_available: value.machine.get_strong_count() != 0,
}
}
}
-259
View File
@@ -1,259 +0,0 @@
use crate::{machines::manager::MachineManager, serial::SerialDevice};
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
use ethercat_hal::{
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use socketioxide::extract::SocketRef;
use std::{
sync::{Arc, Weak},
time::Instant,
};
use crate::socketio::event::GenericEvent;
pub trait MachineNewTrait {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant);
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
> where
'maindevice: 'machine_new_hardware,
'subdevices: 'machine_new_hardware,
'ethercat_devices: 'machine_new_hardware,
'machine_new_hardware_etehrcat: 'machine_new_hardware,
{
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
pub hardware: &'machine_new_hardware MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub machine_manager: Weak<RwLock<MachineManager>>,
}
pub enum MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
> where
'maindevice: 'machine_new_hardware_etehrcat,
'subdevices: 'machine_new_hardware_etehrcat,
'ethercat_devices: 'machine_new_hardware_etehrcat,
{
Ethercat(
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
'maindevice,
'subdevices,
'ethercat_devices,
>,
),
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
}
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
pub subdevices:
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
}
pub struct MachineNewHardwareSerial {
pub device: Arc<RwLock<dyn SerialDevice>>,
}
// validates that all devices in the group have the same machine identification
pub fn validate_same_machine_identification_unique(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let machine_identification_unique = &identified_device_group
.first()
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::validate_same_machine_identification] No devices in group",
module_path!()
)
})?
.device_machine_identification
.machine_identification_unique;
for device in identified_device_group.iter() {
if device
.device_machine_identification
.machine_identification_unique
!= *machine_identification_unique
{
return Err(anyhow::anyhow!(
"[{}::validate_same_machine_identification] Different machine identifications",
module_path!()
));
}
}
Ok(())
}
/// validates that every role is unique
pub fn validate_no_role_dublicates(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let mut roles = vec![];
for device in identified_device_group.iter() {
if roles.contains(&device.device_machine_identification.role) {
return Err(anyhow::anyhow!(
"[{}::validate_no_role_dublicates] Role dublicate",
module_path!(),
));
}
roles.push(device.device_machine_identification.role);
}
Ok(())
}
// Inside control_core::machines::new module:
pub fn get_device_identification_by_role(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
role: u16,
) -> Result<&DeviceIdentificationIdentified, Error> {
for device in identified_device_group.iter() {
if device.device_machine_identification.role == role {
return Ok(device);
}
}
Err(anyhow::anyhow!(
"[{}::get_device_identification_by_role] Role {} not found",
module_path!(),
role
))
}
pub fn get_device_by_index<'maindevice>(
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_device_by_index] Index {} out of bounds for devices",
module_path!(),
subdevice_index
))?
.clone())
}
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
subdevice_index: usize,
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
"Index {} out of bounds for subdevices",
subdevice_index
))?)
}
pub fn get_ethercat_device_by_index<'maindevice>(
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(ethercat_devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
module_path!(),
subdevice_index
))?
.clone())
}
#[cfg(test)]
mod tests {
use crate::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
};
pub use super::*;
#[test]
fn test_get_device_identification_by_role() {
let device_identifications = vec![
// role 0
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 1,
machine: 1,
},
serial: 1,
},
role: 0,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 0 },
),
},
// role 1
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 2,
machine: 2,
},
serial: 2,
},
role: 2,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 1 },
),
},
// role 2
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 3,
machine: 3,
},
serial: 3,
},
role: 1,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 2 },
),
},
];
// search for role 0
let result = get_device_identification_by_role(&device_identifications, 0);
assert_eq!(result.unwrap().device_machine_identification.role, 0,);
// search for role 1
let result = get_device_identification_by_role(&device_identifications, 1);
assert_eq!(result.unwrap().device_machine_identification.role, 1,);
// search for role 2
let result = get_device_identification_by_role(&device_identifications, 2);
assert_eq!(result.unwrap().device_machine_identification.role, 2,);
}
}
-74
View File
@@ -1,74 +0,0 @@
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
use anyhow::Error;
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
}
impl Default for MachineRegistry {
fn default() -> Self {
Self::new()
}
}
impl MachineRegistry {
pub fn new() -> Self {
Self {
type_map: HashMap::new(),
}
}
pub fn register<T: Machine + 'static>(
&mut self,
machine_identficiation: MachineIdentification,
) {
self.type_map.insert(
TypeId::of::<T>(),
(
machine_identficiation,
// create a machine construction closure
Box::new(|machine_new_params| {
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
}),
),
);
}
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
.device_group
.first()
.ok_or(anyhow::anyhow!(
"[{}::MachineConstructor::new_machine] No device in group",
module_path!()
))?;
// find machine new function by comparing MachineIdentification
let (_, machine_new_closure) = self
.type_map
.values()
.find(|(mi, _)| {
mi == &device_identification
.device_machine_identification
.machine_identification_unique
.machine_identification
})
.ok_or(anyhow::anyhow!(
"[{}::MachineConstructor::new_machine] Machine not found",
module_path!()
))?;
// call machine new function by reference
(machine_new_closure)(machine_new_params)
}
}
-1
View File
@@ -1 +0,0 @@
pub mod mutation;
+5 -9
View File
@@ -1,14 +1,10 @@
use std::{any::Any, sync::Arc};
/*use std::{any::Any, sync::Arc};
use smol::{channel::Sender, lock::RwLock};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::{
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
};
use crate::machines::identification::DeviceIdentification;
pub mod panic;
pub mod registry;
pub mod serial_detection;
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
@@ -27,11 +23,11 @@ pub trait SerialDeviceThread {
pub struct SerialDeviceNewParams {
pub path: String,
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
*/
-53
View File
@@ -1,53 +0,0 @@
use smol::channel::Sender;
use crate::serial::serial_detection::SerialDeviceRemoval;
/// Device-level panic handler for individual device crashes
/// Sends device identifier and error information for cleanup
/// Used by serial devices and other individual components that need to be removed on panic
pub fn send_serial_device_panic<T>(
device_identifier: T,
thread_panic_tx: Sender<SerialDeviceRemoval<T>>,
) where
T: Send + Sync + Clone + 'static,
{
// Ensure backtrace is enabled for panics
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "full");
}
}
std::panic::set_hook(Box::new(move |panic_info| {
let thread_name = std::thread::current()
.name()
.unwrap_or("unnamed")
.to_string();
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
});
let message = panic_info
.payload()
.downcast_ref::<&str>()
.unwrap_or(&"Box<dyn Any>")
.to_string();
let panic_message = format!(
"Device panicked in thread '{}' at {}: {}",
thread_name, location, message
);
// Send device panic info through channel
smol::block_on(async {
let _ = thread_panic_tx
.send(SerialDeviceRemoval::Error(
device_identifier.clone(),
anyhow::anyhow!("{}", panic_message),
))
.await;
});
// Note: We don't call old_hook to avoid duplicate logging
// All logging is handled in the main thread when receiving panic details
}));
}
+48 -172
View File
@@ -7,194 +7,70 @@
*/
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
use smol::{
channel::{Receiver, Sender, unbounded},
lock::RwLock,
};
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use crate::{
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
};
use super::SerialDeviceIdentification;
/// Handles scanning and caching of connected USB serial devices.
pub struct SerialDetection {}
use super::{
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
};
pub enum SerialDeviceRemoval<T> {
Disconnect(T),
Error(T, anyhow::Error),
}
pub struct SerialDetection<'serialdeviceregistry> {
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
pub ports: HashMap<
String,
(
UsbPortInfo,
DeviceIdentification,
Arc<RwLock<dyn SerialDevice>>,
),
>,
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
}
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
SerialDetection {
serial_device_registry,
ports: HashMap::new(),
device_removal_signal_rx,
device_removal_signal_tx,
impl SerialDetection {
/// Returns all serial ports on the system (minus macOS "cu" noise).
fn get_ports() -> Vec<SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports
.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect(),
Err(_) => Vec::new(),
}
}
/*
*@return: Option::None if there are no available ports on device
*@return: Option::Some(Vec) with all available ports
*
*@description: detects all available ports and returns them as list
*/
fn get_ports() -> Vec<SerialPortInfo> {
let all = match serialport::available_ports() {
Ok(ports) => ports,
Err(_) => return Vec::new(),
};
// exclude ports that contai "cu" in their name
all.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect::<Vec<_>>()
}
/* @param: port_list -> list of available ports
*
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
*
* @description: This function extracts the USB serial devices from the given list of ports.
*/
fn extract_usb_serial_devices(
&self,
port_list: Vec<SerialPortInfo>,
) -> Vec<(String, UsbPortInfo)> {
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
/// Extracts only USB serial devices from the port list.
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
let mut map = HashMap::new();
for port in port_list {
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
};
if let SerialPortType::UsbPort(usb_info) = port.port_type {
map.insert(port.port_name, usb_info);
}
}
usb_ports
map
}
pub async fn check_ports(&mut self) -> CheckPortsResult {
// get available ports
/// Scans for connected USB serial devices and updates the internal map.
/// Returns a clone of the map for quick access.
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
let ports = Self::get_ports();
// extract all ports that are usb ports
let usb_ports = self.extract_usb_serial_devices(ports);
let last_usb_ports = self
.ports
.iter()
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
.collect::<Vec<_>>();
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
let mut result = CheckPortsResult {
added: Vec::new(),
removed: Vec::new(),
};
// reamove ports
for removed in usb_ports_diff.removed {
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
result.removed.push(device_identification);
}
tracing::trace!("Removed port: {}", removed.0);
}
// add new ports
for added in usb_ports_diff.added {
// add the port to the list
let serial_device_identification = SerialDeviceIdentification {
vendor_id: added.1.vid,
product_id: added.1.pid,
};
let device_result = self.serial_device_registry.new_serial_device(
&SerialDeviceNewParams {
path: added.0.clone(),
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
},
&serial_device_identification,
);
// only if created device driver sucessfully
if let Ok((device_identification, device)) = device_result {
// add the device to the ports list
self.ports.insert(
added.0.clone(),
(
added.1.clone(),
device_identification.clone(),
device.clone(),
),
);
// add to result list
result.added.push((device_identification, device.clone()));
tracing::trace!("Added port: {}", added.0);
}
}
result
let usb_devices = Self::extract_usb_serial_devices(ports);
usb_devices
}
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
pub fn device_port_exists(device_path: &str) -> bool {
let ports = Self::get_ports();
return ports.iter().any(|p| p.port_name == device_path);
}
let mut add_removal_signal = |path: &String| {
if let Some(info) = self.ports.get(path) {
removed_signals.push(info.1.clone());
};
self.ports.remove(path);
};
if self.device_removal_signal_rx.is_empty() {
return removed_signals;
}
let removal = self
.device_removal_signal_rx
.recv()
.await
.expect("Failed to receive removal signals");
match removal {
SerialDeviceRemoval::Disconnect(path) => {
add_removal_signal(&path);
tracing::trace!("Removed serial device after disconnect: {:?}", path);
}
SerialDeviceRemoval::Error(path, error) => {
add_removal_signal(&path);
tracing::error!("Removed erroring serial device: {:?}: {}", path, error);
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
let ports = Self::get_ports();
for port in ports {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
return true;
}
}
}
return false;
}
removed_signals
pub fn get_path_by_id(
sdevid: SerialDeviceIdentification,
map: HashMap<String, UsbPortInfo>,
) -> Option<String> {
for (port, usbport) in map.iter() {
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
return Some(port.clone());
}
}
return None;
}
}
#[derive(Debug)]
pub struct CheckPortsResult {
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
pub removed: Vec<DeviceIdentification>,
}
-1
View File
@@ -1,3 +1,2 @@
pub mod event;
pub mod namespace;
pub mod namespace_id;
+2 -3
View File
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tracing::instrument;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Namespace {
pub sockets: Vec<SocketRef>,
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
@@ -118,7 +118,6 @@ impl Namespace {
) {
// cache the event
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
for socket in self.sockets.clone() {
@@ -168,7 +167,7 @@ impl Namespace {
impl Drop for Namespace {
fn drop(&mut self) {
self.disconnect_all();
//self.disconnect_all();
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::acceleration;
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
"meters per minute per second";
}
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::angular_acceleration;
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
"revolutions per minute per second";
}
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::angular_jerk;
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
"revolution per minute per second squared";
}
-7
View File
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::jerk;
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
"meters per minute per second squared";
}
-5
View File
@@ -1,5 +0,0 @@
pub mod acceleration;
pub mod angular_acceleration;
pub mod angular_jerk;
pub mod jerk;
pub mod velocity;
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::velocity;
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
"meters per minute";
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
# Laser DRE Device Documentation
## Overview
The **Laser DRE** is a precision measurement device designed for real-time diameter measurement of rotating objects. It communicates over a **USB Serial interface** using the **Modbus RTU** protocol.
Key features include:
- High-speed diameter measurement
- Dual-axis capability
- USB Serial communication
- Modbus RTU protocol support
---
## Technical Specifications
- **Communication Interface:** USB Serial
- **Protocol:** Modbus RTU
- **Axes:** 2-axis measurement
- **Polling Rate:** Every 16 ms
- **Measurement Rate:** 320 measurements per second (manufacturer specification)
- **Measurement Type:** Diameter measurement | X/Y Axis width
---
## Operation
### Polling
The device **polls the diameter every 16 milliseconds**, ensuring near real-time data acquisition for precise monitoring.
### Measurement
According to the manufacturer, the Laser DRE can measure the diameter **320 times per second**, providing high-resolution data for process control and analysis.
### Axes
The device is capable of measuring across **two axes**, allowing for comprehensive profiling of the object’s diameter and shape.
---
## Communication Protocol
The device uses **Modbus RTU** over the USB Serial connection. Key points for communication:
- **Master/Slave Model:** The Laser DRE acts as a Modbus slave device.
- **Function Codes:** Supports standard Modbus RTU function codes for reading measurements.
- **Data Format:** Diameter measurements are transmitted in the format specified by the manufacturer (consult the device manual for exact register addresses and scaling).
---
## Settings
The device includes a small display that allows changing several operational settings:
- **Unit:**
The laser can operate in **millimeters (mm)** or **inches (in)**.
Our software uses **millimeters**.
- **Alarm:**
The laser can emit an audible alarm if the measured diameter is out of tolerance.
⚠️ **Note:** The tolerance settings on the laser may differ from the tolerance configured in our software.
- **Communication:**
Several communication parameters can be configured:
- **Set UART:** Configure **parity (8N1)** and **baud rate (38400)**.
- **Set Protocol:** Choose the communication protocol. *(We use **Modbus RTU**.)*
- **Set Slave Address:** The slave address of our laser is **1**.
- **Calibration:**
The laser can be calibrated using **4 mm** and **8 mm** reference rods.
+1 -1
View File
@@ -73,7 +73,7 @@ typings/
# vuepress build output
.vuepress/dist
# Serverless directories
# Serverless directoriesProcessMetrics
.serverless/
# FuseBox cache
+3900 -1633
View File
File diff suppressed because it is too large. Load diff
+11 -9
View File
@@ -28,13 +28,13 @@
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.10.2",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^2.0.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -42,8 +42,9 @@
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/node": "^24.2.1",
"@types/react": "^19.0.10",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
@@ -54,24 +55,24 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.6.2",
"prettier": "^3.7.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.3",
@@ -80,7 +81,7 @@
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.2",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-router": "^1.105.0",
"@tanstack/react-router": "^1.130.17",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
"@tanstack/zod-adapter": "^1.116.0",
@@ -99,6 +100,7 @@
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

+7 -1
View File
@@ -9,6 +9,7 @@ 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();
@@ -21,7 +22,12 @@ export default function App() {
updateAppLanguage(i18n);
}, [i18n]);
return <RouterProvider router={router} />;
return (
<>
<GlobalLaserToastManager />
<RouterProvider router={router} />
</>
);
}
// enable immer MapSet plugin
@@ -720,3 +720,7 @@ The following persons must not work on the machine:
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
## Mechanical Diagrams
![QiTech Pro Extruder Nozzle 2025](images/manuals/extruder/Nozzle.png)
@@ -416,6 +416,29 @@ Then start the machine as follows:
<br>
**Understanding Padding**
The **padding** parameter creates a safety margin that prevents the filament from winding too close to the spool edges. When you set the inner and outer limits during calibration, the padding value is automatically subtracted from the actual travel distance.
**How padding works:**
- When you have a 1.75mm diameter filament and the traverse moves to a flank position, the **center** of the filament aligns with that flank
- If you set 1mm padding, the system automatically maintains at least 1mm distance from the flanks
- This prevents the filament from overlapping or winding beyond the spool edges
**Example:**
- Outer limit: 90mm, Inner limit: 22mm
- Padding: 1mm
- **Actual winding area:** 89mm to 23mm (reduced by 1mm on each side)
- This ensures the filament stays within the spool boundaries with a safe margin
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
![Padding example](images/manuals/winder/winder_padding_example_image.jpg)
<br>
**4. Start pulling**
- Select “Control” in the function bar
+10 -1
View File
@@ -145,8 +145,17 @@ export function mainMessageHandler(
};
}
// Create the store instance once and export it
export const mainNamespaceStore: StoreApi<MainNamespaceStore> =
createMainNamespaceStore();
// Message handler stays the same
export const mainMessageHandlerInstance: EventHandler =
mainMessageHandler(mainNamespaceStore);
// Hook that uses the exported store
const mainRoomImplementation = createNamespaceHookImplementation({
createStore: createMainNamespaceStore,
createStore: () => mainNamespaceStore, // use the exported store
createEventHandler: mainMessageHandler,
});
+26 -23
View File
@@ -8,6 +8,8 @@ import { z } from "zod";
import { toastError, toastZodError } from "@/components/Toast";
import { MachineIdentificationUnique } from "@/machines/types";
import { FPS_30 } from "@/lib/constants";
import { mainNamespaceStore, createMainNamespaceStore } from "./mainNamespace";
import { softReloadMainNamespaceStore } from "./softReload";
/**
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
@@ -305,27 +307,13 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
// add handlers
socket.on("connect", () => {
console.log(`Connected to ${namespace_path}`);
// reset the store
resetStore(set);
});
socket.on("disconnect", (reason) => {
socket.disconnect();
console.warn(`Disconnected from ${namespace_path}, reason: ${reason}`);
// reset the store
resetStore(set);
// Attempt to reconnect after a short delay for any disconnect reason
console.log(
`Disconnected from ${namespace_path}, attempting reconnect in 1s...`,
);
setTimeout(() => {
if (get().hasNamespace(namespaceId) && !socket.connected) {
console.log(`Reconnecting to ${namespace_path}...`);
socket.connect();
}
}, 1000);
// Its hacky but i do not care, electron + react is annoying ...
window.location.reload();
});
socket.on("event", (event: unknown) => {
@@ -334,11 +322,9 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
if (!event_parsed.success) {
toastZodError(event_parsed.error, "Invalid event");
return;
}
// handle the event
} // handle the event
get().namespaces[namespace_path].handler(event_parsed.data);
});
// store the namespace initally
set(
produce((state: SocketioStore) => {
@@ -356,8 +342,25 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}),
);
// finally connect
socket.connect();
if (namespace_path !== "/main") {
const intervalId = setInterval(() => {
const mainState = mainNamespaceStore.getState();
const machineExists =
namespaceId.type === "machine" &&
mainState.machines?.data?.machines.some(
(m) =>
m.machine_identification_unique.serial ===
namespaceId.machine_identification_unique.serial,
);
//console.log(machineExists);
if (machineExists && !socket.connected) {
socket.connect();
clearInterval(intervalId); // stop polling
}
}, 500);
} else {
socket.connect();
}
},
incrementNamespace: (namespaceId: NamespaceId) => {
const namespace_path = serializeNamespaceId(namespaceId);
@@ -386,7 +389,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
// check if the namespace exists
const namespace = get().namespaces[namespace_path];
if (namespace) {
/*if (namespace) {
set(
produce((state: SocketioStore) => {
// decrement the count
@@ -430,7 +433,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}
}),
);
}
}*/
},
}));
+17
View File
@@ -0,0 +1,17 @@
import { mainNamespaceStore, mainMessageHandler } from "./mainNamespace";
// Soft reset function
export function softReloadMainNamespaceStore() {
// 1️⃣ Reset the store to initial state
mainNamespaceStore.setState({
ethercatDevices: null,
machines: null,
ethercatInterfaceDiscovery: null,
});
// 2️⃣ Re-attach the message handler if needed
// This ensures future socket events still update the store
mainMessageHandler(mainNamespaceStore);
console.log("MainNamespaceStore soft reloaded");
}
+2 -7
View File
@@ -58,15 +58,10 @@ export function MarkdownWithToc({
className={`sticky ${stickyTop}`}
style={{ maxHeight: "calc(100vh - 6rem)" }}
>
{/* Hide scrollbar for the TOC container across browsers */}
<style>{`
[data-toc-scroll-container] { -ms-overflow-style: none; scrollbar-width: none; }
[data-toc-scroll-container]::-webkit-scrollbar { display: none; }
`}</style>
{/* Table of Contents with hidden scrollbar */}
<div
className="overflow-y-auto"
className="scrollbar-hidden overflow-y-auto"
style={{ maxHeight: "calc(100vh - 8rem)" }}
data-toc-scroll-container
>
<MarkdownToc
markdownContent={markdownContent}
+52 -2
View File
@@ -1,10 +1,16 @@
import { OutsideCorner } from "@/components/OutsideCorner";
import { useMachines } from "@/client/useMachines";
import { useOnSubpath } from "@/lib/useOnSubpath";
import { Link, Outlet } from "@tanstack/react-router";
import { Fragment } from "react";
import {
Link,
Outlet,
useRouter,
useRouterState,
} from "@tanstack/react-router";
import { Fragment, useEffect } from "react";
import React from "react";
import { Icon, IconName } from "./Icon";
import { useMainNamespace } from "@/client/mainNamespace";
type SidebarItemContent = {
link: string;
@@ -57,7 +63,51 @@ export function useSidebarlessWidth() {
export function SidebarLayout() {
const machines = useMachines();
const router = useRouter();
const routerState = useRouterState();
const { machines: machinesEvent } = useMainNamespace();
const [contentWidth, setContentWidth] = React.useState<number>(0);
// Machine connection guard: redirect to setup if selected machine disconnects
useEffect(() => {
// Wait for machines data to be loaded
if (!machinesEvent?.data) return;
// Extract serial from current path (e.g., /machines/laser1/12345/control)
const pathMatch = routerState.location.pathname.match(
/\/machines\/[^/]+\/(\d+)/,
);
if (!pathMatch) return; // Not on a machine page
// Safety check: ensure pathMatch has at least 2 elements (full match + capture group)
if (pathMatch.length < 2) {
console.warn("[ConnectionGuard] Invalid pathMatch structure:", pathMatch);
return;
}
// Parse serial number with error handling
const serialString = pathMatch[1];
const serialNumber = parseInt(serialString, 10);
// Check if parseInt succeeded (NaN means parsing failed)
if (isNaN(serialNumber)) {
console.warn(
`[ConnectionGuard] Failed to parse serial number from "${serialString}"`,
{ pathname: routerState.location.pathname },
);
return;
}
// Check if machine still exists in the raw machines data
const machineExists = machinesEvent.data.machines.some(
(m) => m.machine_identification_unique.serial === serialNumber,
);
if (!machineExists) {
router.navigate({ to: "/_sidebar/setup/machines" });
}
}, [machines, machinesEvent, routerState.location.pathname, router]);
const items: SidebarItemContent[] = [
...machines.map((machine) => ({
link: `/_sidebar/machines/${machine.slug}/${machine.machine_identification_unique.serial}/control`,
+78 -123
View File
@@ -1,20 +1,32 @@
import { cva } from "class-variance-authority";
import React, { useEffect, useRef, useState } from "react";
import { Icon } from "./Icon";
import {
List,
CellMeasurer,
CellMeasurerCache,
ListRowRenderer,
AutoSizer,
} from "react-virtualized";
// Create cache (outside the component so it persists across renders)
const cache = new CellMeasurerCache({
defaultHeight: 20,
fixedWidth: true,
});
type Props = {
lines: string[];
autoScroll?: boolean; // Optional prop to control if terminal should auto-scroll
className?: string; // Optional prop to control terminal height
autoScroll?: boolean;
className?: string;
title?: string;
exportPrefix?: string; // Optional prefix for exported log files
exportPrefix?: string;
};
const terminalStyle = cva([
"flex flex-col overflow-hidden rounded-md border border-neutral-700 font-mono text-sm",
]);
// Color mapping for ANSI color codes
const colorMap: Record<string, string> = {
"30": "text-gray-900",
"31": "text-red-600",
@@ -32,7 +44,6 @@ const colorMap: Record<string, string> = {
"95": "text-purple-400",
"96": "text-cyan-400",
"97": "text-white",
// Background colors
"40": "bg-gray-900",
"41": "bg-red-600",
"42": "bg-green-600",
@@ -43,36 +54,26 @@ const colorMap: Record<string, string> = {
"47": "bg-gray-200",
};
// Parse ANSI color codes in text
const parseColorCodes = (text: string) => {
// Split by ANSI escape sequences
// eslint-disable-next-line no-control-regex
/* eslint-disable-next-line no-control-regex */
const parts = text.split(/(\x1b\[\d+m)/g);
let currentClass = "";
const result: { text: string; className: string }[] = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.startsWith("\x1b[")) {
// This is a color code
const code = part.slice(2, -1); // Extract the number from \x1b[XXm
const code = part.slice(2, -1);
currentClass = colorMap[code] || "";
} else if (part) {
// This is text content
result.push({ text: part, className: currentClass });
}
}
return result;
};
// Function to strip ANSI color codes for plain text copy
const stripColorCodes = (text: string): string => {
const stripColorCodes = (text: string): string =>
// eslint-disable-next-line no-control-regex
return text.replace(/\x1b\[\d+m/g, "");
};
text.replace(/\x1b\[\d+m/g, "");
export function Terminal({
lines,
@@ -81,99 +82,88 @@ export function Terminal({
title = "Terminal",
exportPrefix,
}: Props) {
const terminalRef = useRef<HTMLDivElement>(null);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
const listRef = useRef<List>(null);
const [copySuccess, setCopySuccess] = useState(false);
const [exportSuccess, setExportSuccess] = useState(false);
// Handle scrolling
// Auto-scroll to bottom when new lines arrive
useEffect(() => {
const terminal = terminalRef.current;
if (!terminal) return;
// If auto-scroll is enabled and user was at bottom, scroll to bottom when lines change
if (autoScroll && isScrolledToBottom) {
terminal.scrollTop = terminal.scrollHeight;
if (autoScroll && listRef.current) {
listRef.current.scrollToRow(lines.length - 1);
}
}, [lines, autoScroll, isScrolledToBottom]);
}, [lines, autoScroll]);
// Handle scroll events to detect if user is at bottom
const handleScroll = () => {
const terminal = terminalRef.current;
if (!terminal) return;
const isAtBottom =
Math.abs(
terminal.scrollHeight - terminal.clientHeight - terminal.scrollTop,
) < 10; // Small threshold to account for rounding errors
setIsScrolledToBottom(isAtBottom);
};
// Handle copy to clipboard
const handleCopy = async () => {
// Strip ANSI color codes and join lines
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
const plainText = lines.map(stripColorCodes).join("\n");
try {
await navigator.clipboard.writeText(plainText);
setCopySuccess(true);
// Reset copy success message after 2 seconds
setTimeout(() => {
setCopySuccess(false);
}, 2000);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
// Handle export to file
const handleExport = () => {
if (!exportPrefix) return;
// Strip ANSI color codes and join lines
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
// Create timestamp for filename
const plainText = lines.map(stripColorCodes).join("\n");
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, -5);
const filename = `${exportPrefix}_${timestamp}.log`;
// Create blob and download
const blob = new Blob([plainText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
// Create temporary link element and trigger download
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up
URL.revokeObjectURL(url);
// Show success feedback
setExportSuccess(true);
setTimeout(() => {
setExportSuccess(false);
}, 2000);
setTimeout(() => setExportSuccess(false), 2000);
};
const rowRenderer: ListRowRenderer = ({ index, key, parent, style }) => {
const colorParts = parseColorCodes(lines[index] || "");
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<div
style={style}
className="bg-black px-2 py-1 whitespace-pre-wrap text-white"
>
{colorParts.length > 0
? colorParts.map((part, partIndex) => (
<span
key={partIndex}
className={part.className || "text-white"}
>
{part.text || " "}
</span>
))
: lines[index] || " "}
</div>
</CellMeasurer>
);
};
return (
<div className={terminalStyle({ className })}>
{/* Terminal header */}
{/* Header */}
<div className="flex items-center justify-between border-b border-neutral-700 bg-neutral-800 px-4 py-2">
<div className="text-xs text-neutral-400">{title}</div>
<div className="flex items-center gap-2">
<button
onClick={handleCopy}
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
title="Copy to clipboard"
>
{copySuccess ? (
<>
@@ -191,7 +181,6 @@ export function Terminal({
<button
onClick={handleExport}
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
title="Export logs to file"
>
{exportSuccess ? (
<>
@@ -209,62 +198,28 @@ export function Terminal({
</div>
</div>
{/* Terminal content with custom scrollbar styling */}
<div
ref={terminalRef}
onScroll={handleScroll}
className={`scrollbar-thin scrollbar-thumb-neutral-600 scrollbar-track-transparent flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300`}
style={{
scrollbarWidth: "thin",
scrollbarColor: "rgb(82 82 91) transparent",
}}
>
<style>{`
Add commentMore actions
/* For Webkit browsers (Chrome, Safari) */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(82 82 91);
border-radius: 3px;
}
/* For Firefox */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(82 82 91) transparent;
}
`}</style>
{lines.map((line, index) => {
const colorParts = parseColorCodes(line);
return (
<div key={index} className="whitespace-pre-wrap">
{colorParts.length > 0
? colorParts.map((part, partIndex) => (
<span key={partIndex} className={part.className}>
{part.text || " "}
</span>
))
: line || " "}
</div>
);
})}
{/* Virtualized List */}
<div className="flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300">
<AutoSizer>
{({ width, height }) => (
<List
ref={listRef}
width={width}
height={height}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowCount={lines.length}
rowRenderer={rowRenderer}
overscanRowCount={5}
/>
)}
</AutoSizer>
</div>
{/* Status bar */}
{/* Footer */}
<div className="flex items-center justify-between bg-neutral-800 px-4 py-1 text-xs text-neutral-400">
<div>{lines.length} lines</div>
<div>
{isScrolledToBottom ? "At bottom" : "Scrolled up"} |
{autoScroll ? " Auto-scroll enabled" : " Auto-scroll disabled"}
</div>
<div>{autoScroll ? "Auto-scroll enabled" : "Auto-scroll disabled"}</div>
</div>
</div>
);
@@ -0,0 +1,137 @@
import React from "react";
import { Icon } from "./Icon";
import { cn } from "@/lib/utils";
import type { UpdateStep } from "@/stores/updateStore";
interface UpdateProgressBarProps {
steps: UpdateStep[];
overallProgress: number;
className?: string;
}
export function UpdateProgressBar({
steps,
overallProgress,
className,
}: UpdateProgressBarProps) {
return (
<div className={cn("space-y-4", className)}>
{/* Overall Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Overall Progress</span>
<span className="text-sm font-semibold tabular-nums">
{overallProgress}%
</span>
</div>
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
<div
className="h-full rounded-full bg-gray-900 transition-all duration-500 ease-out dark:bg-gray-100"
style={{ width: `${overallProgress}%` }}
/>
</div>
</div>
{/* Steps List */}
<div className="space-y-2">
{steps.map((step) => (
<div
key={step.id}
className={cn(
"flex items-center gap-3 rounded border px-3 py-2.5 transition-all duration-200",
step.status === "in-progress" &&
"border-gray-400 bg-gray-50 dark:border-gray-600 dark:bg-gray-900",
step.status === "completed" &&
"border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-950",
step.status === "error" &&
"border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/30",
step.status === "pending" &&
"border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950",
)}
>
{/* Status Icon */}
<div className="flex-shrink-0">
{step.status === "pending" && (
<div className="size-5 rounded-full border-2 border-gray-300 dark:border-gray-700" />
)}
{step.status === "in-progress" && (
<Icon
name="lu:Loader"
className="size-5 animate-spin text-gray-600 dark:text-gray-400"
/>
)}
{step.status === "completed" && (
<div className="flex size-5 items-center justify-center rounded-full bg-gray-700 dark:bg-gray-300">
<Icon
name="lu:Check"
className="size-3 text-white dark:text-gray-900"
/>
</div>
)}
{step.status === "error" && (
<div className="flex size-5 items-center justify-center rounded-full bg-red-600 dark:bg-red-500">
<Icon name="lu:X" className="size-3 text-white" />
</div>
)}
</div>
{/* Step Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className={cn(
"text-sm",
step.status === "completed" &&
"text-gray-600 dark:text-gray-400",
step.status === "error" &&
"font-medium text-red-700 dark:text-red-400",
step.status === "in-progress" &&
"font-medium text-gray-900 dark:text-gray-100",
step.status === "pending" &&
"text-gray-500 dark:text-gray-500",
)}
>
{step.label}
</span>
{/* Subsector Badge - minimal inline display */}
<span
className={cn("text-xs text-gray-400 dark:text-gray-600")}
>
(
{step.id === "rust-build"
? "rust & electron"
: step.subsector}
)
</span>
</div>
{/* Progress percentage for in-progress steps */}
{step.status === "in-progress" &&
step.progress !== undefined &&
step.progress > 0 && (
<span className="text-xs font-medium text-gray-600 tabular-nums dark:text-gray-400">
{Math.round(step.progress)}%
</span>
)}
</div>
{/* Progress bar for steps with detailed tracking */}
{step.status === "in-progress" &&
step.progress !== undefined &&
step.progress > 0 && (
<div className="mt-2">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-full rounded-full bg-gray-700 transition-all duration-500 dark:bg-gray-300"
style={{ width: `${step.progress}%` }}
/>
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}
@@ -474,6 +474,7 @@ export function BigGraph({
isLiveMode,
selectedTimeWindow,
visibleSeries, // Keep for initial creation
showFromTimestamp: syncGraph?.showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
setIsLiveMode,
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TimeInput } from "@/components/time/TimeInput";
import {
DropdownMenu,
DropdownMenuContent,
@@ -21,8 +22,14 @@ export function GraphControls({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -35,7 +42,10 @@ export function GraphControls({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -60,6 +70,15 @@ export function GraphControls({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
@@ -109,10 +128,16 @@ export function FloatingControlPanel({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const [isExpanded, setIsExpanded] = useState(false);
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -134,7 +159,10 @@ export function FloatingControlPanel({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -159,6 +187,15 @@ export function FloatingControlPanel({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
+6 -7
View File
@@ -31,10 +31,8 @@ export function createChart({
setCursorValue,
setCursorValues,
visibleSeries,
}: CreateChartParams & {
setCursorValues?: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries?: boolean[];
}): (() => void) | undefined {
showFromTimestamp,
}: CreateChartParams): (() => void) | undefined {
if (!containerRef.current) return;
// Get ALL series from original data
@@ -73,7 +71,7 @@ export function createChart({
initialMin = manualScaleRef.current.x.min;
initialMax = manualScaleRef.current.x.max;
} else if (selectedTimeWindow === "all") {
initialMin = fullStart;
initialMin = showFromTimestamp ?? fullStart;
initialMax = getHistoricalEndTimestamp();
} else {
const endTimestamp = getHistoricalEndTimestamp();
@@ -83,9 +81,10 @@ export function createChart({
endTimestamp - (selectedTimeWindow as number),
fullStart,
);
initialMin = defaultViewStart;
initialMin = showFromTimestamp ?? defaultViewStart;
} else {
initialMin = endTimestamp - (selectedTimeWindow as number);
initialMin =
showFromTimestamp ?? endTimestamp - (selectedTimeWindow as number);
}
initialMax = endTimestamp;
+11 -1
View File
@@ -10,6 +10,7 @@ export type PropGraphSync = {
isLiveMode: boolean;
xRange?: { min: number; max: number };
historicalFreezeTimestamp?: number | null;
showFromTimestamp?: number | null;
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
onViewModeChange?: (
graphId: string,
@@ -62,7 +63,11 @@ export type DataSeries = SeriesData | SeriesData[];
export type BigGraphProps = {
newData: DataSeries;
unit?: Unit;
renderValue?: (value: number) => string;
renderValue?: (
value: number,
seriesIndex?: number,
seriesTitle?: string,
) => string;
config: GraphConfig;
graphId: string;
syncGraph?: PropGraphSync;
@@ -81,6 +86,8 @@ export type ControlProps = {
onSwitchToHistorical: () => void;
onExport?: () => void;
timeWindowOptions?: TimeWindowOption[];
showFromTimestamp?: number | null;
onShowFromChange?: (timestamp: number | null) => void;
};
export interface LiveModeHandlers {
@@ -128,6 +135,9 @@ export interface CreateChartParams {
>;
setIsLiveMode: React.Dispatch<React.SetStateAction<boolean>>;
setCursorValue: React.Dispatch<React.SetStateAction<number | null>>;
setCursorValues: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries: boolean[];
showFromTimestamp?: number | null;
}
export interface AnimationState {
@@ -41,6 +41,7 @@ interface UseBigGraphEffectsProps {
isLiveMode: boolean;
selectedTimeWindow: number | "all";
visibleSeries: boolean[];
showFromTimestamp?: number | null;
// State setters
setSelectedTimeWindow: (value: number | "all") => void;
@@ -90,6 +91,7 @@ export function useBigGraphEffects({
isLiveMode,
selectedTimeWindow,
visibleSeries,
showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
@@ -275,6 +277,7 @@ export function useBigGraphEffects({
setCursorValue,
setCursorValues,
visibleSeries,
showFromTimestamp,
});
setIsChartCreated(true);
@@ -15,6 +15,9 @@ export function useGraphSync(exportGroupId?: string) {
const [xRange, setXRange] = useState<
{ min: number; max: number } | undefined
>();
const [showFromTimestamp, setShowFromTimestamp] = useState<number | null>(
null,
);
const historicalFreezeTimestampRef = useRef<number | null>(null);
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
@@ -59,6 +62,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode?: "default" | "all" | "manual";
isLiveMode?: boolean;
xRange?: { min: number; max: number } | undefined;
showFromTimestamp?: number | null;
clearHistoricalFreeze?: boolean;
setHistoricalFreeze?: boolean;
},
@@ -90,6 +94,8 @@ export function useGraphSync(exportGroupId?: string) {
if (updates.viewMode !== undefined) setViewMode(updates.viewMode);
if (updates.isLiveMode !== undefined) setIsLiveMode(updates.isLiveMode);
if (updates.xRange !== undefined) setXRange(updates.xRange);
if (updates.showFromTimestamp !== undefined)
setShowFromTimestamp(updates.showFromTimestamp);
// Handle historical freeze timestamp
if (updates.clearHistoricalFreeze) {
@@ -195,6 +201,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode: timeWindow === "all" ? "all" : "default",
xRange: undefined,
clearHistoricalFreeze: true,
showFromTimestamp: null, // Clear the show from timestamp when switching to live
});
}, [updateSyncState, timeWindow]);
@@ -206,6 +213,15 @@ export function useGraphSync(exportGroupId?: string) {
});
}, [updateSyncState]);
const handleShowFromChange = useCallback(
(timestamp: number | null) => {
updateSyncState("control", {
showFromTimestamp: timestamp,
});
},
[updateSyncState],
);
const handleControlTimeWindowChange = useCallback(
(newTimeWindow: number | "all") => {
updateSyncState("control", {
@@ -225,6 +241,7 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
onTimeWindowChange: handleTimeWindowChange,
onViewModeChange: handleViewModeChange,
onZoomChange: handleZoomChangeThrottled,
@@ -237,6 +254,8 @@ export function useGraphSync(exportGroupId?: string) {
onSwitchToLive: handleSwitchToLive,
onSwitchToHistorical: handleSwitchToHistorical,
onExport: handleExport,
showFromTimestamp,
onShowFromChange: handleShowFromChange,
};
return {
@@ -250,5 +269,6 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
};
}
+367
View File
@@ -0,0 +1,367 @@
import React, { useState, useCallback, useEffect } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Separator } from "@/components/ui/separator";
type TimeInputProps = {
timestamp?: number | null;
onTimeChange?: (timestamp: number | null) => void;
onClear?: () => void;
};
const timeSchema = z.object({
hours: z.number().min(0).max(23),
minutes: z.number().min(0).max(59),
seconds: z.number().min(0).max(59),
});
type TimeFormData = z.infer<typeof timeSchema>;
export function TimeInput({
timestamp,
onTimeChange,
onClear,
}: TimeInputProps) {
const [isOpen, setIsOpen] = useState(false);
// Get current time for initial values
const getCurrentTime = useCallback(() => {
const now = new Date();
return {
hours: now.getHours(),
minutes: now.getMinutes(),
seconds: now.getSeconds(),
};
}, []);
// Get initial values based on timestamp or current time
const getInitialValues = useCallback(() => {
if (timestamp) {
const date = new Date(timestamp);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}
return getCurrentTime();
}, [timestamp, getCurrentTime]);
const form = useForm<TimeFormData>({
resolver: zodResolver(timeSchema),
defaultValues: getInitialValues(),
mode: "all",
});
// Convert timestamp to time components
const timestampToTime = useCallback((ts: number) => {
const date = new Date(ts);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}, []);
// Check if the selected time would be in the future (today)
const isTimeInFuture = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime() > now.getTime();
}, []);
// Convert time components to timestamp (today's date only)
const timeToTimestamp = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime();
}, []);
// Initialize form with timestamp or current time when dialog opens
useEffect(() => {
if (isOpen) {
const initialValues = getInitialValues();
form.reset(initialValues);
}
}, [isOpen, getInitialValues, form]);
const formatTimeDisplay = useCallback(() => {
if (!timestamp) return "Show from Time";
const time = timestampToTime(timestamp);
return `${time.hours.toString().padStart(2, "0")}:${time.minutes.toString().padStart(2, "0")}:${time.seconds.toString().padStart(2, "0")}`;
}, [timestamp, timestampToTime]);
// Increment/decrement functions with bounds checking and cascading
const adjustTimeValue = useCallback(
(field: "hours" | "minutes" | "seconds", increment: boolean) => {
// Define the order of fields for cascading
const fields: ("hours" | "minutes" | "seconds")[] = [
"seconds",
"minutes",
"hours",
];
const startIndex = fields.indexOf(field);
let carry = increment ? 1 : -1;
for (let i = startIndex; i < fields.length; i++) {
const currentField = fields[i];
const currentValue = form.getValues(currentField);
const max = currentField === "hours" ? 23 : 59;
let newValue = currentValue + carry;
carry = 0;
if (increment) {
if (newValue > max) {
newValue = 0;
carry = 1;
}
} else {
if (newValue < 0) {
newValue = max;
carry = -1;
}
}
form.setValue(currentField, newValue, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
// If no carry, stop cascading
if (carry === 0) {
break;
}
}
},
[form],
);
const handleSubmit = () => {
form.handleSubmit((data) => {
// Prevent submission if time is in the future
if (isTimeInFuture(data)) {
return;
}
const newTimestamp = timeToTimestamp(data);
onTimeChange?.(newTimestamp);
setIsOpen(false);
})();
};
const handleAbort = () => {
if (timestamp) {
const time = timestampToTime(timestamp);
form.reset(time);
} else {
const currentTime = getCurrentTime();
form.reset(currentTime);
}
setIsOpen(false);
};
const handleClear = () => {
onClear?.();
setIsOpen(false);
};
// Watch individual form fields for real-time updates
const hours = useWatch({
control: form.control,
name: "hours",
defaultValue: getInitialValues().hours,
});
const minutes = useWatch({
control: form.control,
name: "minutes",
defaultValue: getInitialValues().minutes,
});
const seconds = useWatch({
control: form.control,
name: "seconds",
defaultValue: getInitialValues().seconds,
});
// Get current form values with fallback
const currentValues = {
hours: hours ?? 0,
minutes: minutes ?? 0,
seconds: seconds ?? 0,
};
// TimeValueInput component for individual time fields
const TimeValueInput = ({
field,
label,
value,
}: {
field: "hours" | "minutes" | "seconds";
label: string;
value: number;
}) => (
<div className="flex flex-col items-center gap-3">
<label className="text-sm font-medium text-gray-700">{label}</label>
<div className="flex flex-col items-center gap-2">
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, true);
}}
>
<Icon name="lu:ChevronUp" className="size-4" />
</TouchButton>
<div className="flex h-16 w-20 items-center justify-center rounded border-2 border-gray-300 bg-white font-mono text-3xl">
{value.toString().padStart(2, "0")}
</div>
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, false);
}}
>
<Icon name="lu:ChevronDown" className="size-4" />
</TouchButton>
</div>
</div>
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
>
<Icon name="lu:Clock" className="mr-2 size-4" />
{formatTimeDisplay()}
</TouchButton>
</DialogTrigger>
<DialogContent className="max-w-2xl p-8">
<div className="flex flex-col gap-8">
{/* Header */}
<div className="flex min-h-[2.5rem] items-center justify-between">
<div className="flex items-center gap-3">
<Icon name="lu:Clock" className="size-6" />
<h3 className="text-xl font-semibold">Show from Time</h3>
</div>
{timestamp && (
<TouchButton
variant="ghost"
size="sm"
onClick={handleClear}
className="text-red-600 hover:text-red-700"
>
<Icon name="lu:X" className="mr-2 size-4" />
Clear
</TouchButton>
)}
</div>
{/* Description */}
<div className="-mt-4 text-base text-gray-600">
Set a specific time to display data from that point forward in the
graph.
</div>
{/* Error for future time */}
{isTimeInFuture(currentValues) && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<div className="flex items-start gap-3">
<Icon name="lu:X" className="mt-0.5 size-5 text-red-600" />
<div>
<div className="font-medium text-red-800">
Cannot set future time
</div>
<div className="mt-1 text-sm text-red-700">
The selected time is in the future. Please choose a time
that has already occurred today.
</div>
</div>
</div>
</div>
)}
<Separator />
{/* Time inputs */}
<div className="flex items-center justify-center gap-8">
<TimeValueInput
field="hours"
label="Hours"
value={currentValues.hours}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="minutes"
label="Minutes"
value={currentValues.minutes}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="seconds"
label="Seconds"
value={currentValues.seconds}
/>
</div>
<Separator />
{/* Action buttons */}
<div className="flex gap-4">
<TouchButton
variant="outline"
className="h-14 flex-1"
onClick={handleAbort}
>
<Icon name="lu:X" className="mr-2 size-4" />
Cancel
</TouchButton>
<TouchButton
className={`h-14 flex-1 ${
isTimeInFuture(currentValues)
? "cursor-not-allowed bg-gray-300 text-gray-500"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
onClick={handleSubmit}
disabled={isTimeInFuture(currentValues)}
>
<Icon name="lu:Check" className="mr-2 size-4" />
Apply
</TouchButton>
</div>
</div>
</DialogContent>
</Dialog>
);
}
+1 -1
View File
@@ -109,7 +109,7 @@ export function EditValue({
type FormSchema = z.infer<typeof formSchema>;
const form = useForm<FormSchema>({
resolver: zodResolver<FormSchema>(formSchema),
resolver: zodResolver(formSchema),
values: { value: value ?? defaultOrZero },
defaultValues: { value: defaultValue },
mode: "all",
+173
View File
@@ -0,0 +1,173 @@
import { Icon, IconName } from "@/components/Icon";
import React, { useState, useMemo } from "react";
import {
getUnitIcon,
renderValueToReactNode,
renderUnitSymbol,
Unit,
} from "./units";
import { Label } from "./Label";
import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
type TimeframeOption = {
label: string;
value: number; // in milliseconds
};
export const TIMEFRAME_OPTIONS: TimeframeOption[] = [
{ label: "1 min", value: 1 * 60 * 1000 },
{ label: "5 min", value: 5 * 60 * 1000 },
{ label: "10 min", value: 10 * 60 * 1000 },
{ label: "30 min", value: 30 * 60 * 1000 },
{ label: "1 hour", value: 60 * 60 * 1000 },
];
type Props = {
label: string;
unit?: Unit;
timeseries: TimeSeries;
icon?: IconName;
renderValue?: (value: number) => string;
defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes
/** Optional externally-controlled timeframe (ms). When provided, component becomes controlled. */
timeframe?: number;
/** Optional callback when timeframe is changed via this component's selector */
onTimeframeChange?: (t: number) => void;
/** When true, hide the built-in timeframe selector (useful when sharing a selector outside) */
hideSelector?: boolean;
};
export function MinMaxValue({
unit,
timeseries,
icon,
label,
renderValue,
defaultTimeframe = 5 * 60 * 1000, // 5 minutes default
timeframe,
onTimeframeChange,
hideSelector = false,
}: Props) {
const [internalTimeframe, setInternalTimeframe] = useState(defaultTimeframe);
const selectedTimeframe = timeframe ?? internalTimeframe;
// Calculate min/max from the timeseries using the selected timeframe
const { min, max, hasData } = useMemo(() => {
// Use short series for timeframes <= 5 seconds, otherwise use long series
// Short series typically has ~5 second retention with high resolution
// Long series typically has ~1 hour retention with lower resolution
const series =
selectedTimeframe <= 5000 ? timeseries.short : timeseries.long;
// Manually calculate min/max to exclude zero values (which might be invalid readings)
const cutoffTime = series.lastTimestamp - selectedTimeframe;
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
let hasValidData = false;
let dataCount = 0;
const { values: raw, index, size, validCount } = series;
const startIdx = validCount < size ? 0 : index;
for (let i = 0; i < validCount; i++) {
const idx = (startIdx + i) % size;
const val = raw[idx];
if (
val &&
val.timestamp > 0 &&
val.timestamp >= cutoffTime &&
val.value > 0
) {
hasValidData = true;
dataCount++;
if (val.value < min) min = val.value;
if (val.value > max) max = val.value;
}
}
if (!hasValidData) {
return { min: 0, max: 0, hasData: false };
}
return { min, max, hasData: true };
}, [
timeseries.short,
timeseries.long,
timeseries.current,
selectedTimeframe,
]);
return (
<div className="flex w-full flex-col gap-3">
<Label label={label}>
<div className="flex flex-row items-center gap-6">
{/* Min Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-blue-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Min</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(min, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
{/* Max Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-red-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Max</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(max, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
</div>
</Label>
{/* Timeframe Selector (hidden when sharing selector externally) */}
{!hideSelector && (
<div className="flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => {
if (onTimeframeChange) onTimeframeChange(option.value);
else setInternalTimeframe(option.value);
}}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
selectedTimeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
)}
</div>
);
}
+56
View File
@@ -25,6 +25,8 @@ export function getUnitIcon(unit: Unit): IconName {
return "lu:Gauge";
case "mHz":
return "lu:AudioWaveform";
case "Hz":
return "lu:AudioWaveform";
case "W":
return "lu:Zap";
case "V":
@@ -33,6 +35,20 @@ export function getUnitIcon(unit: Unit): IconName {
return "lu:Zap";
case "kWh":
return "lu:BatteryFull";
case "l/min":
return "lu:Waves";
case "%":
return "lu:ChartNoAxesColumn";
case "µs":
return "lu:RefreshCcw";
case "s":
return "lu:Clock";
case "MiB":
return "lu:MemoryStick";
case "Mbit/s":
return "lu:Network";
case "/s":
return "lu:Gauge";
default:
return "lu:ChartNoAxesColumn";
}
@@ -60,6 +76,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
return "m/min";
case "mHz":
return "mHz";
case "Hz":
return "Hz";
case "W":
return "W";
case "V":
@@ -68,6 +86,20 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
return "A";
case "kWh":
return "kWh";
case "l/min":
return "l/min";
case "%":
return "%";
case "µs":
return "µs";
case "s":
return "s";
case "MiB":
return "MiB";
case "Mbit/s":
return "Mbit/s";
case "/s":
return "/s";
default:
return "";
}
@@ -117,6 +149,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
return "meters/minute";
case "mHz":
return "millihertz";
case "Hz":
return "hertz";
case "W":
return "watts";
case "V":
@@ -125,6 +159,20 @@ export function renderUnitSymbolLong(unit: Unit): string {
return "amperes";
case "kWh":
return "kilowatt-hours";
case "l/min":
return "liters/minute";
case "%":
return "percent";
case "µs":
return "microseconds";
case "s":
return "seconds";
case "MiB":
return "mebibytes";
case "Mbit/s":
return "megabits/second";
case "/s":
return "per second";
default:
return "";
}
@@ -141,10 +189,18 @@ export const units = [
"bar",
"m/min",
"mHz",
"Hz",
"W",
"V",
"A",
"kWh",
"l/min",
"%",
"µs",
"s",
"MiB",
"Mbit/s",
"/s",
] as const;
export type Unit = (typeof units)[number];
@@ -2,3 +2,4 @@ export const UPDATE_EXECUTE = "update:execute";
export const UPDATE_LOG = "update:log";
export const UPDATE_END = "update:end";
export const UPDATE_CANCEL = "update:cancel";
export const UPDATE_STEP = "update:step";
@@ -3,6 +3,7 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
type UpdateExecuteInvokeParams = {
@@ -16,19 +17,62 @@ type UpdateExecuteInvokeParams = {
};
};
type UpdateStepParams = {
stepId: string;
status: "pending" | "in-progress" | "completed" | "error";
progress?: number;
};
export function exposeUpdateContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
let currentLogListener: ((event: any, log: string) => void) | null = null;
let currentEndListener: ((event: any, params: any) => void) | null = null;
let currentStepListener:
| ((event: any, params: UpdateStepParams) => void)
| null = null;
contextBridge.exposeInMainWorld("update", {
execute: (params: UpdateExecuteInvokeParams) =>
ipcRenderer.invoke(UPDATE_EXECUTE, params),
cancel: () => ipcRenderer.invoke(UPDATE_CANCEL),
onLog: (callback: (log: string) => void) =>
ipcRenderer.on(UPDATE_LOG, (_event, log: string) => {
onLog: (callback: (log: string) => void) => {
if (currentLogListener) {
ipcRenderer.removeListener(UPDATE_LOG, currentLogListener);
}
currentLogListener = (_event, log: string) => {
callback(log);
}),
onEnd: (callback: (params: { success: boolean; error?: string }) => void) =>
ipcRenderer.on(UPDATE_END, (_event, params) => {
};
ipcRenderer.on(UPDATE_LOG, currentLogListener);
},
onEnd: (
callback: (params: { success: boolean; error?: string }) => void,
) => {
if (currentEndListener) {
ipcRenderer.removeListener(UPDATE_END, currentEndListener);
}
currentEndListener = (_event, params) => {
callback(params);
}),
};
ipcRenderer.on(UPDATE_END, currentEndListener);
},
onStep: (callback: (params: UpdateStepParams) => void) => {
if (currentStepListener) {
ipcRenderer.removeListener(UPDATE_STEP, currentStepListener);
}
currentStepListener = (_event, params) => {
callback(params);
};
ipcRenderer.on(UPDATE_STEP, currentStepListener);
},
});
}
@@ -4,6 +4,7 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
import { spawn, ChildProcess } from "child_process";
import tkill from "@jub3i/tree-kill";
@@ -116,6 +117,13 @@ async function update(
commit,
});
// Reset Rust build progress tracking
rustBuildProgress = {
totalDerivations: 0,
builtDerivations: 0,
maxPercent: 0,
};
const qitechControlEnv = process.env.QITECH_CONTROL_ENV;
const homeDir =
qitechControlEnv === "control-os" ? "/home/qitech" : process.env.HOME;
@@ -129,6 +137,8 @@ async function update(
// 1. first make sure the clone path is empty by deleting it if it containsa .git folder
const repoDir = `${homeDir}/${githubRepoName}`;
// Clear repo (not tracked in progress UI)
const clearResult = await clearRepoDirectory(
`${homeDir}/${githubRepoName}`,
event,
@@ -139,6 +149,10 @@ async function update(
}
// 2. clone the repository
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
});
const cloneResult = await cloneRepository(
{
githubRepoOwner,
@@ -151,11 +165,19 @@ async function update(
event,
);
if (!cloneResult.success) {
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "error",
});
event.sender.send(UPDATE_LOG, cloneResult.error);
return;
}
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "completed",
});
// 3. make the nixos-install.sh script executable
// 3. make the nixos-install.sh script executable (not tracked in progress UI)
const chmodResult = await runCommand(
"chmod",
["+x", "nixos-install.sh"],
@@ -168,17 +190,35 @@ async function update(
}
// 4. run the nixos-install.sh script
const installResult = await runCommand(
// This script will handle rust-build, electron-build, and system-install
// Start with rust-build (cargo builds)
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
});
const installResult = await runCommandWithStepTracking(
"./nixos-install.sh",
[],
repoDir,
event,
);
if (!installResult.success) {
// Mark current and remaining steps as error
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "error",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "error",
});
event.sender.send(UPDATE_LOG, installResult.error);
return;
}
// Success - steps are already marked as completed by runCommandWithStepTracking
resolve();
} catch (error: any) {
reject(error);
@@ -244,7 +284,7 @@ async function cloneRepository(
: `https://github.com/${githubRepoOwner}/${githubRepoName}.git`;
// Determine clone arguments based on whether tag, branch, or commit is specified
const cloneArgs = ["clone", repoUrl];
const cloneArgs = ["clone", "--progress", repoUrl];
if (tag) {
// Clone a specific tag
@@ -333,6 +373,11 @@ async function runCommand(
const log = data.toString();
console.error(log);
event.sender.send(UPDATE_LOG, log);
// Git outputs progress to stderr
if (cmd === "git" && args.includes("--progress")) {
parseGitProgress(log, event);
}
});
// Handle process completion
@@ -386,6 +431,249 @@ async function runCommand(
}
}
// Parse Git clone progress
function parseGitProgress(
log: string,
event: Electron.IpcMainInvokeEvent,
): void {
// Git progress format: "Receiving objects: 45% (234/520)"
// or "Resolving deltas: 100% (150/150)"
const receivingMatch = log.match(/Receiving objects:\s*(\d+)%/);
const resolvingMatch = log.match(/Resolving deltas:\s*(\d+)%/);
if (receivingMatch) {
const percent = parseInt(receivingMatch[1], 10);
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
progress: Math.floor(percent * 0.8), // Receiving is 80% of clone
});
} else if (resolvingMatch) {
const percent = parseInt(resolvingMatch[1], 10);
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
progress: Math.floor(80 + percent * 0.2), // Resolving is last 20%
});
}
}
// Track Rust build progress
let rustBuildProgress = {
totalDerivations: 0,
builtDerivations: 0,
maxPercent: 0, // Track max to prevent backward movement
};
// Parse Rust build output for progress
function parseRustBuildOutput(
log: string,
event: Electron.IpcMainInvokeEvent,
): void {
// Track derivations to build
const derivationsMatch = log.match(/these (\d+) derivations? will be built/i);
if (derivationsMatch) {
rustBuildProgress.totalDerivations = parseInt(derivationsMatch[1], 10);
rustBuildProgress.builtDerivations = 0;
rustBuildProgress.maxPercent = 0;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: 0,
});
return;
}
// Track building packages
if (
log.includes("building '/nix/store/") ||
log.includes("building /nix/store/")
) {
rustBuildProgress.builtDerivations++;
// Check if this is the server-deps package (one of the last builds)
const isServerDeps = log.includes("-server-deps");
let percent = 15;
if (isServerDeps) {
// server-deps indicates we're at 85%
percent = 85;
} else if (rustBuildProgress.totalDerivations > 0) {
const derivationProgress =
rustBuildProgress.builtDerivations / rustBuildProgress.totalDerivations;
percent = 15 + Math.floor(derivationProgress * 70); // Map to 15-85%
}
// Only move forward
percent = Math.max(percent, rustBuildProgress.maxPercent);
rustBuildProgress.maxPercent = percent;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: percent,
});
}
// Track installing phase - go up to 90%
if (log.includes("installing") || log.includes("Installing")) {
const percent = Math.max(90, rustBuildProgress.maxPercent);
rustBuildProgress.maxPercent = percent;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: percent,
});
}
}
// Enhanced version of runCommand that tracks build steps based on log output
async function runCommandWithStepTracking(
cmd: string,
args: string[],
workingDir: string,
event: Electron.IpcMainInvokeEvent,
): Promise<{ success: boolean; error?: string }> {
try {
const completeCommand = `${cmd} ${args.join(" ")}`;
const workingDirText = terminalGray(workingDir);
event.sender.send(
UPDATE_LOG,
`🚀 ${workingDirText} ${terminalColor("blue", completeCommand)}`,
);
const childProcess = spawn(cmd, args, {
cwd: workingDir,
});
// Store reference to current process for cancellation
currentUpdateProcess = childProcess;
// Track which steps have been marked as in-progress
// Note: rust-build is already marked as in-progress before calling this function
let systemInstallStarted = false;
// Function to process log output and update steps
const processLogForSteps = (log: string) => {
const logLower = log.toLowerCase();
// Parse Rust build progress
parseRustBuildOutput(log, event);
// Check for system install indicators
// System install happens after Rust build completes (at ~90%)
// Look for bootloader/activation messages that indicate final system installation
if (
!systemInstallStarted &&
rustBuildProgress.maxPercent >= 90 &&
(logLower.includes("updating grub") ||
logLower.includes("installing bootloader") ||
logLower.includes("updating bootloader") ||
logLower.includes("activating the configuration") ||
logLower.includes("building the system configuration") ||
logLower.includes("these 0 derivations"))
) {
console.log("🟢 Detected system install start:", log.substring(0, 100));
// Mark rust as complete, start system install
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "completed",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "in-progress",
});
systemInstallStarted = true;
}
};
// Stream stdout logs back to renderer
childProcess.stdout.on("data", (data) => {
const log = data.toString();
console.log(log);
event.sender.send(UPDATE_LOG, log);
processLogForSteps(log);
});
// Stream stderr logs back to renderer
childProcess.stderr.on("data", (data) => {
const log = data.toString();
console.error(log);
event.sender.send(UPDATE_LOG, log);
processLogForSteps(log);
});
// Handle process completion
return new Promise((resolve, reject) => {
childProcess.on("close", (code, signal) => {
// Clear process reference when completed
if (currentUpdateProcess === childProcess) {
currentUpdateProcess = null;
}
if (signal === "SIGTERM" || signal === "SIGKILL") {
event.sender.send(UPDATE_LOG, terminalInfo("Command was cancelled"));
reject({
success: false,
error: "Command was cancelled",
});
} else if (code === 0) {
// Mark all remaining steps as completed on success
if (!systemInstallStarted) {
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "completed",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "completed",
});
} else {
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "completed",
});
}
event.sender.send(
UPDATE_LOG,
terminalSuccess("Command completed successfully"),
);
resolve({ success: true, error: undefined });
} else {
event.sender.send(
UPDATE_LOG,
terminalError(`Command failed with code ${code}`),
);
reject({
success: false,
error: terminalError(code?.toString() ?? "NO_CODE"),
});
}
});
childProcess.on("error", (err) => {
// Clear process reference on error
if (currentUpdateProcess === childProcess) {
currentUpdateProcess = null;
}
event.sender.send(
UPDATE_LOG,
terminalError(`Command error: ${err.message}`),
);
reject({
success: false,
error: terminalError(err.message),
});
});
});
} catch (error: any) {
event.sender.send(UPDATE_LOG, terminalError(`Error: ${error.toString()}`));
return { success: false, error: error.toString() };
}
}
function terminalColor(
color: "blue" | "green" | "red" | "cyan" | "gray",
text: string,
+43 -3
View File
@@ -1,16 +1,52 @@
import type { UpdateInfo } from "@/stores/updateStore";
import { useUpdateStore } from "@/stores/updateStore";
let currentLogListener: ((line: string) => void) | null = null;
let currentStepListener:
| ((params: {
stepId: string;
status: "pending" | "in-progress" | "completed" | "error";
progress?: number;
}) => void)
| null = null;
export async function updateExecute(
source: UpdateInfo,
onLog: (log: string) => void,
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
window.update.onLog(onLog);
// Remove previous listener if exists
if (currentLogListener) {
window.update.onLog(() => {}); // clear previous
currentLogListener = null;
}
if (currentStepListener) {
window.update.onStep(() => {}); // clear previous
currentStepListener = null;
}
currentLogListener = onLog;
window.update.onLog(currentLogListener);
// Set up step listener
currentStepListener = (params) => {
const { setStepStatus, setStepProgress } = useUpdateStore.getState();
setStepStatus(params.stepId, params.status);
if (params.progress !== undefined) {
setStepProgress(params.stepId, params.progress);
}
};
window.update.onStep(currentStepListener);
window.update.execute(source);
window.update.onEnd((params) => {
window.update.onLog(() => {});
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {});
window.update.onStep(() => {}); // remove step listener
currentLogListener = null;
currentStepListener = null;
resolve(params);
});
});
@@ -40,7 +76,11 @@ export async function updateCancel(): Promise<{
success: boolean;
error?: string;
}> {
return window.update.cancel();
const result = await window.update.cancel();
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {}); // remove listener
window.update.onStep(() => {}); // remove step listener
return result;
}
// Enhanced helper that automatically manages store state
@@ -0,0 +1,59 @@
import { MiniGraph } from "@/components/graph/MiniGraph";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import React from "react";
import { useAnalogInputTestMachine } from "./useAnalogInputTestMachine";
export function AnalogInputTestMachineControl(): React.JSX.Element {
const { seriesData, state, updateMeasurementRate } =
useAnalogInputTestMachine();
if (!seriesData) return <div>Initializing Sensor...</div>;
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Measurement Control">
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div>Measurement Rate (Hz)</div>
<input
type="number"
style={{
border: "2px solid transparent",
borderRadius: "4px",
padding: "3px",
}}
className={"bg-neutral-100 shadow"}
value={state.measurementRateHz}
onChange={(v) => {
const val = Number(v.currentTarget.value);
updateMeasurementRate(val);
}}
min={1}
></input>
</div>
</ControlCard>
<ControlCard title="Results">
<div className="flex flex-row">
<MiniGraph
newData={seriesData}
width={500}
renderValue={(v) => v.toFixed(2)}
></MiniGraph>
<div className="flex flex-col justify-center">
{seriesData.current?.value.toFixed(2)}mA
</div>
</div>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,21 @@
import { Topbar } from "@/components/Topbar";
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function AnalogInputTestMachine(): React.JSX.Element {
const { serial } = analogInputTestMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/analoginputtestmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,107 @@
import { useMachineMutate } from "@/client/useClient";
import {
createTimeSeries,
TimeSeries,
TimeSeriesValue,
TimeSeriesWithInsert,
} from "@/lib/timeseries";
import { useEffect, useMemo, useState } from "react";
import z from "zod";
import {
StateEvent,
useAnalogInputTestMachineNamespace,
} from "./useAnalogInputTestMachineNamespace";
import { toastError } from "@/components/Toast";
import { MachineIdentificationUnique } from "../types";
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
import { analogInputTestMachine } from "../properties";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
export function useAnalogInputTestMachine() {
const { serial: serialString } =
analogInputTestMachineSerialRoute.useParams();
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: analogInputTestMachine.machine_identification,
serial,
};
}, [serialString]);
const state = useAnalogInputTestMachineNamespace({
type: "machine",
machine_identification_unique: machineIdentification,
});
const mutateMachine = useMachineMutate(
z.object({
measurement_rate_hz: z.number(),
}),
);
const stateOptimistic = useStateOptimistic<StateEvent>();
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const updateMeasurementRate = (val: number) => {
updateStateOptimistically(
(current) => {
current.measurementRateHz = val;
},
() => {
mutateMachine.request({
machine_identification_unique: machineIdentification,
data: { measurement_rate_hz: val },
});
},
);
};
const timeSeries: TimeSeriesWithInsert = createTimeSeries(
20,
1000,
5000,
60 * 60 * 1000,
);
const [seriesData, setSeriesData] = useState<TimeSeries | null>(
timeSeries.initialTimeSeries,
);
useEffect(() => {
const measurement = state.currentMeasurement;
if (measurement && seriesData) {
const dataPoint: TimeSeriesValue = {
value: measurement[0] * 1000,
timestamp: Number(measurement[1]),
};
setSeriesData(timeSeries.insert(seriesData, dataPoint));
}
}, [state]);
return {
seriesData,
state,
updateMeasurementRate,
};
}
@@ -0,0 +1,67 @@
import {
createNamespaceHookImplementation,
EventHandler,
eventSchema,
} from "@/client/socketioStore";
import z from "zod";
import { create, StoreApi } from "zustand";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
measurementRateHz: z.number().optional(),
currentMeasurement: z.tuple([z.number(), z.string()]).optional(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type AnalogInputTestMachineNamespaceStore = z.infer<
typeof stateEventDataSchema
>;
const createMachineStore = (): StoreApi<AnalogInputTestMachineNamespaceStore> =>
create<AnalogInputTestMachineNamespaceStore>(() => ({
measurementRateHz: 1,
currentMeasurement: undefined,
}));
// ========== Message Handler ==========
function analogInputTestMachineMessageHandler(
store: StoreApi<AnalogInputTestMachineNamespaceStore>,
): EventHandler {
return (event) => {
const oldState = store.getState();
const newMeasurementDataHz = event.data["MeasurementRateHz"];
const newMeasurement: [number, string] = event.data["Measurement"];
switch (event.name) {
case "MeasurementRateHz":
if (
newMeasurementDataHz &&
newMeasurementDataHz !== oldState.measurementRateHz
)
store.setState({
...oldState,
measurementRateHz: newMeasurementDataHz,
});
break;
case "Measurement":
if (newMeasurement && newMeasurement !== oldState.currentMeasurement)
store.setState({
...oldState,
currentMeasurement: newMeasurement,
});
break;
}
};
}
// ========== Namespace Hook ==========
export const useAnalogInputTestMachineNamespace =
createNamespaceHookImplementation({
createStore: () => createMachineStore(),
createEventHandler: analogInputTestMachineMessageHandler,
});
@@ -0,0 +1,195 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { useAquapath1 } from "./useAquapath";
import { SelectionGroup } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
export function Aquapath1ControlPage() {
const {
state,
defaultState,
front_flow,
back_flow,
front_temperature,
back_temperature,
front_temp_reservoir,
back_temp_reservoir,
setAquapathMode,
setFrontTemperature,
setBackTemperature,
setFrontFlow,
setBackFlow,
} = useAquapath1();
const frontTargetTemperature =
state?.temperature_states?.front.target_temperature ?? 0;
const backTargetTemperature =
state?.temperature_states?.back.target_temperature ?? 0;
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Flow Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Flow 1"
unit="l/min"
timeseries={front_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Flow 2"
unit="l/min"
timeseries={back_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
</div>
<div className="mt-8">
<Label label="Pump 1">
<SelectionGroup<"On" | "Off">
value={frontTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setFrontFlow(true);
} else {
setFrontFlow(false);
}
}}
/>
</Label>
<Label label="Pump 2">
<SelectionGroup<"On" | "Off">
value={backTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setBackFlow(true);
} else {
setBackFlow(false);
}
}}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Temperature Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Reservoir 1"
unit="C"
timeseries={front_temperature}
renderValue={(value) => value.toFixed(1)}
/>
<Label label="Set Reservoir 1 Target Temperature">
<EditValue
title="Set Reservoir 1 Target Temperature"
value={frontTargetTemperature}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontTemperature(val);
}}
defaultValue={
defaultState?.temperature_states.front.target_temperature
}
/>
</Label>
</div>
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Reservoir 2"
unit="C"
timeseries={back_temperature}
renderValue={(value) => value.toFixed(1)}
/>
<Label label="Set Reservoir 2 Target Temperature">
<EditValue
title="Set Reservoir 2 Target Temperature"
value={backTargetTemperature}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackTemperature(val);
}}
defaultValue={
defaultState?.temperature_states.back.target_temperature
}
/>
</Label>
</div>
</div>
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Standby" | "Auto">
value={state?.mode_state.mode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Auto: {
children: "Auto",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setAquapathMode(value)}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
Loaded 100 of 348 files, more files were not shown because too many files have changed in this diff. Show more