Compare commits

..
Author SHA1 Message Date
Milan von dem Bussche 265100d699 Update README.md 2025-12-03 16:20:19 +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
33 changed files with 1149 additions and 587 deletions

No files matched your search

Generated
+424 -407
View File
File diff suppressed because it is too large. Load diff
+52
View File
@@ -152,3 +152,55 @@ 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
# Hello EtherCAT - LED Control on the EL2004
1. Introduction
2. Requirements
3. Wiring / Hardware Setup
4. Software Overview
5. Step-by-Step Implementation
6. Final Code Listing
7. Expected Behavior
---
## 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 existing QiTech machine framework. It is designed as the simplest possible working example of hardware interaction: turning an LED on and off.
---
## 2. Requirements
### Hardware
- Beckhoff EL2004 EtherCAT Terminal (Digital Output, 4 channels)
- Beckhoff EK1100 EtherCAT Coupler
- Power supply for EK1100 and EL2004 module (AC/DC Adapter with 24V Output )
- Connecting wires
- A PC connected to the EtherCAT network
### Software
- Rust toolchain (for building backend machine code)
- Node.js and npm/yarn (for frontend control interface)
- The project repository cloned locally
- Access to EtherCAT drivers and any required HAL bindings (available in the QITech control repository)
---
## 3. Wiring / Hardware Setup
### Schematic
<img width="1123" height="859" alt="Screenshot 2025-12-03 at 16 12 26" src="https://github.com/user-attachments/assets/a04278d9-7f6a-442d-9ea7-7ace9ac44872" />
### Needed Hardware Overview
<img width="813" height="1079" alt="Screenshot 2025-12-03 at 16 12 42" src="https://github.com/user-attachments/assets/af34a581-a668-4c6b-9d7f-062355813b6a" />
Figure 1: Beckhoff EK1100 Bus Coupler — the device circles in red is a simple 24V Power Source
Binary file not shown.
+36 -12
View File
@@ -175,6 +175,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -638,6 +639,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -660,6 +662,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1145,6 +1148,7 @@
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.1.1",
"fs-extra": "^9.0.1",
@@ -4091,6 +4095,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
@@ -4172,6 +4177,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4405,8 +4411,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4563,6 +4568,7 @@
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -4579,6 +4585,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4588,6 +4595,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4670,6 +4678,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@@ -5024,6 +5033,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5471,6 +5481,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -6163,7 +6174,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -6436,8 +6448,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7437,6 +7448,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7497,6 +7509,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8803,6 +8816,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -8865,6 +8879,7 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -9539,6 +9554,7 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10104,7 +10120,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12049,6 +12064,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12162,7 +12178,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12177,7 +12192,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12306,6 +12320,7 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12314,6 +12329,7 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -12325,6 +12341,7 @@
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -12365,8 +12382,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -13117,6 +13133,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -13906,7 +13923,8 @@
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -14008,7 +14026,8 @@
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"peer": true
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@@ -14059,6 +14078,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14370,6 +14390,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14731,6 +14752,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14852,6 +14874,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15395,6 +15418,7 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -720,3 +720,7 @@ The following persons must not work on the machine:
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
## Mechanical Diagrams
![QiTech Pro Extruder Nozzle 2025](images/manuals/extruder/Nozzle.png)
+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>
);
}
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
@@ -28,6 +29,12 @@ export function Laser1ControlPage() {
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
// Detect if this is a 2-axis laser
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
// Shared timeframe state (default 5 minutes)
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
return (
<Page>
<ControlGrid columns={2}>
@@ -77,6 +84,57 @@ export function Laser1ControlPage() {
/>
</div>
)}
<div className="mt-4 border-t pt-4">
{isTwoAxis ? (
// For 2-axis lasers: show diameter and roundness min/max side by side
<div className="grid grid-cols-2 gap-4">
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
{roundness?.current && (
<MinMaxValue
label="Roundness Range"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
)}
</div>
) : (
// For single-axis lasers: show only diameter min/max with shared selector
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
)}
{/* Shared timeframe selector for diameter/roundness */}
<div className="mt-3 flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => setTimeframe(option.value)}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
timeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
</div>
</ControlCard>
<ControlCard title="Settings">
<Label label="Set Target Diameter">
+1 -1
View File
@@ -10,10 +10,10 @@ workspace = true
clap = { version = "4.5.40", features = ["derive"] }
ethercrab = "0.6.0"
interfaces = "0.0.9"
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
futures = "0.3.31"
anyhow = "1.0.100"
+3 -3
View File
@@ -7,7 +7,7 @@ use ethercat_hal::{
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
};
use ethercrab::MainDevice;
use futures::executor::block_on;
use smol;
use crate::{MAX_SUBDEVICES, PDI_LEN, print::print_markdown_table};
@@ -34,7 +34,7 @@ pub fn ls(
let driver = device_from_subdevice_identity(&identity);
let identification_adresses = get_identification_addresses(&identity, device.name());
let identification = match identification_adresses {
Ok(_) => block_on(machine_device_identification(&device, maindevice)),
Ok(_) => smol::block_on(machine_device_identification(&device, maindevice)),
Err(_) => Err(anyhow!("")),
};
@@ -122,7 +122,7 @@ pub fn ls(
print_markdown_table(&table, true);
// Put group into op state
match block_on(group.into_op(maindevice)) {
match smol::block_on(group.into_op(maindevice)) {
Ok(_) => println!("Successfully put group into OP state"),
Err(e) => println!("Failed to put group into OP state: {}", e),
}
+7 -6
View File
@@ -2,18 +2,18 @@ use std::sync::Arc;
use dump::dump_eeprom;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, std::ethercat_now};
use futures::executor::block_on;
use read::read_eeprom;
use restore::restore_eeoprom;
use smol;
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
const MAX_SUBDEVICES: usize = 16;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
const MAX_PDU_DATA: usize = PduStorage::element_size(256);
const MAX_PDU_DATA: usize = PduStorage::element_size(1024);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
const PDI_LEN: usize = 256;
const PDI_LEN: usize = 1024;
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
@@ -57,7 +57,7 @@ async fn main() {
.get_one::<usize>("SUBDEVICE")
.expect("subdevice index is required");
let file = sub_matches.get_one::<String>("file");
let result = block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
let result = smol::block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error reading EEPROM: {}", e);
}
@@ -69,7 +69,8 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result = block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
let result =
smol::block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error writing EEPROM: {}", e);
}
@@ -78,7 +79,7 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result = block_on(read_eeprom(file));
let result = smol::block_on(read_eeprom(file));
if let Err(e) = result {
eprintln!("Error parsing EEPROM: {}", e);
}
-1
View File
@@ -14,7 +14,6 @@ units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
smol = "2.0.2"
rand = "0.9.2"
futures = "0.3.31"
tracing = "0.1.41"
[dev-dependencies]
@@ -0,0 +1,229 @@
use ethercrab::{SubDevice, SubDeviceRef};
use std::fmt;
const DIAGNOSIS_HISTORY_INDEX: u16 = 0x10f3;
const NEWEST_MESSAGE: u8 = 02;
const NEW_MESSAGE_AVAILABLE: u8 = 04;
const DIAG_MESSAGE_LENGTH: usize = 26;
pub struct SubdeviceDiagnosisEntry {
pub diag_code: u32,
pub flags: u16,
pub text_id: u16,
pub timestamp: u64,
pub data_type_p1: u16,
pub p1: u8,
pub flags_p2: u16,
pub p2: [u8; 5],
}
impl fmt::Display for SubdeviceDiagnosisEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SubdeviceDiagnosisEntry {{ diag_code: {}, flags: {}, text_id: {}, timestamp: {}, data_type_p1: {}, p1: {}, flags_p2: {}, p2: {:?} }}",
self.diag_code,
self.flags,
self.text_id,
self.timestamp,
self.data_type_p1,
self.p1,
self.flags_p2,
self.p2
)
}
}
impl SubdeviceDiagnosisEntry {
pub fn to_pretty_string(&self) -> String {
// Decode message type
let msg_type = match self.flags {
0x0000 => "Info",
0x0001 => "Warning",
0x0002 => "Error",
_ => "Unknown",
};
// Decode data type of P1
let p1_type = match self.data_type_p1 {
0x0005 => "UINT8",
_ => "Unknown",
};
// Decode data type of P2
let p2_type = match self.flags_p2 {
0x0005 => "BYTE[5]",
_ => "Unknown",
};
// Format P2 bytes as hex list
let p2_hex: Vec<String> = self.p2.iter().map(|b| format!("0x{:02X}", b)).collect();
format!(
"Diagnosis Entry:\n\
├─ diag_code: 0x{:08X}\n\
├─ message type: {} (0x{:04X})\n\
├─ text_id: {}{}\n\
├─ timestamp: {} (64-bit)\n\
├─ P1 type: {} (0x{:04X})\n\
├─ P1 value: {}\n\
├─ P2 type: {} (0x{:04X})\n\
└─ P2 data: [{}]",
self.diag_code,
msg_type,
self.flags,
self.text_id,
if self.text_id == 0 { " (no text)" } else { "" },
self.timestamp,
// P1
p1_type,
self.data_type_p1,
self.p1,
// P2
p2_type,
self.flags_p2,
p2_hex.join(", ")
)
}
}
/*
Could be tested with this:
00 E8 00 A0 02 00 00 00 5F FB D5 A5 A6 0B 00 00 05 00 11 05 10 08 2A 00 2A 00
*/
pub fn convert_raw_diagnosis_bytes(
message: [u8; DIAG_MESSAGE_LENGTH],
) -> Result<SubdeviceDiagnosisEntry, anyhow::Error> {
let diag_code_bytes = [message[0], message[1], message[2], message[3]];
let diag_code = u32::from_le_bytes(diag_code_bytes);
let flags_code_bytes = [message[4], message[5]];
let flags = u16::from_le_bytes(flags_code_bytes);
let text_id_bytes = [message[6], message[7]];
let text_id = u16::from_le_bytes(text_id_bytes);
let timestamp_bytes = [
message[8],
message[9],
message[10],
message[11],
message[12],
message[13],
message[14],
message[15],
];
let timestamp = u64::from_le_bytes(timestamp_bytes);
let data_type_p1_bytes = [message[16], message[17]];
let data_type_p1 = u16::from_le_bytes(data_type_p1_bytes);
// TODO: Find the Constants
// 0x0005 Apparently Means p1 variable is u8
// For now we only handle u8
let p1 = match data_type_p1 {
0x0005 => message[18],
_ => 0,
};
if p1 == 0 {
return Err(anyhow::anyhow!(
"convert_raw_diagnosis_bytes: Datatype_p1 is unknown {:?}",
data_type_p1
));
}
// flags_p2 is packed, whatever thats supposed to mean
let flags_p2_bytes = [message[19], message[20]];
let flags_p2 = u16::from_le_bytes(flags_p2_bytes);
let p2 = [
message[21],
message[22],
message[23],
message[24],
message[25],
];
return Ok(SubdeviceDiagnosisEntry {
diag_code,
flags,
text_id,
timestamp,
data_type_p1,
p1,
flags_p2,
p2,
});
}
/// Expects the SubdeviceRef to be the Coupler or any other device with a diag history index
pub async fn get_most_recent_diagnosis_message(
device: &SubDeviceRef<'_, &SubDevice>,
) -> Option<String> {
let new_message_available = device
.sdo_read::<bool>(DIAGNOSIS_HISTORY_INDEX, NEW_MESSAGE_AVAILABLE)
.await;
let new_message_available = match new_message_available {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read new_message_available {:?}",
e
);
return None;
}
};
if !new_message_available {
tracing::info!("get_most_recent_diagnosis_message: No Diagnosis Messages found");
// No messages available
return None;
}
let newest_message_index = device
.sdo_read::<u8>(DIAGNOSIS_HISTORY_INDEX, NEWEST_MESSAGE)
.await;
let newest_message_index = match newest_message_index {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read newest_message_index {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: Reading newest message at index: {}",
newest_message_index
);
let res = device
.sdo_read::<[u8; DIAG_MESSAGE_LENGTH]>(DIAGNOSIS_HISTORY_INDEX, newest_message_index)
.await;
let message: [u8; DIAG_MESSAGE_LENGTH] = match res {
Ok(res) => res,
Err(_) => {
tracing::error!("get_most_recent_diagnosis_message: Failed to read Diagnosis Message");
return None;
}
};
let message = convert_raw_diagnosis_bytes(message);
let message = match message {
Ok(msg) => msg,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to convert Diagnosis Message bytes to struct {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: {}",
message.to_pretty_string()
);
return Some(message.to_pretty_string());
}
+1
View File
@@ -0,0 +1 @@
pub mod diagnosis_history;
+1 -2
View File
@@ -1,7 +1,6 @@
use std::fmt;
use std::sync::Arc;
use futures::executor::block_on;
use physical::{AnalogInputRange, AnalogInputValue};
use smol::lock::RwLock;
@@ -40,7 +39,7 @@ impl AnalogInput {
let get_input = Box::new(move || -> AnalogInputInput {
let device2 = Arc::clone(&device);
let port_clone = port.clone();
block_on(async move {
smol::block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
+1 -2
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Input (DI) device
@@ -27,7 +26,7 @@ impl DigitalInput {
let port2 = port;
let device2 = device.clone();
let get_input = Box::new(move || {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_input(port2.clone())
});
+2 -3
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Output (DO) device
@@ -29,7 +28,7 @@ impl DigitalOutput {
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: DigitalOutputOutput| {
let mut device = block_on(device1.write());
let mut device = smol::block_on(device1.write());
device.set_output(port1.clone(), value);
});
@@ -37,7 +36,7 @@ impl DigitalOutput {
let port2 = port;
let device2 = device.clone();
let get_output = Box::new(move || -> DigitalOutputOutput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_output(port2.clone())
});
+4 -5
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Encoder Input device
@@ -34,7 +33,7 @@ impl EncoderInput {
let port_counter = port.clone();
let device_counter = device.clone();
let get_counter = Box::new(move || {
let device = block_on(device_counter.read());
let device = smol::block_on(device_counter.read());
device.get_counter_value(port_counter.clone())
});
@@ -42,7 +41,7 @@ impl EncoderInput {
let port_frequency = port.clone();
let device_frequency = device.clone();
let get_frequency = Box::new(move || {
let device = block_on(device_frequency.read());
let device = smol::block_on(device_frequency.read());
device.get_frequency(port_frequency.clone())
});
@@ -50,7 +49,7 @@ impl EncoderInput {
let port_period = port.clone();
let device_period = device.clone();
let get_period = Box::new(move || {
let device = block_on(device_period.read());
let device = smol::block_on(device_period.read());
device.get_period(port_period.clone())
});
@@ -58,7 +57,7 @@ impl EncoderInput {
let port_set = port;
let device_set = device;
let set_counter = Box::new(move |value: u32| {
let mut device = block_on(device_set.write());
let mut device = smol::block_on(device_set.write());
device.set_counter(port_set.clone(), value)
});
+3 -4
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -29,21 +28,21 @@ impl<'device> PulseTrainOutput {
// build sync write closure
let device1 = device.clone();
let set_output = Box::new(move |value: PulseTrainOutputOutput| {
let mut device = block_on(device1.write());
let mut device = smol::block_on(device1.write());
device.set_output(port, value);
});
// build sync get closure
let device2 = device.clone();
let get_output = Box::new(move || -> PulseTrainOutputOutput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_output(port)
});
// build sync get closure
let device2 = device;
let get_input = Box::new(move || -> PulseTrainOutputInput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_input(port)
});
@@ -2,7 +2,6 @@ use std::{fmt, sync::Arc};
use crate::helpers::el70xx_velocity_converter::EL70x1VelocityConverter;
use anyhow::Error;
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -47,7 +46,7 @@ impl<'device> StepperVelocityEL70x1 {
// build sync get closure
let device2 = device.clone();
let get_input = Box::new(move || -> Result<StepperVelocityEL70x1Input, Error> {
block_on(async {
smol::block_on(async {
let device = device2.read().await;
device.get_input(port)
})
@@ -55,7 +54,7 @@ impl<'device> StepperVelocityEL70x1 {
let device3 = device.clone();
let get_output = Box::new(move || -> Result<StepperVelocityEL70x1Output, Error> {
block_on(async {
smol::block_on(async {
let device = device3.read().await;
device.get_output(port)
})
@@ -65,7 +64,7 @@ impl<'device> StepperVelocityEL70x1 {
let device3 = device;
let get_speed_range = Box::new(
move || -> Result<crate::shared_config::el70x1::EL70x1SpeedRange, Error> {
block_on(async {
smol::block_on(async {
let device = device3.read().await;
Ok(device.get_speed_range(port))
})
+3 -5
View File
@@ -1,4 +1,3 @@
use futures::executor::block_on;
use smol::lock::RwLock;
use crate::pdo::basic::Limit;
@@ -29,10 +28,9 @@ impl TemperatureInput {
let get_input = Box::new(move || {
let device2 = device2.clone();
let port_clone = port2.clone();
block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
let device = device2.read_blocking();
device.get_input(port_clone)
});
Self { get_input }
}
+1
View File
@@ -1,4 +1,5 @@
pub mod coe;
pub mod debugging;
pub mod devices;
pub mod helpers;
pub mod io;
-1
View File
@@ -19,7 +19,6 @@ euclid = "0.22.11"
# concurrency
smol = "2.0.2"
futures = "0.3.31"
# web
serde_json = "1.0.143"
@@ -6,7 +6,6 @@ use control_core::{
transmission::{Transmission, fixed::FixedTransmission},
};
use ethercat_hal::io::analog_input::AnalogInput;
use futures::executor::block_on;
use units::angular_velocity::revolution_per_minute;
use units::electric_current::milliampere;
use units::f64::*;
@@ -206,7 +205,7 @@ impl ScrewSpeedController {
pub fn update(&mut self, now: Instant, is_extruding: bool) {
// TODO: move this logic elsewhere or make non async
block_on(self.inverter.act(now));
smol::block_on(self.inverter.act(now));
let wiring_error = self.get_wiring_error();
if wiring_error {
+13 -22
View File
@@ -1,6 +1,5 @@
use serialport::UsbPortInfo;
use smol::Task;
use std::{collections::HashMap, time::Duration};
use std::collections::HashMap;
/*
*@author: Alisher Darmenov
*@company: QiTech
@@ -75,38 +74,30 @@ impl SerialDetection {
}
}
// Async function that runs until at least one serial device is found.
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
smol::Timer::after(Duration::from_secs(1)).await;
/*
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
return Some(devices);
} else {
return None;
pub async fn start_serial_discovery(app_state: Arc<>) {
loop {
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
handle_serial_device_hotplug(app_state.clone(), devices).await;
}
smol::Timer::after(Duration::from_secs(1)).await;
}
}
// Returns a smol::Task that resolves when atleast one device is found.
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
smol::spawn(find_serial())
}
/*
pub async fn handle_serial_device_hotplug(
app_state: Arc<SharedState>,
map: Option<HashMap<String, UsbPortInfo>>,
map: HashMap<String, UsbPortInfo>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let laser = SerialDetection::get_path_by_id(laser_ident, map);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
+1 -6
View File
@@ -23,7 +23,7 @@ bitvec = "1.0.1"
# concurrency
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
futures = "0.3.31"
regex = "1.11.3"
# web
serde_json = "1.0.143"
@@ -37,14 +37,12 @@ serialport = "4.7.3"
#tracing
tracing = { version = "0.1.41", features = ["attributes"] }
tracing-journald = { version = "0.3.1", optional = true }
tracing-futures = "0.2.5"
tracing-subscriber = { version = "0.3.20", features = [
"env-filter",
"chrono",
"registry",
] }
utils = { version = "0.1.0", path="../utils" }
tonic = { version = "0.13.1", optional = true }
spin_sleep = "1.3.3"
dhat = { version = "0.3.3", optional = true }
ctrlc = { version = "3.3", optional = true }
@@ -71,6 +69,3 @@ heap-profile = ["dhat"]
[profile.release]
codegen-units = 1
lto = "fat"
[package.metadata.cargo-machete]
ignored = ["tonic"]
+2 -2
View File
@@ -3,8 +3,8 @@ use ethercrab::PduStorage;
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
pub const MAX_SUBDEVICES: usize = 16;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
pub const MAX_PDU_DATA: usize = PduStorage::element_size(256);
pub const MAX_PDU_DATA: usize = PduStorage::element_size(512);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
pub const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
pub const PDI_LEN: usize = 256;
pub const PDI_LEN: usize = 512;
@@ -17,6 +17,7 @@ pub async fn send_ethercat_discovering(app_state: Arc<SharedState>) {
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
@@ -31,6 +32,7 @@ pub async fn send_ethercat_found(app_state: Arc<SharedState>, interface: &str) {
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
+26 -7
View File
@@ -1,13 +1,18 @@
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::{Task, Timer};
use std::time::Duration;
use smol::Timer;
use std::{sync::Arc, time::Duration};
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
use crate::{
app_state::SharedState,
ethercat::{ethercat_discovery_info::send_ethercat_found, setup::setup_loop},
};
pub async fn find_ethercat_interface() -> String {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found EtherCAT Interface at: {}", interface);
return Ok(interface);
return interface;
}
Err(e) => {
tracing::warn!("No working interface found: {}. Retrying...", e);
@@ -17,7 +22,21 @@ pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
}
}
// Returns a Future to the interface
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
smol::spawn(find_ethercat_interface())
pub async fn start_interface_discovery(app_state: Arc<SharedState>) {
let interface = find_ethercat_interface().await;
tracing::info!("Calling setup_loop");
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::main] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
send_ethercat_found(app_state, &interface).await;
}
+13 -14
View File
@@ -7,13 +7,14 @@ use crate::{
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
use control_core::irq_handling::set_irq_affinity;
use control_core::{irq_handling::set_irq_affinity, realtime::set_realtime_priority};
use ethercat_hal::debugging::diagnosis_history::get_most_recent_diagnosis_message;
use machines::machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
DeviceIdentificationIdentified, MachineIdentificationUnique, read_device_identifications,
};
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use control_core::realtime::set_core_affinity;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
@@ -109,9 +110,11 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
shared_state: Arc<SharedState>,
socket_queue_tx: Sender<(SocketRef, Arc<control_core::socketio::event::GenericEvent>)>,
) -> Result<(), anyhow::Error> {
tracing::info!("set_ethercat_devices");
let device_grouping_result = group_devices_by_identification(device_identifications);
tracing::info!("{:?}", device_grouping_result.unidentified_devices);
tracing::info!(
"set_ethercat_devices: {:?}",
device_grouping_result.unidentified_devices
);
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
let mut machines: Vec<Box<dyn Machine>> = vec![];
@@ -194,10 +197,9 @@ pub async fn setup_loop(
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
{
use ethercrab::std::tx_rx_task;
use futures::executor::block_on;
let rt = smol::LocalExecutor::new();
let _ = block_on(rt.run(async {
let _ = smol::block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
@@ -245,6 +247,7 @@ pub async fn setup_loop(
.write()
.await
.main_namespace;
let event = EthercatDevicesEventBuilder().initializing();
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
}
@@ -334,6 +337,10 @@ pub async fn setup_loop(
tracing::info!("Found Devices: {:?}", ethercat_meta_devices);
drop(ethercat_meta_devices);
// We always need to have atleast one subdevice anyways
let coupler = subdevices.get(0).unwrap();
let _resp = get_most_recent_diagnosis_message(coupler).await;
for subdevice in subdevices.iter() {
if subdevice.name() == "EL5152" {
subdevice.sdo_write(SM_OUTPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
@@ -359,14 +366,6 @@ pub async fn setup_loop(
app_state.clone().socketio_setup.socket_queue_tx.clone(),
)
.await?;
/*let main_namespace = &mut app_state_clone
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_clone.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));*/
// Put group in operational state
let group_op = match group_preop.into_op(&maindevice).await {
+74 -67
View File
@@ -1,4 +1,3 @@
use futures::{FutureExt, select};
use machines::{
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
@@ -8,19 +7,15 @@ use machines::{
DeviceIdentification, DeviceIdentificationIdentified, MachineIdentificationUnique,
},
registry::{MACHINE_REGISTRY, MachineRegistry},
serial::{
devices::laser::Laser,
init::{SerialDetection, start_serial_discovery},
},
serial::{devices::laser::Laser, init::SerialDetection},
winder2::api::GenericEvent,
};
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
use app_state::{HotThreadMessage, SharedState};
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
setup::setup_loop,
};
use ethercat::ethercat_discovery_info::send_ethercat_discovering;
use r#loop::start_loop_thread;
use panic::init_panic_handling;
@@ -28,15 +23,24 @@ use rest::init::start_api_thread;
use serialport::UsbPortInfo;
use smol::{
channel::{Receiver, Sender},
future,
lock::RwLock,
};
use socketio::{main_namespace::machines_event::MachineObj, queue::start_socketio_queue};
use socketio::main_namespace::machines_event::MachineObj;
use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
#[cfg(feature = "mock-machine")]
use mock_init::init_mock;
use crate::{
ethercat::{
ethercat_discovery_info::send_ethercat_found, init::find_ethercat_interface,
setup::setup_loop,
},
socketio::queue::socketio_queue_worker,
};
#[cfg(feature = "mock-machine")]
pub mod mock_init;
@@ -110,17 +114,31 @@ pub async fn add_serial_device(
shared_state.clone().send_machines_event().await;
}
async fn setup_ethercat(
interface: &str,
pub async fn start_serial_discovery(app_state: Arc<SharedState>) {
loop {
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
handle_serial_device_hotplug(app_state.clone(), devices).await;
}
smol::Timer::after(Duration::from_secs(1)).await;
}
}
pub async fn start_interface_discovery(
app_state: Arc<SharedState>,
sender: Sender<HotThreadMessage>,
) {
tracing::info!("Calling setup_loop");
let interface = find_ethercat_interface().await;
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(setup) => {
let _ = sender.send(HotThreadMessage::AddEtherCatSetup(setup)).await;
tracing::info!("Successfully initialized EtherCAT network");
tracing::info!("Successfully initialized EtherCAT devices");
}
Err(e) => {
@@ -131,23 +149,20 @@ async fn setup_ethercat(
);
}
}
send_ethercat_found(app_state.clone(), &interface).await;
}
pub async fn handle_serial_device_hotplug(
app_state: Arc<SharedState>,
map: Option<HashMap<String, UsbPortInfo>>,
map: HashMap<String, UsbPortInfo>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let laser = SerialDetection::get_path_by_id(laser_ident, map);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
{
@@ -267,10 +282,18 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
}
}
}
tracing::warn!("Async handler task finished");
}
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}
#[cfg(feature = "development-build")]
fn setup_ctrlc_handler() -> Arc<AtomicBool> {
@@ -306,18 +329,20 @@ fn main() {
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();
let (main_sender, main_receiver) = smol::channel::unbounded();
let shared_state = SharedState::new(sender.clone(), main_sender.clone());
let shared_state = SharedState::new(sender.clone(), main_sender);
let app_state = Arc::new(shared_state);
let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME);
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut socketio_task = smol::spawn(start_socketio_queue(app_state.clone()));
let mut serial_task = smol::spawn(start_serial_discovery(app_state.clone()));
let mut async_machine_task = smol::spawn(handle_async_requests(
main_receiver.clone(),
app_state.clone(),
));
#[cfg(not(feature = "mock-machine"))]
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
let mut handle_async_machine_requests =
smol::spawn(handle_async_requests(main_receiver, app_state.clone())).fuse();
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
smol::block_on(async {
send_empty_machines_event(app_state.clone()).await;
@@ -334,47 +359,29 @@ fn main() {
tracing::info!("Shutdown signal received, exiting main loop.");
break;
}
// lets the async runtime decide which future to run next
#[cfg(not(feature = "mock-machine"))]
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
match res {
Ok(interface) =>
{
setup_ethercat(&interface,app_state.clone(),sender.clone()).await;
},
Err(_) => (),
};
},
res = serial_fut => {
let _ = handle_serial_device_hotplug(app_state.clone(),res).await;
serial_fut = start_serial_discovery().fuse();
},
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
if serial_task.is_finished() {
tracing::warn!("Serial task died! Restarting...");
serial_task.cancel().await;
serial_task = smol::spawn(start_serial_discovery(app_state.clone()));
}
#[cfg(feature = "mock-machine")]
select! {
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
_ = smol::Timer::after(Duration::from_millis(1)).fuse() => {
}
if socketio_task.is_finished() {
tracing::warn!("SocketIO task died! Restarting...");
socketio_task.cancel().await;
socketio_task = smol::spawn(start_socketio_queue(app_state.clone()));
}
if async_machine_task.is_finished() {
tracing::warn!("Async handler task died! Restarting...");
async_machine_task.cancel().await;
async_machine_task = smol::spawn(handle_async_requests(
main_receiver.clone(),
app_state.clone(),
));
}
future::yield_now().await;
}
});
}
+2 -4
View File
@@ -1,13 +1,11 @@
use super::namespace_id::NamespaceId;
use crate::app_state::SharedState;
use socketioxide::ParserConfig;
use socketioxide::extract::SocketRef;
use socketioxide::layer::SocketIoLayer;
use std::str::FromStr;
use std::sync::Arc;
use crate::app_state::SharedState;
use super::namespace_id::NamespaceId;
pub async fn init_socketio(app_state: Arc<SharedState>) -> SocketIoLayer {
// create
let (socketio_layer, io) = socketioxide::SocketIoBuilder::new()
+9 -7
View File
@@ -1,9 +1,7 @@
use smol::Task;
use crate::app_state::SharedState;
use std::{sync::Arc, time::Instant};
use tracing::{debug, info, instrument, trace};
use crate::app_state::SharedState;
/// Send a single event with retry logic
#[instrument(skip_all)]
async fn send_event_with_retry(
@@ -60,7 +58,7 @@ async fn send_event_with_retry(
}
}
async fn socketio_queue_worker(app_state: Arc<SharedState>) {
pub async fn socketio_queue_worker(app_state: &SharedState) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
@@ -89,7 +87,11 @@ async fn socketio_queue_worker(app_state: Arc<SharedState>) {
info!("SocketIO global queue listener stopped");
}
/// Returns a spawned async task handling the Socket.IO event queue
pub fn start_socketio_queue(app_state: Arc<SharedState>) -> Task<()> {
smol::spawn(socketio_queue_worker(app_state))
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}