Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
265100d699 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 | ||
|
|
fb85cc6db7 | ||
|
|
4e128ded36 | ||
|
|
f50388b141 | ||
|
|
4025a68fe1 | ||
|
|
7a05fa206f | ||
|
|
8594b06c3b | ||
|
|
17523574d8 | ||
|
|
7971797178 | ||
|
|
a8b0837e58 | ||
|
|
e419974844 | ||
|
|
773d0f6c15 | ||
|
|
afc4e0d8b4 | ||
|
|
5e86988ac4 | ||
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f |
No files matched your search
Generated
+424
-407
File diff suppressed because it is too large.
Load diff
@@ -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.
Generated
+36
-12
@@ -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:
|
||||

|
||||
|
||||

|
||||
|
||||
## Mechanical Diagrams
|
||||
|
||||

|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod diagnosis_history;
|
||||
@@ -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,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())
|
||||
});
|
||||
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
pub mod coe;
|
||||
pub mod debugging;
|
||||
pub mod devices;
|
||||
pub mod helpers;
|
||||
pub mod io;
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user