Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f4571e798 | ||
|
|
53efecc636 | ||
|
|
c2371d095e | ||
|
|
cacde7868c | ||
|
|
7cf06e6a9d | ||
|
|
83688daaf4 | ||
|
|
f55a6c6569 | ||
|
|
e42cc64f5d | ||
|
|
5c0a9f3bb6 | ||
|
|
b719731926 | ||
|
|
5b4d81fec2 | ||
|
|
4f50395eb9 | ||
|
|
30a7134db9 | ||
|
|
474d64c2a2 | ||
|
|
0293654803 | ||
|
|
80ca509998 | ||
|
|
a84157bd94 | ||
|
|
0007f14296 | ||
|
|
307ad265a9 | ||
|
|
41f36fdf2e | ||
|
|
86c28fef8a | ||
|
|
c01c09548c | ||
|
|
8f76bb88e7 | ||
|
|
ea24c2f85c | ||
|
|
806603c583 | ||
|
|
4b9a788be4 | ||
|
|
2d91d34e9a | ||
|
|
7e50cce009 | ||
|
|
2f642a11d0 | ||
|
|
6cb027b12e | ||
|
|
58903a885d | ||
|
|
eda4b7a9b5 | ||
|
|
2f48b4093f | ||
|
|
676f19ea0d | ||
|
|
0252cd5ef3 | ||
|
|
ac248e31a1 | ||
|
|
58e43f4fc8 | ||
|
|
3845926b97 | ||
|
|
78c99580bf | ||
|
|
3526711f4d | ||
|
|
5ab97839c6 | ||
|
|
c92455226f | ||
|
|
3b91162fd3 | ||
|
|
c3da8bb357 | ||
|
|
70dd9d2072 | ||
|
|
2a87e17909 | ||
|
|
ac95e23e18 | ||
|
|
5e606e485f | ||
|
|
a35f65b8d0 | ||
|
|
587f7f6c88 | ||
|
|
eedf71b933 | ||
|
|
671c36e9c1 | ||
|
|
f59b1f4096 | ||
|
|
138c5614d1 | ||
|
|
b0e1a09d61 | ||
|
|
29a8a5281b | ||
|
|
92cb25749a | ||
|
|
0b26dfe9de | ||
|
|
a1117ad09d | ||
|
|
d39bd0b7fa | ||
|
|
51db45c341 | ||
|
|
c74fabb1f0 | ||
|
|
afc9b2a135 | ||
|
|
e3cfb6d0a0 | ||
|
|
30a521f27d | ||
|
|
bba2759c1a | ||
|
|
4c1f870eaa | ||
|
|
5d317a9841 | ||
|
|
4c750ec3be | ||
|
|
da531208e5 | ||
|
|
554560681b | ||
|
|
05198ee7e9 | ||
|
|
a3b9f1b505 | ||
|
|
08578bf856 | ||
|
|
ea65f3cd2c | ||
|
|
4df8815bfa | ||
|
|
5f645c3d3a | ||
|
|
c774ace112 | ||
|
|
64a6ab0410 | ||
|
|
def9c22b64 | ||
|
|
6d6f90bf9f | ||
|
|
12dc482e91 | ||
|
|
b52c047cdf | ||
|
|
fa75e91ce6 | ||
|
|
c52d4d464d | ||
|
|
9719b5251c | ||
|
|
ea796bbea6 | ||
|
|
594a8730a4 | ||
|
|
03ae4aeefd | ||
|
|
57f6e50e0e | ||
|
|
fcebb2d0f2 | ||
|
|
56ebc80aab | ||
|
|
6d8aba1577 | ||
|
|
ebaf033b36 | ||
|
|
71f314cf75 | ||
|
|
f520c759ce | ||
|
|
b8d0b1a630 | ||
|
|
11deedb75d | ||
|
|
50a61f9e21 | ||
|
|
2df5a06930 | ||
|
|
0ae30ba8fb | ||
|
|
2b80d6c806 | ||
|
|
fd2ff77b62 | ||
|
|
46e699b745 | ||
|
|
5e0dbf9d7e | ||
|
|
eefc9b3834 | ||
|
|
bbc9783961 | ||
|
|
3c998d2f5c | ||
|
|
52cd1ce050 | ||
|
|
5140410006 | ||
|
|
852beb5a06 | ||
|
|
2cf150a541 | ||
|
|
4f2f1b227c | ||
|
|
7c1f77ff7b | ||
|
|
8b2b769435 | ||
|
|
5350ca8f53 | ||
|
|
14f4ac1248 | ||
|
|
548ecf5723 | ||
|
|
99e74759b5 | ||
|
|
ec177906c8 | ||
|
|
afdf38eaf2 | ||
|
|
d8d509812b | ||
|
|
3eeeadab5c | ||
|
|
657c3cff85 | ||
|
|
7ea7721ea3 | ||
|
|
b5ccf10128 | ||
|
|
a5a0b1a5e5 | ||
|
|
1e8ef812e9 | ||
|
|
4965e73b0c | ||
|
|
6db2b91052 | ||
|
|
a930d058c2 | ||
|
|
5b7acbc7f3 | ||
|
|
344d319d12 | ||
|
|
d5534e0967 | ||
|
|
680cb71dde | ||
|
|
a26ccb0a1f | ||
|
|
9fa037a9f9 | ||
|
|
f07dcbf8f8 | ||
|
|
783ee38a3a | ||
|
|
4a7be41dfe | ||
|
|
8b6960ae64 | ||
|
|
cf0b71bb69 | ||
|
|
e5608d061f | ||
|
|
7eba9523d4 | ||
|
|
5ad85b1a8c | ||
|
|
064a904975 | ||
|
|
ccbab4afba | ||
|
|
79870d995a | ||
|
|
0d95f1c44f | ||
|
|
62aabaa36a | ||
|
|
b57b746379 | ||
|
|
d1dbe7550d | ||
|
|
38d2364ac4 | ||
|
|
581b4f7773 | ||
|
|
0550ecec7b | ||
|
|
3071482a87 | ||
|
|
0c926a5acd | ||
|
|
59387eb214 | ||
|
|
9693af313f | ||
|
|
df1beabfa8 | ||
|
|
9630142921 | ||
|
|
fcef922ab1 | ||
|
|
edd8c83402 | ||
|
|
09d9801482 | ||
|
|
f50997ec23 | ||
|
|
5524e9f10a | ||
|
|
7e73e2f879 | ||
|
|
96719dae15 | ||
|
|
e78e9ba9e3 | ||
|
|
a730ed3228 | ||
|
|
c3b0a8643d | ||
|
|
422d98bc7c | ||
|
|
f1b61dad9b | ||
|
|
c97768975b | ||
|
|
0b882e8bde | ||
|
|
6752b503bc | ||
|
|
847963fb1c | ||
|
|
4441b60d4a | ||
|
|
cb74f4fb6a | ||
|
|
fae9c7151c | ||
|
|
7aa36c0361 | ||
|
|
eb4b4e948d | ||
|
|
13af890e29 | ||
|
|
d366f52127 | ||
|
|
8f8535b6a3 | ||
|
|
83fa27c7be | ||
|
|
8295496671 | ||
|
|
9277521dbd | ||
|
|
3797fc6469 | ||
|
|
9700e47358 | ||
|
|
8ee1c63e8d | ||
|
|
dae3def188 | ||
|
|
fa3dc0aa8c | ||
|
|
69fcdb969d | ||
|
|
9b03b4d909 | ||
|
|
5e9fb93a21 | ||
|
|
b6842a71fb | ||
|
|
f89645a516 | ||
|
|
d928a4ba81 | ||
|
|
c457b21e64 | ||
|
|
c63be3688b | ||
|
|
21c40ac7ad | ||
|
|
0938f68d8d | ||
|
|
87e903781b | ||
|
|
6753f92128 | ||
|
|
59353ff06f | ||
|
|
938bbc97a6 | ||
|
|
bb02eeb2b0 | ||
|
|
7bc91d73a6 | ||
|
|
59783bc2c3 | ||
|
|
d93ff842ab | ||
|
|
78417edcb8 | ||
|
|
f0f1c2c69e | ||
|
|
b4043d1a75 | ||
|
|
3614f812b9 | ||
|
|
26d3af414f | ||
|
|
ec8eb6a835 | ||
|
|
69f0f73087 | ||
|
|
0b3e4c8c96 | ||
|
|
b8777ee4da | ||
|
|
0bc415271e | ||
|
|
efa32e91ee | ||
|
|
f0927513cd | ||
|
|
3599c26c54 | ||
|
|
aa97091b98 | ||
|
|
ea62de3fdc | ||
|
|
b42089e992 | ||
|
|
ebffbb69ec | ||
|
|
c48b5b2292 | ||
|
|
8abd6558d1 | ||
|
|
dc0b4fada3 | ||
|
|
92cacbc6c8 | ||
|
|
4bad318ce7 | ||
|
|
0eeb7f372e | ||
|
|
1a72f3f8bb | ||
|
|
522ace8a4f | ||
|
|
7e776dc360 | ||
|
|
6487ddc6d2 | ||
|
|
4ebd09d2eb | ||
|
|
330b000b6e | ||
|
|
7a817945ee | ||
|
|
47b1b7fc82 | ||
|
|
7f2ea72ad3 | ||
|
|
62553d1a2e | ||
|
|
cff28876ee | ||
|
|
56032ed59c | ||
|
|
73245a31c6 | ||
|
|
f423ac2a14 | ||
|
|
82377c2c25 | ||
|
|
08bd2c8cee | ||
|
|
7205fdea22 | ||
|
|
c1c420ce8f | ||
|
|
576e80cc39 | ||
|
|
a51a871b70 | ||
|
|
f686420e1e | ||
|
|
2237a84d88 | ||
|
|
5c00d28396 | ||
|
|
2b0cff358e | ||
|
|
c3b7f9d050 | ||
|
|
a7fce331a0 | ||
|
|
6dd8573584 | ||
|
|
124418dc7a | ||
|
|
bf3ee15562 | ||
|
|
48fac7f86c |
No files matched your search
@@ -1,16 +1,19 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/electron" # Location of package manifests
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/electron"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
@@ -36,6 +36,9 @@ jobs:
|
||||
- name: Install
|
||||
run: npm i
|
||||
|
||||
- name: Build
|
||||
run: tsc
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
@@ -45,5 +48,5 @@ jobs:
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Build
|
||||
- name: Package
|
||||
run: npm run package
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set Rust version
|
||||
run: rustup override set ${{ env.RUST_VERSION }}
|
||||
- name: Install Formatter
|
||||
run: rustup component add rustfmt
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -35,7 +37,12 @@ jobs:
|
||||
${{ runner.os }}-${{ env.RUST_VERSION }}-cargo-
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --check --verbose
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
@@ -1,3 +1,36 @@
|
||||
# `2.4.0`
|
||||
_14.08.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
|
||||
|
||||
## General
|
||||
- [#568](https://github.com/qitechgmbh/control/pull/569) switched to a Realtime Linuxkernel on nixos
|
||||
- [#574](https://github.com/qitechgmbh/control/pull/587) isolated cores 2 and 3 on nixos to guarentee strict timings for ethercat
|
||||
- [#588](https://github.com/qitechgmbh/control/pull/589) limited frontend to 30 fps to reduce cpu usage
|
||||
- [#572](https://github.com/qitechgmbh/control/pull/572) switched to io_uring to reduce latency
|
||||
|
||||
|
||||
|
||||
# `2.3.0`
|
||||
_06.08.2025_
|
||||
|
||||
## Extruder V2
|
||||
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
|
||||
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
|
||||
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
|
||||
|
||||
## Winder V2
|
||||
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
|
||||
|
||||
## Laser V1
|
||||
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
|
||||
|
||||
## General
|
||||
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
|
||||
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
|
||||
|
||||
|
||||
# `2.2.0`
|
||||
|
||||
_15.07.2025_
|
||||
|
||||
@@ -256,7 +256,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -273,7 +273,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -345,7 +345,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -539,7 +539,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -728,7 +728,7 @@ dependencies = [
|
||||
"deluxe-macros",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -741,7 +741,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -756,7 +756,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -767,7 +767,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -804,7 +804,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1019,7 +1019,7 @@ dependencies = [
|
||||
"deluxe",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1071,7 +1071,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1200,7 +1200,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1736,9 +1736,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2175,7 +2175,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2206,7 +2206,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2342,7 +2342,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2374,9 +2374,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
@@ -2582,7 +2582,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2602,7 +2602,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2683,9 +2683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialport"
|
||||
version = "4.7.1"
|
||||
version = "4.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2daa7abb9b965493e3c8f4184c6f46435484ff2538a332b886788cf6768b927b"
|
||||
checksum = "cdb0bc984f6af6ef8bab54e6cf2071579ee75b9286aa9f2319a0d220c28b0a2b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
@@ -2856,9 +2856,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socketioxide"
|
||||
version = "0.17.1"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fe8a90fd0cdac0bcb7b3c40c1084f5658f8737c62dfb277f5620cf78f3849a"
|
||||
checksum = "476190583b592f1e3d55584269600f2d3c4f18af36adad03c41c27e82dcb6bd5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"engineioxide",
|
||||
@@ -2963,9 +2963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
version = "2.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2989,7 +2989,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3034,7 +3034,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3045,16 +3045,16 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||
|
||||
[[package]]
|
||||
name = "thread-priority"
|
||||
version = "1.2.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe075d7053dae61ac5413a34ea7d4913b6e6207844fd726bdd858b37ff72bf5"
|
||||
checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3129,9 +3129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -3151,7 +3151,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3313,7 +3313,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3570,7 +3570,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3605,7 +3605,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3670,6 +3670,28 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -3683,6 +3705,17 @@ dependencies = [
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -3691,7 +3724,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3702,14 +3735,24 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
@@ -3812,6 +3855,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -3991,7 +4043,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4012,7 +4064,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4032,7 +4084,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4066,5 +4118,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.105",
|
||||
]
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
|
||||
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
|
||||
# Repo Structure
|
||||
|
||||
Frontend
|
||||
@@ -41,6 +44,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
|
||||
# Dev Setup
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
|
||||
## Backend
|
||||
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
@@ -66,7 +71,6 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
@@ -90,8 +94,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
- [X] [Threading](./docs/control-loop.md)
|
||||
- [X] [Logging](./docs/logging.md)
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $1 == "release" ]; then
|
||||
FEATURE=""
|
||||
|
||||
# Check if "mock-machine" was passed as an argument
|
||||
if [[ "$1" == "mock-machine" ]]; then
|
||||
echo "Building Debug Code with mock-machine feature"
|
||||
FEATURE="--features mock-machine"
|
||||
fi
|
||||
if [[ "$1" == "release" ]]; then
|
||||
echo "building Release Code"
|
||||
cargo build --release
|
||||
else
|
||||
echo "building Debug Code"
|
||||
cargo build
|
||||
echo "Building Debug Code"
|
||||
cargo build $FEATURE
|
||||
fi
|
||||
|
||||
# Only setcap if not building with mock-machine
|
||||
if [ "$1" != "mock-machine" ]; then
|
||||
echo "Setting capabilities for server executable"
|
||||
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
|
||||
fi
|
||||
# set cap for server executable
|
||||
sudo setcap cap_net_raw=eip ./target/debug/server
|
||||
# run
|
||||
./target/debug/server
|
||||
|
||||
@@ -11,9 +11,9 @@ ethercrab = "0.6"
|
||||
interfaces = "0.0.9"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serialport = "4.7.1"
|
||||
serialport = "4.7.2"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.1"
|
||||
socketioxide = "0.17.2"
|
||||
uom = "0.36.0"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
@@ -23,7 +23,7 @@ erased-serde = "0.4.6"
|
||||
core_affinity = "0.8.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.172"
|
||||
libc = "0.2.175"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
|
||||
@@ -44,10 +44,10 @@ impl AccelerationSpeedController {
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(max_acceleration: f64, initial_speed: f64) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,12 +37,15 @@ impl AngularAccelerationSpeedController {
|
||||
/// Creates a new angular acceleration speed controller with simplified parameters.
|
||||
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(max_acceleration: AngularAcceleration, initial_speed: AngularVelocity) -> Self {
|
||||
pub fn new_simple(
|
||||
max_acceleration: AngularAcceleration,
|
||||
initial_speed: AngularVelocity,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ impl LinearAccelerationLimitingController {
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(acceleration: Acceleration, initial_speed: Velocity) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
acceleration, // acceleration
|
||||
-acceleration, // deceleration (negative for deceleration)
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
acceleration, // acceleration
|
||||
-acceleration, // deceleration (negative for deceleration)
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
|
||||
use ethercat_hal::devices::el1002::EL1002_IDENTITY_A;
|
||||
use ethercat_hal::devices::el1008::EL1008_IDENTITY_A;
|
||||
use ethercat_hal::devices::el2002::EL2002_IDENTITY_A;
|
||||
use ethercat_hal::devices::el2002::EL2002_IDENTITY_B;
|
||||
use ethercat_hal::devices::el2004::EL2004_IDENTITY_A;
|
||||
use ethercat_hal::devices::el2008::EL2008_IDENTITY_A;
|
||||
use ethercat_hal::devices::el2521::{
|
||||
@@ -13,6 +14,7 @@ use ethercat_hal::devices::el2522::EL2522_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3001::EL3001_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3021::EL3021_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3024::EL3024_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3062_0030::EL3062_0030_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_B;
|
||||
use ethercat_hal::devices::el6021::{
|
||||
@@ -26,6 +28,7 @@ use ethercat_hal::helpers::ethercrab_types::{
|
||||
EthercrabSubDeviceOperational, EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::MainDevice;
|
||||
use ethercrab::SubDeviceGroup;
|
||||
use ethercrab::SubDeviceIdentity;
|
||||
|
||||
use crate::machines::identification::DeviceMachineIdentification;
|
||||
@@ -66,11 +69,11 @@ impl Default for MachineIdentificationAddresses {
|
||||
///
|
||||
/// Returns a vector of MachineDeviceIdentification for all subdevices
|
||||
pub async fn read_device_identifications<'maindevice>(
|
||||
subdevices: &Vec<EthercrabSubDevicePreoperational<'maindevice>>,
|
||||
group: &SubDeviceGroup<16, 256>,
|
||||
maindevice: &MainDevice<'maindevice>,
|
||||
) -> Vec<Result<DeviceMachineIdentification, Error>> {
|
||||
let mut result = Vec::new();
|
||||
for subdevice in subdevices.iter() {
|
||||
for subdevice in group.iter(maindevice) {
|
||||
let identification = machine_device_identification(&subdevice, maindevice).await;
|
||||
result.push(identification);
|
||||
}
|
||||
@@ -228,7 +231,7 @@ pub fn get_identification_addresses<'maindevice>(
|
||||
EK1100_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL1002_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2002_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2002_IDENTITY_A | EL2002_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2004_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2008_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
@@ -237,8 +240,9 @@ pub fn get_identification_addresses<'maindevice>(
|
||||
MachineIdentificationAddresses::default()
|
||||
}
|
||||
EL2522_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3062_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
|
||||
@@ -56,7 +56,7 @@ impl DivideByCount for u64 {
|
||||
/// let mut window = MovingTimeWindow::new(Duration::from_secs(10), 100);
|
||||
/// window.update(1.5, Instant::now());
|
||||
/// window.update(2.5, Instant::now());
|
||||
///
|
||||
///
|
||||
/// let average = window.average();
|
||||
/// println!("Average: {}", average);
|
||||
/// ```
|
||||
|
||||
@@ -12,7 +12,10 @@ use crate::{
|
||||
serial::SerialDevice,
|
||||
socketio::event::GenericEvent,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
@@ -23,9 +26,9 @@ use super::{
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
@@ -42,6 +45,7 @@ impl MachineManager {
|
||||
machine_registry: &MachineRegistry,
|
||||
hardware: &MachineNewHardwareEthercat,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
) {
|
||||
// empty ethercat machines
|
||||
self.ethercat_machines.clear();
|
||||
@@ -69,6 +73,7 @@ impl MachineManager {
|
||||
device_group,
|
||||
hardware: &machine_new_hardware,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
machine_manager: machine_manager.clone(),
|
||||
});
|
||||
|
||||
// insert the machine into the ethercat machines map
|
||||
@@ -82,7 +87,7 @@ impl MachineManager {
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<&Result<Box<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
@@ -94,6 +99,7 @@ impl MachineManager {
|
||||
device: Arc<RwLock<dyn SerialDevice>>,
|
||||
machine_registry: &MachineRegistry,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
) {
|
||||
let hardware = MachineNewHardwareSerial { device };
|
||||
|
||||
@@ -107,6 +113,7 @@ impl MachineManager {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
machine_manager: machine_manager.clone(),
|
||||
});
|
||||
|
||||
tracing::info!("Adding serial machine {:?}", new_machine);
|
||||
@@ -137,6 +144,48 @@ impl MachineManager {
|
||||
.machine_identification_unique,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_machine_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.ethercat_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
return Some(machine_weak);
|
||||
}
|
||||
|
||||
pub fn get_serial_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.serial_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
return Some(machine_weak);
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups devices by machine identification
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
@@ -9,14 +9,14 @@ pub enum MachineManagerIter<'a> {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
@@ -25,13 +25,13 @@ pub enum MachineManagerIter<'a> {
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use crate::machines::new::MachineAct;
|
||||
use anyhow::anyhow;
|
||||
use api::MachineApi;
|
||||
use new::MachineNewTrait;
|
||||
use serde::Serialize;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::new::MachineAct;
|
||||
pub mod api;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
@@ -10,4 +16,65 @@ pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineNewTrait + MachineApi + MachineAct + Any + Send + Sync + Debug {}
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
/// Casts a `dyn Machine` to a specific machine type
|
||||
pub async fn downcast_machine<T: Machine>(
|
||||
machine: Arc<Mutex<dyn Machine>>,
|
||||
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let read_lock = machine.lock().await;
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
if read_lock.as_any().is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned_machine = Arc::clone(&machine);
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
Ok(Arc::from_raw(
|
||||
Arc::into_raw(cloned_machine) as *const Mutex<T>
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"[{}::downcast_machine] Downcast failed",
|
||||
module_path!()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to get strong count to see if Weak has reference
|
||||
pub trait GetStrongCount {
|
||||
fn get_strong_count(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<T> GetStrongCount for Weak<Mutex<T>> {
|
||||
fn get_strong_count(&self) -> usize {
|
||||
self.strong_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectedMachine<T: GetStrongCount> {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub machine: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct ConnectedMachineData {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
|
||||
where
|
||||
T: GetStrongCount,
|
||||
{
|
||||
fn from(value: &ConnectedMachine<T>) -> Self {
|
||||
Self {
|
||||
machine_identification_unique: value.machine_identification_unique.clone(),
|
||||
is_available: value.machine.get_strong_count() != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::serial::SerialDevice;
|
||||
use crate::{machines::manager::MachineManager, serial::SerialDevice};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
@@ -8,7 +8,11 @@ use ethercat_hal::{
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::{channel::Sender, lock::RwLock};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{pin::Pin, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::socketio::event::GenericEvent;
|
||||
|
||||
@@ -47,6 +51,7 @@ pub struct MachineNewParams<
|
||||
'machine_new_hardware_serial,
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
|
||||
use anyhow::Error;
|
||||
use smol::lock::Mutex;
|
||||
use std::{any::TypeId, collections::HashMap};
|
||||
use std::{any::TypeId, collections::HashMap, sync::Arc};
|
||||
|
||||
pub type MachineNewClosure =
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Box<Mutex<dyn Machine>>, Error> + Send + Sync>;
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
|
||||
|
||||
pub struct MachineRegistry {
|
||||
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
|
||||
@@ -27,7 +27,7 @@ impl MachineRegistry {
|
||||
machine_identficiation,
|
||||
// create a machine construction closure
|
||||
Box::new(|machine_new_params| {
|
||||
Ok(Box::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -36,7 +36,7 @@ impl MachineRegistry {
|
||||
pub fn new_machine(
|
||||
&self,
|
||||
machine_new_params: &MachineNewParams,
|
||||
) -> Result<Box<Mutex<dyn Machine>>, anyhow::Error> {
|
||||
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
|
||||
// get machiine identification
|
||||
let device_identification =
|
||||
&machine_new_params
|
||||
|
||||
@@ -160,7 +160,7 @@ pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_core_affinity_first_core(core_id: usize) -> Result<(), anyhow::Error> {
|
||||
pub fn set_core_affinity(core_id: usize) -> Result<(), anyhow::Error> {
|
||||
let cores = core_affinity::get_core_ids().unwrap_or_default();
|
||||
let core = cores.get(core_id);
|
||||
if let Some(core) = core {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Environment Setup
|
||||
|
||||
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
|
||||
|
||||
## Ubuntu/Debian Installation
|
||||
|
||||
```bash
|
||||
# Just press enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
apt install npm
|
||||
apt install nodejs
|
||||
# assuming you dont have git already, Git setup not included in here
|
||||
apt install git
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control
|
||||
cd electron
|
||||
npm install
|
||||
```
|
||||
|
||||
# Running Backend and Frontend Linux
|
||||
|
||||
## Backend
|
||||
|
||||
To Compile backend code and run it on Linux:
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
The script sets capabilities on the compiled binary like raw socket access.
|
||||
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
|
||||
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
|
||||
|
||||
## Mock-Machine
|
||||
|
||||
The Mock-Machine can be used to test code that does not require an actual machine connection.
|
||||
Like Frontend Code for example.
|
||||
|
||||
Mock machines can be used if the compile feature mock-machine is enabled.
|
||||
|
||||
```bash
|
||||
# in root git folder
|
||||
./cargo_run_linux.sh mock-machine
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
To run the Frontend Code:
|
||||
|
||||
```bash
|
||||
# need to be in the electron folder and an additional terminal
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
Generally you contribute to the codebase by:
|
||||
|
||||
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
|
||||
2. Work on it locally
|
||||
3. Commit your changes locally
|
||||
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
|
||||
|
||||
```bash
|
||||
git fetch
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
5. Push changes
|
||||
|
||||
```bash
|
||||
# IF you needed to rebase
|
||||
git push --force-with-lease
|
||||
# ELSE you just push
|
||||
git push
|
||||
```
|
||||
|
||||
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
|
||||
7. Request a Review and hope for the best :)
|
||||
|
||||
# Recommended Editor Setup
|
||||
|
||||
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Presets
|
||||
|
||||
To allow the user to save the current configuration of a machine, they can create a preset in the presets page. Presets are presited in electron's local storage.
|
||||
|
||||
Presets are all handled by the `PresetsPage`. To use the `PresetsPage` for a single machine, one must implement the following.
|
||||
|
||||
Create a zod schema for the data, the presets contain.
|
||||
```tsx
|
||||
const mock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof mock1PresetDataSchema;
|
||||
```
|
||||
Here, intensionally, `Mock1` is the schema type itself. It can be used as a type argument everything that uses presets.
|
||||
|
||||
Next, create a map containing all schema versions currently supported (the new schema, to be precise).
|
||||
```tsx
|
||||
const schemas = new Map([[1, mock1PresetDataSchema]]);
|
||||
```
|
||||
New, the schema will be used to parse version `1`. To create new versions, see migrations below.
|
||||
|
||||
One also needs to define a table, how the preset data should be shown to the user.
|
||||
```tsx
|
||||
const previewEntries: PresetPreviewEntries<Mock1> = [
|
||||
{
|
||||
name: "Frequency 1",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 2",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 3",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
|
||||
},
|
||||
];
|
||||
```
|
||||
`renderValue` gets the data itself and renders the corresponding part of it. Returning undefined will automatically be rendered as the value is not set, showing "N/A".
|
||||
|
||||
Create a function for the new react component and use the appropreate machine.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Here, two more functions need to be created. const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,The first takes in a preset and applies it on the machine. If data is missing, use the default defined by the backend.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
|
||||
const applyPreset = (preset: Preset<Mock1>) => {
|
||||
const frequency1 = preset.data?.frequency1 ?? 100;
|
||||
const frequency2 = preset.data?.frequency2 ?? 200;
|
||||
const frequency3 = preset.data?.frequency3 ?? 500;
|
||||
|
||||
setFrequency1(frequency1);
|
||||
setFrequency2(frequency2);
|
||||
setFrequency3(frequency3);
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
The later does the opposite, it turns a machine state, comming from the backend, into the preset data
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Having all this, we can simple pass it to the `PresetsPage`
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
// ...
|
||||
return (
|
||||
<PresetsPage
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
schemaVersion={1}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
Also mind the machine identification, that has to match the machine we are using and the schema versionen. Here we pass `1`, as we also used `1` in the schemas map.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are handled by the `PresetsPage`, however, one still has to define what to migrate.
|
||||
To do so, first create the new schema.
|
||||
```tsx
|
||||
const fancyNewMock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1_now_with_ai: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
frequency4: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof fancyNewMock1PresetDataSchema;
|
||||
```
|
||||
and then, also create a migration that uses the new schema, but can handle the old format.
|
||||
```tsx
|
||||
const mock1PresetDataSchemaMigration: PresetSchema = z.preprocess(
|
||||
(obj: any): PresetData<Mock1> => ({
|
||||
frequency1_now_with_ai: obj?.frequency1, // Here, a field have been renamed
|
||||
frequency2: obj?.frequency2,
|
||||
frequency3: obj?.frequency3,
|
||||
frequency4: 666.0, // A new field is added. Initialize it with default the value
|
||||
}),
|
||||
fancyNewMock1PresetDataSchema,
|
||||
);
|
||||
```
|
||||
Usings zods preprocessing step,
|
||||
we can first transform the data as needed and then validate using the new schema accordingly.
|
||||
|
||||
We have to register both schemas in our schema map.
|
||||
```tsx
|
||||
const schemas = new Map<number, Mock1>([
|
||||
[1, mock1PresetDataSchemaMigration as Mock1],
|
||||
[2, fancyNewMock1PresetDataSchema],
|
||||
]);
|
||||
```
|
||||
Mind the cast, which is ok, because `mock1PresetDataSchemaMigration` parses `fancyNewMock1PresetDataSchema` by construction.
|
||||
|
||||
Lastly, simply change the schema version being passed to the `PresetsPage`.
|
||||
```diff
|
||||
@@ -62,7 +77,7 @@ export function Mock1PresetsPage() {
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
- schemaVersion={1}
|
||||
+ schemaVersion={2}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
```
|
||||
Make sure, that all (exsiting and new migrations) return a schema version 2 conform format.
|
||||
Of corse, one can chain migrations of needed - just the end result must be complient.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Electrical Diagrams
|
||||
|
||||
This directory contains electrical wiring diagrams for various machines and systems supported by the control system.
|
||||
|
||||
## Available Diagrams
|
||||
|
||||
### Extruder
|
||||
- **Location**: [`extruder/`](extruder/)
|
||||
- **File**: [QiTech Pro Extruder Electrical Diagram 2025.pdf](extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025.pdf)
|
||||
|
||||
### Winder
|
||||
- **Location**: [`winder/`](winder/)
|
||||
- **File**: [QiTech Pro Winder Electrical Diagram.pdf](winder/QiTech_Pro_Winder_Electrical_Diagram.pdf)
|
||||
@@ -3,6 +3,6 @@
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"endOfLine": "auto",
|
||||
"endOfLine": "lf",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint .",
|
||||
"lint:write": "eslint . --fix",
|
||||
"format": "prettier --check .",
|
||||
"format:write": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
@@ -23,14 +24,14 @@
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.1",
|
||||
"@electron-forge/maker-deb": "^7.8.0",
|
||||
"@electron-forge/maker-rpm": "^7.6.1",
|
||||
"@electron-forge/maker-rpm": "^7.8.3",
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.0",
|
||||
"@electron-forge/plugin-vite": "^7.6.1",
|
||||
"@electron-forge/shared-types": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/fuses": "^2.0.0",
|
||||
"@eslint/compat": "^1.2.6",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
@@ -40,21 +41,21 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"electron": "36.4.0",
|
||||
"electron": "^37.2.6",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
@@ -78,7 +79,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.66.7",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.105.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
@@ -98,6 +99,7 @@
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-msgpack-parser": "^3.0.2",
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -4,7 +4,7 @@
|
||||
|
||||
This is the operating manual for the Mock Machine control system. The Mock Machine is a simulated industrial device used for testing and development purposes.
|
||||
|
||||

|
||||

|
||||
_Figure 1: Mock Machine Mascot_
|
||||
|
||||
## Safety Information
|
||||
@@ -20,7 +20,6 @@ _Figure 1: Mock Machine Mascot_
|
||||
### Starting the Machine
|
||||
|
||||
1. **Power On Sequence**
|
||||
|
||||
- Check all safety switches
|
||||
- Verify emergency stops are released
|
||||
- Power on the main controller
|
||||
|
||||
@@ -1 +1,689 @@
|
||||
# Winder Manual
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
# EC Declaration of Conformity
|
||||
|
||||
**according to 2006/42/EC, Annex II, No. 1 A**
|
||||
|
||||
The content of the EC Declaration of Conformity corresponds to Annex II, No. 1 A, of Directive 2006/42/EC.
|
||||
|
||||
<br><br><br>
|
||||
|
||||
**Manufacturer of the machine:**
|
||||
|
||||
QiTech Industries GmbH
|
||||
|
||||
Landwehrsrtaße 55
|
||||
|
||||
64293, Darmstadt
|
||||
<br><br><br>
|
||||
|
||||
**Representative of the manufacturer:**
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
Landwehrsrtaße 55
|
||||
|
||||
64293, Darmstadt
|
||||
<br><br><br>
|
||||
|
||||
**under consideration:**
|
||||
| General designation | Filament winding machine |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| function | Winding and rewinding of filaments |
|
||||
| Model | QiTech Winder |
|
||||
| type | Filament winding machine |
|
||||
| Trade name | QiTech Winder |
|
||||
<br><br>
|
||||
|
||||
We hereby declare that the above-mentioned machine is in conformity with all relevant provisions of the EC Machinery Directive 2006/42/EC. The machine is also in compliance with all relevant provisions of the following EC Directives:
|
||||
|
||||
- 2014/30/EU Electromagnetic Compatibility / European EMC Directive
|
||||
- 2014/35/EU Low Voltage Directive (protection objectives only)
|
||||
- 2011/65/EU ROHS Directives (ROHS 2)
|
||||
|
||||
The following harmonized standards (or parts of these standards) have been applied:
|
||||
|
||||
- DIN EN ISO 12100
|
||||
|
||||
Note: References of the EC directives, harmonized standards and standards and specifications can be found in the standards search
|
||||
|
||||
<br><br><br>
|
||||
|
||||
Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="images/manuals/winder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# Operating instructions based on DIN EN ISO 20607
|
||||
|
||||
| Designation | QiTech Winder |
|
||||
| ------------- | --------------------------------- |
|
||||
| Manufacturer | QiTech Industries GmbH |
|
||||
| Administrator | Milan von dem Bussche |
|
||||
| Address | Landwehrstraße 55 64293 Darmstadt |
|
||||
|
||||
<br><br>
|
||||
|
||||
**Read before commissioning!**
|
||||
|
||||
These operating instructions are intended to enable all persons working on the machine to carry out their work safely and correctly.
|
||||
|
||||
Every person who operates, maintains or repairs the machine must have read the operating instructions, in particular the safety regulations, before commissioning.
|
||||
|
||||
These operating instructions must be available on the machine at all times. The operator must ensure that all information necessary for the safe operation of the machine is made available to the operating personnel.
|
||||
|
||||
If the operating instructions are available in electronic form, the operator must ensure that the operating personnel is able to view the operating instructions.
|
||||
|
||||
The original version of these operating instructions is written in English.
|
||||
|
||||
We therefore ask you to read through the relevant instructions carefully and to observe them thoroughly.
|
||||
|
||||
After unpacking the machine, check for any damage that may have occurred during transport.
|
||||
|
||||
<br>
|
||||
|
||||
ATTENTION: Before using the machine for the first time, please inform yourself about the national regulations (occupational safety, environment).
|
||||
|
||||
<br>
|
||||
|
||||
Reproduction and reprinting in any form, even in extracts, requires the written authorization of QiTech Industries.
|
||||
|
||||
Operating instructions
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||
|
||||
| Item number | Item number Description |
|
||||
| ----------- | ----------------------------------------- |
|
||||
| 1 | Spool axis |
|
||||
| 2 | Coil quick-release fastener |
|
||||
| 3 | Grooved plate |
|
||||
| 4 | Traverse |
|
||||
| 5 | Installer |
|
||||
| 6 | Laser diode |
|
||||
| 7 | Load arm |
|
||||
| 8 | Laser measuring system (optional) |
|
||||
| 9 | Puller rollers |
|
||||
| 10 | Filament guide |
|
||||
| 11 | Emergency stop switch |
|
||||
| 12 | LAN-cable |
|
||||
| 13 | Connecter cable (if purchased with laser) |
|
||||
| 14 | Power supply cable |
|
||||
| 15 | Underframe |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1.1 Noise emissions
|
||||
|
||||
**Sound power level (LWAC)**
|
||||
|
||||
| Specification | Value | Unit |
|
||||
| ------------- | ----- | ---- |
|
||||
| Idle mode | <70 | dB |
|
||||
|
||||
<br><br>
|
||||
|
||||
**Workplace-related emission value (LPAC)**
|
||||
|
||||
| Specification | Value | Unit |
|
||||
| ------------- | ----- | ---- |
|
||||
| Idle mode | <70 | dB |
|
||||
|
||||
Noise emission specification according to DIN 45649 Part 1
|
||||
|
||||
<br><br>
|
||||
|
||||
Noise measurement method and operating condition acc: DIN 45635 Part 16 and Part 1605
|
||||
|
||||
Accuracy class: 2
|
||||
|
||||
<br>
|
||||
|
||||
# 1.2 General, important Safety instructions
|
||||
|
||||
1. Only professionally trained persons may operate the machine. The guarantee and warranty will be invalidated if damage is caused by improper operation.
|
||||
2. We would like to point out that no liability is accepted for damage caused by failure to observe these operating instructions.
|
||||
3. The operator of the machine must ensure that at least one copy of the operating instructions is kept in the immediate vicinity of the machine and is accessible to persons working with the machine.
|
||||
4. The operator must ensure that the safety and danger notices on the machine are observed and that the signs are clearly legible.
|
||||
5. Never work without the prescribed personal protective equipment (e.g. safety footwear).
|
||||
6. Wear close-fitting clothing and a hairnet if you have long hair. Do not wear loose or baggy clothing (ties, shirt sleeves, jewelry, etc.).
|
||||
7. Activities with a risk of entrapment may only be carried out without gloves. If gloves are required for individual activities, the machine must first be brought into a safe condition.
|
||||
8. If the emission sound pressure level at the workplace exceeds 80 dB (A), hearing protection must be worn.
|
||||
9. Never leave the machine in operation without supervision.
|
||||
10. Secure your machine so that it cannot be accessed by children. Persons who have not been instructed must not operate the machine.
|
||||
11. Before using the machine, make sure that it is in perfect condition (carry out a visual inspection before starting work). Pay particular attention to any damage to the earthing contact plug or the electrical connection. Never use the machine with defective, crushed or bare cables.
|
||||
12. Insert the earthing contact plug into an earthing contact socket suitable for the machine. The supply cable for the machine may only be connected to a socket outlet with earthing contact or a connection box. Have the socket outlet or connection box checked by a qualified electrician beforehand.
|
||||
13. During maintenance and cleaning work, the machine must be switched off and the earthing contact plug disconnected. Work on the electrical equipment of the machine may only be carried out by qualified electricians. The safety regulations for electrical engineering must be observed:
|
||||
- Switch off the machine
|
||||
- Disconnect the LAN-connection
|
||||
- Secure the machine against being switched on again
|
||||
- Have the machine de-energized by a qualified electrician
|
||||
- Earth and short-circuit the machine
|
||||
- Cover or isolate neighboring live parts
|
||||
|
||||
14. Do not reach into moving parts of the machine.
|
||||
15. Keep a safe distance from rotating parts of the machine. Only change the bobbin when the bobbin axis is stationary.
|
||||
16. Keep the work area clean. Do not place any objects on the machine.
|
||||
17. Always switch off the machine when you are not using it.
|
||||
18. Stay with the machine until it has come to a standstill.
|
||||
19. Repairs may only be carried out by qualified specialist personnel! Repair work may only be carried out by persons who are qualified for the repair work in question and who are familiar with the relevant occupational safety regulations.
|
||||
20. Protect the machine from moisture.
|
||||
21. Check the machine regularly for damage. Only replace damaged parts with original parts and have them replaced by qualified specialists. The guarantee and warranty will be invalidated if accessories and spare parts are used that are not authorized for the machine.
|
||||
22. To avoid inadequate lighting, we recommend installing light sources in accordance with national health and safety regulations (e.g. workplace directive).
|
||||
23. All persons working on the machine must be trained based on these operating instructions before starting work. In particular, persons working on the machine must be familiarized with the safety markings described in chapter 1.3.
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
## 1.3 Description of the pictograms, symbols and warning notices used
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# Prohibition signs
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="images/manuals/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="images/manuals/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="images/manuals/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="images/manuals/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="images/manuals/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="images/manuals/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="images/manuals/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="images/manuals/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="images/manuals/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="images/manuals/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="images/manuals/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="images/manuals/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="images/manuals/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="images/manuals/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="images/manuals/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1.4 Intended use
|
||||
|
||||
The QiTech Winder is designed for winding and rewinding filaments between 1 mm and 3 mm.
|
||||
The winder must be placed on a stable, level surface and secured if necessary. The test and work sequence is described in chapter 3.4 Work sequence.
|
||||
When using the winder, it is essential that the work steps described in the chapter "Work sequence" or in the operating instructions are followed.
|
||||
In the event of significant changes or deviations, it is necessary to carry out a new risk assessment in accordance with the Machinery Directive and to document the changes in the corresponding documents.
|
||||
|
||||
<br>
|
||||
|
||||
The machine is designed exclusively for use inside buildings. The machine must not be operated in potentially explosive atmospheres. Use in the area of food production is prohibited.
|
||||
Use of the machine for purposes other than those specified is prohibited.
|
||||
Other intended uses include cleaning and maintenance work as well as troubleshooting, fault rectification and repair.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.5 Reasonably foreseeable misuse
|
||||
|
||||
Reasonably foreseeable misuse exists in particular when winding unsuitable filaments:
|
||||
|
||||
- Filaments that are difficult to bend
|
||||
- Easily tearable filaments
|
||||
- Filaments outside the permitted diameter range of 1mm - 3mm
|
||||
- Filaments with a surface temperature of > 50°C
|
||||
- Particularly flat, sharp-edged or pointed filaments
|
||||
|
||||
In addition, the following procedures must be regarded as misuse:
|
||||
|
||||
- Operating the winder on an uneven surface or in a position other than the intended horizontal position or on an unbraked trolley.
|
||||
- The use of unauthorized coil sizes.
|
||||
- Use of spare parts and components not authorized by the manufacturer.
|
||||
- Operating the winder without supervision or in ghost shifts.
|
||||
|
||||
<br><br><br>
|
||||
|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
Furthermore, there is always the possibility that existing/provided safety guards are dismantled or the existing sensors are manipulated or triggered by means other than the intended components.
|
||||
|
||||
The misuse mentioned must be explicitly prohibited with reference to these operating instructions and by written operating instructions, and the use of the winder must also be explicitly restricted in the operating instructions to the intended use mentioned above.
|
||||
|
||||
Maintenance and repair of the machine may only be carried out by the manufacturer's employees, trained skilled workers or semi-skilled and trained personnel.
|
||||
|
||||
<br>
|
||||
|
||||
# 1.6 Significant modification / other use
|
||||
|
||||
Any use of the machine other than the intended use is prohibited and is considered improper use. Claims of any kind against the manufacturer or its authorized representatives due to damage resulting from improper use of the machine are excluded. The operator alone is liable for all damage caused by improper use.
|
||||
|
||||
<br>
|
||||
|
||||
# 1.7 Description of the safety devices
|
||||
|
||||
**Emergency stop devices**
|
||||
|
||||
The emergency stop switch is used to quickly stop the machine in the event of danger. It can be reached from all positions in front of the machine. When an emergency stop device is actuated, the entire machine is stopped by disconnecting the mains supply.
|
||||
|
||||
Hazardous movements are brought to a controlled standstill as quickly as possible. The machine can only be restarted by physically resetting the emergency stop switch and then starting one of the operating modes in the software.
|
||||
|
||||
<br>
|
||||
|
||||
<img src="images/manuals/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
|
||||
<br>
|
||||
|
||||
**Overheating protection**
|
||||
|
||||
If one of the motor drivers becomes too hot, it switches off automatically and sends an error message to the control system. This then stops all other components of the machine.
|
||||
|
||||
<br>
|
||||
|
||||
**Spool stop in the event of filament breakage**
|
||||
|
||||
If the filament tears during winding, the spool stops automatically after a short time.
|
||||
|
||||
<br>
|
||||
|
||||
**Separating protective devices:**
|
||||
|
||||
The housing clad with aluminum-dibond plates prevents contact with hot motors, interference with the drive spindle of the traverse and manipulation of the electronics. It also prevents electrostatic charges from jumping from the filament to the electronics.
|
||||
|
||||
If guards are opened or removed for maintenance work, they must be closed or refitted before the machine is switched on again. It is not permitted to operate the machine without all properly installed and operational protective devices.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.8 Instruction and Training
|
||||
|
||||
As the contractor/operator, you are obliged to inform and instruct the operating personnel about existing legal and accident prevention regulations as well as about existing safety equipment on the winder. This obligation also extends to safety equipment installed around the winder. The various technical qualifications of the employees must be taken into account.
|
||||
|
||||
The operating personnel must have understood the instruction and it must be ensured that the instruction is observed. This is the only way to ensure that your personnel work in a safety and hazard-conscious manner. Compliance with the contents of the instruction should be checked regularly.
|
||||
|
||||
As the owner/operator, you must therefore obtain written confirmation from each employee that they have attended an instruction session.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.9 Noise emissions from the machine during activities
|
||||
|
||||
The noise emissions of the machine were measured on the same machine types. Under normal operating conditions, the maximum noise emission of the extruder is below the maximum permitted values.
|
||||
|
||||
Depending on the activity or the duration of the activity, the operator must determine whether suitable hearing protection is required as part of a risk assessment.
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 2 Life phases of the machine
|
||||
|
||||
<br>
|
||||
|
||||
# 2.1 Transport and Assembly
|
||||
|
||||
**Requirements for the installation site**
|
||||
|
||||
- Installation only in industrial and commercial indoor areas.
|
||||
- The surface must be level and solid.
|
||||
- The location must be as fire-free as possible and equipped with a fire extinguisher.
|
||||
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br><br>
|
||||
|
||||
# 2.2 Assembly, installation and Commissioning
|
||||
|
||||
**Basic installation**
|
||||
|
||||
The Winder is supplied with a mobile container, the parking brakes must be applied.
|
||||
|
||||
The cables must be routed in such a way that they cannot be tripped over.
|
||||
|
||||
<br>
|
||||
|
||||
**Updating the QiTech Control Software**
|
||||
|
||||
The machines are delivered with the newest version of the software installed. Updates, as well as previous updates and the software version can be viewed under Setup-> Updates. If a new version of the software is available, it will be displayed here. To perform the update an internet connection is required. In the next step, click the red "Select" button next to the chosen update. The display will now shut down, and when it is turned back on, the latest version of the software will be installed.
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**Safety Measures**
|
||||
|
||||
Some important safety measures must be observed to ensure safe operation of the QiTech Winder:
|
||||
|
||||
- It is important to only use original fuses with the prescribed current ratings to avoid overloading.
|
||||
- If inspection, maintenance and repair work is carried out on electrical components, these must be de-energized beforehand. Equipment that has been disconnected must be secured against unintentional or automatic reconnection. First check that there is no voltage and insulate neighboring live components.
|
||||
- Care must be taken to ensure that design features are not modified in such a way as to reduce safety.
|
||||
- If it is necessary to work on live components, an additional person should be called in to operate the emergency stop button or the main switch in an emergency.
|
||||
- Only voltage-insulated tools and appropriate clothing may be used.
|
||||
- Proper earthing of the electrical system must be ensured by protective conductor systems.
|
||||
- Cables must be checked regularly for damage and replaced if necessary.
|
||||
|
||||
<br>
|
||||
|
||||
# 2.3 Operation and control (work sequence)
|
||||
|
||||
A visual inspection must be carried out every working day before the machine is started.
|
||||
|
||||
Then start the machine as follows:
|
||||
|
||||
**1. Switch on the machine by plugging in the 220V power supply**
|
||||
|
||||
- Make sure that the emergency Stop button is inactive
|
||||
- Turn on the display by pressing the button on the right side of the display above the USB connectors
|
||||
- Winder then appears in the left column
|
||||
|
||||
**2. Mount the spool on the spool axle**
|
||||
|
||||
- Remove the quick-release fastener
|
||||
- Move the driver in the groove to the height of the hole in the spool flank
|
||||
- Slide the spool over the axle and the driver
|
||||
- Tighten the quick-release fastener again
|
||||
|
||||
**3. Calibrating the spool sides**
|
||||
|
||||
- Select "Control" in the function bar of the software
|
||||
- And go to the “Hold” mode
|
||||
- Move the "Outer Limit" slider to the approximate position of the outside of the bobbin
|
||||
- Click on "save"
|
||||
- Using the laser dot, repeat the previous steps and fine-tune until the dot is exactly on the edge of the inner bobbin flank
|
||||
- Repeat the same process for the other outer spool flank
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**4. Start pulling**
|
||||
|
||||
- Select “Control” in the function bar
|
||||
- Press the "Pull" button
|
||||
- Thread the filament through the puller rollers (be careful, counter-rotating rollers!)
|
||||
- For extrusion: Use the "Puller, Target Speed" slider to manually adjust the speed until the filament has the desired diameter
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**5. Start winding**
|
||||
|
||||
- Rotate the load arm upwards and place it on the puller
|
||||
- Press the "Wind" button
|
||||
- Thread the filament coming out of the puller through the feeder into the spool and fix it there
|
||||
- Place the load arm on the filament strand from above
|
||||
|
||||
<br>
|
||||
|
||||
**6. Change the spool**
|
||||
|
||||
- Press the "Pull" button
|
||||
- Remove the spool as described above and insert a new spool
|
||||
- Continue winding by clicking on "Wind" and repeat the winding process as mentioned above
|
||||
|
||||
<br>
|
||||
|
||||
**7. Stopping the winding process**
|
||||
|
||||
- Press the "Hold" button - motors energized but not in motion
|
||||
- Select "Standby" mode - no current on the motors
|
||||
- If the filament gets caught, the spool does not spin or there is a danger to persons, press the emergency stop button
|
||||
|
||||
<br>
|
||||
|
||||
**8. Switch off the machine**
|
||||
|
||||
- switch off the machine by disconnecting the power supply cable
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
**Unwinding filament**
|
||||
|
||||
- Mount the filament roll to be unwound onto the spool axle
|
||||
- Select “Config” in the function bar
|
||||
- Press the “Reverse” button to change the mode
|
||||
- Go back to “Control” in the function bar and adjust the “Puller Target Speed” as normal
|
||||
- Feed the filament through the puller rollers in the opposite direction to winding (caution, counter-rotating rollers!)
|
||||
|
||||
<br>
|
||||
|
||||
**Changing process variables during operation**
|
||||
|
||||
All settings such as the position of the spool sides, the winding speed or the filament diameter can also be adjusted during the winding process without interrupting it. To do this, change the value in the relevant field and click the "Wind" button again to apply it.
|
||||
|
||||
<br>
|
||||
|
||||
**Rewinding filament**
|
||||
|
||||
- Mount the full spool in front of the Puller wheels, so that it can spin freely
|
||||
- Mount the empty bobbin on the bobbin axle
|
||||
- Calibrate the Traverse the same way as mentioned above
|
||||
- Guide the filament from the full spool over the feeder and fix it to the spool
|
||||
- Click on “Control" in the function bar
|
||||
- Click on “Wind”
|
||||
- Adjust the pulling speed using the slider.
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
|
||||
<br><br>
|
||||
|
||||
## 2.4 Maintenance, inspection, Testing
|
||||
|
||||
Maintenance and servicing work on the machine may only be carried out by reliable, trained personnel. The minimum age required by law in the country of use must be observed!
|
||||
|
||||
Personnel to be trained, instructed or undergoing general training may only work on the Winder under the constant supervision of an experienced person!
|
||||
|
||||
<br>
|
||||
|
||||
**Updating the Winder software**
|
||||
|
||||
There are regular updates for the "QiTech Control" software, which may contain bug fixes as well as new functions or security updates. It is therefore recommended that you regularly check if new Updates are available and install them as described in section 2.2.
|
||||
|
||||
<br>
|
||||
|
||||
**Lubricating the traverse spindle**
|
||||
|
||||
The threaded spindle must be re-oiled after approx. 20 operating hours, but at the latest when the traverser no longer moves to the desired position because the motor is spinning.
|
||||
|
||||
To do this, loosen the plate on the top of the housing with an M5 Allen key and drizzle commercially available lubricating oil or grease, e.g. WD40, over the entire length of the threaded spindle.
|
||||
|
||||
<br>
|
||||
|
||||
**Retightening screw connections**
|
||||
|
||||
After approx. 100 operating hours, the rear panel of the winder should be removed using an M5 Allen key so that the screw connections inside the housing can be retightened. The following should be checked:
|
||||
|
||||
- Grub screws of the motor couplings (puller, traverse, coil)
|
||||
- Fastening screws of the motors (puller, traverse, coil)
|
||||
- Fastening screws of the angle sensor of the load arm
|
||||
|
||||
All should be tightened at least hand-tight.
|
||||
|
||||
<br>
|
||||
|
||||
**Replacing the puller rollers**
|
||||
|
||||
The polyurethane coating of the puller rollers will inevitably wear out over time. As soon as you notice the filament slipping through the rollers, they should be replaced. Suitable spare parts are available from the dealer. To replace them, unfasten the screws marked red in the picture.
|
||||
|
||||
Now remove the black plastic caps and simply pull out the puller rollers. To install the new rollers back on, slide them on the axes, put the plastic cap back on and screw the screws.
|
||||
|
||||
<img src="images/manuals/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## 2.5 Troubleshooting and fault rectification
|
||||
|
||||
<br>
|
||||
|
||||
**Machine is not recognized by QiTech Control**
|
||||
|
||||
- Is the LAN-cable connected?
|
||||
- Is the latest version of QiTech Control installed?
|
||||
|
||||
<br>
|
||||
|
||||
**Filament tears off frequently**
|
||||
|
||||
- Was the filament sufficiently cooled before winding?
|
||||
- Is the filament within the permitted diameter range of 1mm - 3mm?
|
||||
|
||||
<br>
|
||||
|
||||
**Puller rollers are scored**
|
||||
|
||||
It is normal for the puller rollers to wear out over time. However, some options can be checked:
|
||||
|
||||
- Is the surface temperature < 50°C when the filament runs through the rollers?
|
||||
- Is the filament slipping through the rollers instead of being conveyed by them, causing more abrasion?
|
||||
- Is the tension of the filament too strong?
|
||||
|
||||
<br>
|
||||
|
||||
**Filament is not wound perfectly parallel**
|
||||
|
||||
- How large is the inner diameter of the spool on which the filament is wound? The closer the winding is to the traverse, the better the winding pattern will be.
|
||||
- Have the spool sides been calibrated as described?
|
||||
- Has the correct filament diameter been set?
|
||||
- Have you tried to fine-tune the two settings independently of the procedure described here?
|
||||
- Does the problem already occur in the first layer, or only afterwards? If the first layer is not perfectly wound (e.g. due to skewed threading), the filament in the subsequent layers always slips into the gaps in the winding below and "perfect winding" can no longer take place.
|
||||
|
||||
<br>
|
||||
|
||||
**Winder suddenly switches off**
|
||||
|
||||
One of the motor drivers has probably overheated. To prevent uncontrolled winding behavior, the controller therefore switches off the entire winder. It is advisable to wait approx. 5 minutes and try winding again. If the error occurs frequently, a lower ambient temperature (optionally housing temperature) must be ensured.
|
||||
|
||||
<br>
|
||||
|
||||
**Load adjustment does not work correctly at high or low speeds**
|
||||
|
||||
The sensor inside the housing has probably twisted and the load adjustment range has therefore shifted upwards or downwards. To rectify this, turn the angle sensor back and tighten its screws as described in "Tightening screw connections".
|
||||
|
||||
<br><br>
|
||||
|
||||
# 2.6 Disassembly, decommissioning and Disposal
|
||||
|
||||
When the machine has reached the end of its planned service life, it is recommended that the manufacturer be contacted for the purpose of taking it back free of charge. If this is not possible, the machine must be taken out of service permanently and then disposed of or scrapped properly.
|
||||
|
||||
To do this, first disconnect the entire power supply. All components must then be separated by material class to enable subsequent recycling or differentiated scrapping.
|
||||
|
||||
The machine consists largely of aluminum and plastic and can be easily separated by loosening the screw connections. Electrical scrap, plastic, rubber seals, lubricants and other auxiliary materials must be disposed of separately by an authorized specialist company.
|
||||
|
||||
| Picture | Disposal Instructions |
|
||||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 3 Hazard warning and information on Residual risks
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="images/manuals/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="images/manuals/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
# 4 Glossary and Definitions
|
||||
|
||||
**Specialized personnel**
|
||||
|
||||
Due to their specialist training, knowledge and experience as well as knowledge of the relevant regulations, skilled personnel are able to carry out the work assigned to them and to recognize and avoid potential hazards independently. It is assumed that specialized personnel have also been instructed (see definition of instructed person).
|
||||
|
||||
<br>
|
||||
|
||||
**Instructed person**
|
||||
|
||||
The instructed person has been instructed by the operator on the tasks assigned to them and the potential hazards associated with improper behavior. The correct execution of the activities is monitored by specialized personnel.
|
||||
|
||||
<br>
|
||||
|
||||
**Qualified electrician**
|
||||
|
||||
A qualified electrician is someone who is able to assess the work assigned to them and recognize potential hazards based on their technical training, knowledge and experience as well as knowledge of the relevant regulations.
|
||||
|
||||
<br>
|
||||
|
||||
**Operator**
|
||||
|
||||
The operator is the person who uses, adjusts and monitors the machine in day-to-day operation. He is characterized by experience in handling the machine as well as in-depth knowledge of its functions and how to proceed in the event of a fault.
|
||||
|
||||
<br>
|
||||
|
||||
**Employment ban**
|
||||
|
||||
The following persons may not work on the machine:
|
||||
|
||||
- Persons with skin diseases
|
||||
- Persons with chronic respiratory problems or a tendency to allergic reactions
|
||||
- Pregnant women and nursing mothers
|
||||
- Children under 18 years
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## Life phases of the machine / system
|
||||
|
||||
| Life phase of the machine/system | User/qualification |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| A Construction | Trained and instructed specialized personnel of the manufacturer |
|
||||
| B Transport | Trained and instructed specialist personnel of the manufacturer or forwarding agent/transporter |
|
||||
| C Installation | Trained and instructed specialist personnel of the manufacturer or service technician of the manufacturer |
|
||||
| D Automatic operation | Instructed personnel of the operator |
|
||||
| E Semi-automatic operation | |
|
||||
| F Setting/adjusting/restarting the system | |
|
||||
| G Testing | |
|
||||
| H Eliminating faults in the work process | Trained and instructed specialized personnel of the operator or service technicians of the manufacturer |
|
||||
| I Observation of production processes | Instructed personnel of the operator |
|
||||
| J Troubleshooting and fault rectification | |
|
||||
| K Cleaning, maintenance | |
|
||||
| L Maintenance | Trained and instructed specialized personnel of the operator or service technicians of the manufacturer |
|
||||
| M Decommissioning | Trained and instructed specialized personnel of the operator |
|
||||
@@ -7,16 +7,17 @@ import { useSyncExternalStore } from "react";
|
||||
import { z } from "zod";
|
||||
import { toastError, toastZodError } from "@/components/Toast";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { FPS_30 } from "@/lib/constants";
|
||||
|
||||
/**
|
||||
* Simple buffer-based store updater to limit React re-renders to ~60 FPS
|
||||
* Writes events to a plain JS object and syncs to store every 16ms
|
||||
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
|
||||
* Writes events to a plain JS object and syncs to store every 33ms
|
||||
*/
|
||||
export class ThrottledStoreUpdater<S> {
|
||||
private store: StoreApi<S>;
|
||||
private buffer: S;
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private readonly syncDelay = 1000 / 60; // ~60 FPS (16.67ms)
|
||||
private readonly syncDelay = FPS_30; // ~30 FPS (33.33ms)
|
||||
|
||||
constructor(store: StoreApi<S>) {
|
||||
this.store = store;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useId } from "react";
|
||||
import { Icon, IconName } from "./Icon";
|
||||
|
||||
export type FileInputProps = {
|
||||
children: React.ReactNode;
|
||||
accept?: string;
|
||||
icon?: IconName;
|
||||
onFile?: (files: File) => void;
|
||||
onFiles?: (files: FileList) => void;
|
||||
};
|
||||
|
||||
export const FileInput = ({
|
||||
children,
|
||||
icon,
|
||||
accept,
|
||||
onFiles,
|
||||
onFile,
|
||||
}: FileInputProps) => {
|
||||
const id = useId();
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
|
||||
if (!files || files.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onFile && files.length == 1) {
|
||||
onFile(files[0]);
|
||||
}
|
||||
|
||||
if (onFiles) {
|
||||
onFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="bg-primary text-primary-foreground item-center flex w-full cursor-pointer flex-row justify-center gap-2 rounded-md px-6 py-6 select-none"
|
||||
>
|
||||
<Icon name={icon} className="size-6" />
|
||||
{children}
|
||||
<input
|
||||
id={id}
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={handleFileChange}
|
||||
multiple={!!onFiles}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export type JsonFileInputProps = {
|
||||
children: React.ReactNode;
|
||||
icon?: IconName;
|
||||
onJson: (json: any) => void;
|
||||
};
|
||||
|
||||
export function JsonFileInput({ children, icon, onJson }: JsonFileInputProps) {
|
||||
const handleFile = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const text = e.target?.result as string;
|
||||
const json = JSON.parse(text);
|
||||
onJson(json);
|
||||
} catch (err) {
|
||||
console.error("Invalid JSON file:", err);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<FileInput accept=".json,application/json" onFile={handleFile} icon={icon}>
|
||||
{children}
|
||||
</FileInput>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
|
||||
interface MachineIdentificationUnique {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
}
|
||||
|
||||
interface Machine {
|
||||
name: string;
|
||||
machine_identification_unique: MachineIdentificationUnique;
|
||||
}
|
||||
|
||||
interface MachineSelectorProps {
|
||||
machines: Machine[];
|
||||
selectedMachine?: Machine | null;
|
||||
connectedMachineState?: {
|
||||
machine_identification_unique?: {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
} | null;
|
||||
is_available: boolean;
|
||||
};
|
||||
setConnectedMachine: (machine: MachineIdentificationUnique) => void;
|
||||
clearConnectedMachine?: () => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const MachineSelector: React.FC<MachineSelectorProps> = ({
|
||||
machines,
|
||||
selectedMachine,
|
||||
connectedMachineState,
|
||||
setConnectedMachine,
|
||||
clearConnectedMachine,
|
||||
title = "Machine Connection",
|
||||
}) => {
|
||||
return (
|
||||
<ControlCard title={title}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
|
||||
<Icon name="lu:Settings" className="text-xl" />
|
||||
<span>
|
||||
{selectedMachine?.name ?? "Select a Machine"}{" "}
|
||||
{selectedMachine?.machine_identification_unique.serial ?? ""}
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{machines.map((machine) => {
|
||||
const isSelected =
|
||||
connectedMachineState?.machine_identification_unique
|
||||
?.machine_identification.machine ===
|
||||
machine.machine_identification_unique.machine_identification
|
||||
.machine;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={machine.machine_identification_unique.serial}
|
||||
onClick={() =>
|
||||
setConnectedMachine(machine.machine_identification_unique)
|
||||
}
|
||||
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
|
||||
isSelected ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
{connectedMachineState?.is_available ? (
|
||||
<Icon name="lu:Power" className="text-lg text-green-600" />
|
||||
) : (
|
||||
<Icon name="lu:Power" className="text-lg text-gray-400" />
|
||||
)}
|
||||
<span>
|
||||
{machine.name} – Serial:{" "}
|
||||
{machine.machine_identification_unique.serial}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{clearConnectedMachine && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={clearConnectedMachine}
|
||||
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
|
||||
>
|
||||
<Icon name="lu:Settings" className="text-lg" />
|
||||
<span>Clear Selection</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ControlCard>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import { generateHeadingId } from "@/lib/markdown/heading";
|
||||
|
||||
type MarkdownProps = {
|
||||
@@ -71,6 +72,7 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
<div>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h1: ({ node, children, ...props }) => {
|
||||
@@ -78,13 +80,17 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
const id = generateHeadingId(text);
|
||||
const isFirst = isFirstHeading(text, 1);
|
||||
return (
|
||||
<h1
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-2xl`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
<>
|
||||
<h1
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-2xl font-bold`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
<hr className="my-2" />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -93,13 +99,16 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
const id = generateHeadingId(text);
|
||||
const isFirst = isFirstHeading(text, 2);
|
||||
return (
|
||||
<h2
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-xl`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
<>
|
||||
<h2
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-xl font-bold`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
<hr className="my-2" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -108,13 +117,16 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
const id = generateHeadingId(text);
|
||||
const isFirst = isFirstHeading(text, 3);
|
||||
return (
|
||||
<h3
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-lg`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
<>
|
||||
<h3
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-lg font-bold`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
<hr className="my-2" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -123,26 +135,35 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
const id = generateHeadingId(text);
|
||||
const isFirst = isFirstHeading(text, 4);
|
||||
return (
|
||||
<h4
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-lg`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
<>
|
||||
<h4
|
||||
id={id}
|
||||
className={`${isFirst ? "" : "pt-4"} pb-2 text-lg font-bold`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
<hr className="my-2" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul className="markdown-list list-inside list-disc" {...props} />
|
||||
<ul
|
||||
className="markdown-list mb-6 ml-6 list-outside list-disc"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
li: ({ node, ...props }) => (
|
||||
<li className="markdown-list list-inside list-disc" {...props} />
|
||||
<li className="markdown-list" {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol className="markdown-list list-inside list-decimal" {...props} />
|
||||
<ol
|
||||
className="markdown-list mb-6 ml-6 list-outside list-decimal"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node, ...props }) => <p className="text-base" {...props} />,
|
||||
@@ -219,6 +240,8 @@ export function Markdown({ text }: MarkdownProps) {
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
hr: ({ node, ...props }) => <hr className="my-4" {...props} />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
br: ({ node, ...props }) => <br {...props} />,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RefObject } from "react";
|
||||
import uPlot from "uplot";
|
||||
import { GraphConfig, HandlerRefs } from "./types";
|
||||
import { FPS_30 } from "@/lib/constants";
|
||||
|
||||
export interface HandlerCallbacks {
|
||||
updateYAxisScale: (xMin?: number, xMax?: number) => void;
|
||||
@@ -40,13 +41,13 @@ export function createEventHandlers(
|
||||
if (callbackTimeouts.zoom) clearTimeout(callbackTimeouts.zoom);
|
||||
callbackTimeouts.zoom = setTimeout(() => {
|
||||
callbacks.onZoomChange?.(graphId, range);
|
||||
}, 16); // ~60fps
|
||||
}, FPS_30);
|
||||
},
|
||||
onViewModeChange: (mode: "default" | "all" | "manual", isLive: boolean) => {
|
||||
if (callbackTimeouts.viewMode) clearTimeout(callbackTimeouts.viewMode);
|
||||
callbackTimeouts.viewMode = setTimeout(() => {
|
||||
callbacks.onViewModeChange?.(graphId, mode, isLive);
|
||||
}, 16);
|
||||
}, FPS_30);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { PresetData, PresetSchema } from "@/lib/preset/preset";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import React, { useState } from "react";
|
||||
import { TouchButton } from "../touch/TouchButton";
|
||||
import { DialogHeader } from "../ui/dialog";
|
||||
import { Icon } from "../Icon";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export type NewPresetDialogProps<T extends PresetSchema> = {
|
||||
currentState?: PresetData<T>;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
onSave: (name: string) => void;
|
||||
};
|
||||
|
||||
export function NewPresetDialog<T extends PresetSchema>({
|
||||
currentState,
|
||||
onSave,
|
||||
previewEntries,
|
||||
}: NewPresetDialogProps<T>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const handleSave = () => {
|
||||
setOpen(false);
|
||||
setName("");
|
||||
onSave(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<TouchButton disabled={!currentState} icon="lu:SquarePlus">
|
||||
Create New Preset
|
||||
</TouchButton>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex flex-row items-center gap-2">
|
||||
<Icon name="lu:SquarePlus" />
|
||||
Create a New Preset
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Save the configuration of this machine to use it later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
|
||||
<Input
|
||||
placeholder="New Preset Name"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex flex-col gap-6 text-sm">
|
||||
<span>Current Settings:</span>
|
||||
<PresetPreviewTable entries={previewEntries} data={currentState} />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="flex flex-row gap-4">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:X"
|
||||
className="h-21 flex-1"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Abort
|
||||
</TouchButton>
|
||||
|
||||
<TouchButton
|
||||
className="h-21 flex-1 flex-shrink-0"
|
||||
onClick={handleSave}
|
||||
icon="lu:Save"
|
||||
disabled={!name}
|
||||
>
|
||||
Save
|
||||
</TouchButton>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from "react";
|
||||
import { Preset, PresetSchema } from "@/lib/preset/preset";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { PresetShowDialog } from "./PresetShowDialog";
|
||||
import { Icon } from "../Icon";
|
||||
import { PresetPreviewEntries } from "./PresetPreviewTable";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
|
||||
type PresetCardMenu = {
|
||||
onOverwrite?: () => void;
|
||||
onDelete?: () => void;
|
||||
onExport?: () => void;
|
||||
};
|
||||
|
||||
export function PresetCardMenu({
|
||||
onOverwrite,
|
||||
onDelete,
|
||||
onExport,
|
||||
}: PresetCardMenu) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<TouchButton variant="outline" icon="lu:Menu" />
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="flex flex-col gap-4">
|
||||
{onOverwrite && (
|
||||
<TouchButton
|
||||
className="flex-shrink-0"
|
||||
variant="outline"
|
||||
onClick={onOverwrite}
|
||||
>
|
||||
<Icon name="lu:HardDriveUpload" />
|
||||
Overwrite
|
||||
</TouchButton>
|
||||
)}
|
||||
{onExport && (
|
||||
<TouchButton
|
||||
className="flex-shrink-0"
|
||||
variant="outline"
|
||||
onClick={onExport}
|
||||
>
|
||||
<Icon name="lu:Download" />
|
||||
Export
|
||||
</TouchButton>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchButton
|
||||
className="flex-shrink-0"
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
</TouchButton>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export type PresetCardProps<T extends PresetSchema> = {
|
||||
preset: Preset<T>;
|
||||
onApply: (preset: Preset<T>) => void;
|
||||
onOverwrite: (preset: Preset<T>) => void;
|
||||
onDelete: (preset: Preset<T>) => void;
|
||||
onExport?: (preset: Preset<T>) => void;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
isReadOnly?: boolean;
|
||||
hideDate?: boolean;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export function PresetCard<T extends PresetSchema>({
|
||||
preset,
|
||||
onApply,
|
||||
onOverwrite,
|
||||
onDelete,
|
||||
onExport,
|
||||
previewEntries,
|
||||
isReadOnly,
|
||||
hideDate,
|
||||
isActive,
|
||||
}: PresetCardProps<T>) {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-4 rounded-3xl border border-gray-200 bg-white p-4 shadow">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="flex flex-row gap-2 text-lg font-semibold">
|
||||
{isActive && <Icon name="lu:Check" className="text-green-500" />}
|
||||
<span title={preset.name} className="w-60 truncate">
|
||||
{preset.name}
|
||||
</span>
|
||||
</div>
|
||||
{!hideDate && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{preset.lastModified?.toLocaleString() || "Unknown date"}
|
||||
</div>
|
||||
)}
|
||||
{isActive && (
|
||||
<span className="text-green-500">This preset is active</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<PresetShowDialog
|
||||
preset={preset}
|
||||
onApply={onApply}
|
||||
previewEntries={previewEntries}
|
||||
hideDate={hideDate}
|
||||
/>
|
||||
{isReadOnly ? (
|
||||
<TouchButton variant="outline" icon="lu:Menu" disabled />
|
||||
) : (
|
||||
<PresetCardMenu
|
||||
onOverwrite={() => onOverwrite(preset)}
|
||||
onDelete={() => onDelete(preset)}
|
||||
onExport={() => onExport && onExport(preset)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { PresetData, PresetSchema } from "@/lib/preset/preset";
|
||||
import { renderUnitSymbol, renderUnitSyntax, Unit } from "@/control/units";
|
||||
|
||||
export const previewSeparator = undefined;
|
||||
|
||||
type PreviewSeparator = typeof previewSeparator;
|
||||
|
||||
type PresetPreviewEntry<T extends PresetSchema> = {
|
||||
name: string;
|
||||
unit?: Unit;
|
||||
renderValue: (data: PresetData<T>) => string | undefined;
|
||||
};
|
||||
|
||||
export type PresetPreviewEntries<T extends PresetSchema> = (
|
||||
| PresetPreviewEntry<T>
|
||||
| PreviewSeparator
|
||||
)[];
|
||||
|
||||
export type PresetPreviewTableProps<T extends PresetSchema> = {
|
||||
data: PresetData<T>;
|
||||
entries: PresetPreviewEntries<T>;
|
||||
};
|
||||
|
||||
export function PresetPreviewTable<T extends PresetSchema>({
|
||||
data,
|
||||
entries,
|
||||
}: PresetPreviewTableProps<T>) {
|
||||
return (
|
||||
<div className="grid grid-cols-[60%_10%_30%]">
|
||||
{entries.map((entry, i) => {
|
||||
if (entry === previewSeparator) {
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<div className="h-[1em]" />
|
||||
<div />
|
||||
<div />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const value = entry.renderValue(data);
|
||||
return (
|
||||
<Fragment key={entry.name}>
|
||||
<div>{entry.name}</div>
|
||||
<div>=</div>
|
||||
<div>
|
||||
{value === undefined
|
||||
? "N/A"
|
||||
: renderUnitSyntax(value, entry.unit)}
|
||||
{entry.unit && " " + renderUnitSymbol(entry.unit)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Preset, PresetSchema } from "@/lib/preset/preset";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import React, { useState } from "react";
|
||||
import { TouchButton } from "../touch/TouchButton";
|
||||
import { DialogHeader } from "../ui/dialog";
|
||||
import { Icon } from "../Icon";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
|
||||
|
||||
export type PresetShowDialogProps<T extends PresetSchema> = {
|
||||
preset: Preset<T>;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
onApply: (preset: Preset<T>) => void;
|
||||
hideDate?: boolean;
|
||||
};
|
||||
|
||||
export function PresetShowDialog<T extends PresetSchema>({
|
||||
preset,
|
||||
onApply,
|
||||
previewEntries,
|
||||
hideDate,
|
||||
}: PresetShowDialogProps<T>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleApply = (preset: Preset<T>) => {
|
||||
setOpen(false);
|
||||
onApply(preset);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<TouchButton variant="outline" icon="lu:Eye" className="w-max">
|
||||
Show
|
||||
</TouchButton>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex flex-row items-center gap-2">
|
||||
<Icon name="lu:Save" />
|
||||
<span className="w-100 text-center">{preset.name}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Applying presets carelessly might damage machines.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-6 text-sm">
|
||||
{!hideDate && (
|
||||
<div>
|
||||
Latest modification at{" "}
|
||||
{preset.lastModified.toLocaleString() || "N/A"}
|
||||
</div>
|
||||
)}
|
||||
<PresetPreviewTable entries={previewEntries} data={preset.data} />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="flex flex-row gap-4">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:X"
|
||||
className="h-21 flex-1"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Abort
|
||||
</TouchButton>
|
||||
|
||||
<TouchButton
|
||||
className="h-21 flex-1 flex-shrink-0"
|
||||
onClick={() => handleApply(preset)}
|
||||
icon="lu:HardDriveDownload"
|
||||
>
|
||||
Apply to Machine
|
||||
</TouchButton>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { Page } from "@/components/Page";
|
||||
import { usePresets, UsePresetsParams } from "@/lib/preset/usePresets";
|
||||
import { Preset, PresetSchema } from "@/lib/preset/preset";
|
||||
import { PresetCard } from "./PresetCard";
|
||||
import { PresetPreviewEntries } from "./PresetPreviewTable";
|
||||
import { NewPresetDialog } from "./NewPresetDialog";
|
||||
import { downloadJson } from "@/lib/download";
|
||||
import { JsonFileInput } from "../FileInput";
|
||||
|
||||
type PresetsPageProps<T extends PresetSchema> = UsePresetsParams<T> & {
|
||||
applyPreset: (preset: Preset<T>) => void;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
};
|
||||
|
||||
export function PresetsPage<T extends PresetSchema>({
|
||||
applyPreset,
|
||||
machine_identification,
|
||||
currentState,
|
||||
schemas,
|
||||
schemaVersion,
|
||||
previewEntries,
|
||||
defaultState,
|
||||
}: PresetsPageProps<T>) {
|
||||
const presets = usePresets<T>({
|
||||
machine_identification,
|
||||
currentState,
|
||||
schemas,
|
||||
schemaVersion,
|
||||
defaultState,
|
||||
});
|
||||
|
||||
const handleOverwritePreset = (preset: Preset<T>) => {
|
||||
const msg = `Are you sure you want to overwrite the preset "${preset.name}" with the current settings? This cannot be undone.`;
|
||||
|
||||
if (!confirm(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
presets.updateFromCurrentState(preset);
|
||||
};
|
||||
|
||||
const handleDeletePreset = (preset: Preset<T>) => {
|
||||
const msg = `Are you sure you want to delete the preset "${preset.name}"? This cannot be undone.`;
|
||||
|
||||
if (!confirm(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
presets.remove(preset);
|
||||
};
|
||||
|
||||
const handleExport = (preset: Preset<T>) => {
|
||||
const data = { ...preset, id: undefined };
|
||||
const filename = `${preset.name}.preset.json`;
|
||||
downloadJson(data, filename);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<NewPresetDialog
|
||||
previewEntries={previewEntries}
|
||||
onSave={presets.createFromCurrentState}
|
||||
currentState={currentState}
|
||||
/>
|
||||
<JsonFileInput onJson={presets.import} icon="lu:Upload">
|
||||
Import Preset from File
|
||||
</JsonFileInput>
|
||||
{presets.get().map((preset) => {
|
||||
const isLatest = presets.isLatest(preset);
|
||||
return (
|
||||
<PresetCard
|
||||
key={preset.id}
|
||||
preset={preset}
|
||||
onOverwrite={handleOverwritePreset}
|
||||
onApply={applyPreset}
|
||||
onDelete={handleDeletePreset}
|
||||
onExport={isLatest ? undefined : handleExport}
|
||||
previewEntries={previewEntries}
|
||||
isReadOnly={isLatest}
|
||||
isActive={presets.isActive(preset)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{presets.defaultPreset !== undefined && (
|
||||
<PresetCard
|
||||
preset={presets.defaultPreset}
|
||||
onOverwrite={handleOverwritePreset}
|
||||
onApply={applyPreset}
|
||||
onDelete={handleDeletePreset}
|
||||
previewEntries={previewEntries}
|
||||
isReadOnly
|
||||
hideDate
|
||||
isActive={presets.isActive(presets.defaultPreset)}
|
||||
/>
|
||||
)}
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type Props = {
|
||||
step?: number;
|
||||
valueSchema?: z.ZodType<number>;
|
||||
inverted?: boolean;
|
||||
confirmation?: string;
|
||||
renderValue: (value: number) => string;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
@@ -96,6 +97,7 @@ export function EditValue({
|
||||
maxLabel,
|
||||
minSlider,
|
||||
maxSlider,
|
||||
confirmation,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const defaultOrZero = defaultValue ?? 0;
|
||||
@@ -252,10 +254,20 @@ export function EditValue({
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.handleSubmit((data) => {
|
||||
onChange?.(data.value);
|
||||
setOpen(false);
|
||||
setNumpadExtended(false);
|
||||
resetInput();
|
||||
if (confirmation) {
|
||||
if (window.confirm(confirmation)) {
|
||||
onChange?.(data.value);
|
||||
setOpen(false);
|
||||
setNumpadExtended(false);
|
||||
resetInput();
|
||||
}
|
||||
// else do nothing if user cancels
|
||||
} else {
|
||||
onChange?.(data.value);
|
||||
setOpen(false);
|
||||
setNumpadExtended(false);
|
||||
resetInput();
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "@tanstack/react-router";
|
||||
import { ReactNode } from "react";
|
||||
import { IconName } from "@/components/Icon";
|
||||
import React from "react";
|
||||
import { LoadingSpinner } from "@/components/LoadingSpinner";
|
||||
@@ -27,6 +27,10 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:AudioWaveform";
|
||||
case "W":
|
||||
return "lu:Zap";
|
||||
case "V":
|
||||
return "lu:Zap";
|
||||
case "A":
|
||||
return "lu:Zap";
|
||||
default:
|
||||
return "lu:ChartNoAxesColumn";
|
||||
}
|
||||
@@ -56,6 +60,10 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "mHz";
|
||||
case "W":
|
||||
return "W";
|
||||
case "V":
|
||||
return "V";
|
||||
case "A":
|
||||
return "A";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -105,6 +113,12 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "meters/minute";
|
||||
case "mHz":
|
||||
return "millihertz";
|
||||
case "W":
|
||||
return "watts";
|
||||
case "V":
|
||||
return "volts";
|
||||
case "A":
|
||||
return "amperes";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -122,6 +136,8 @@ export const units = [
|
||||
"m/min",
|
||||
"mHz",
|
||||
"W",
|
||||
"V",
|
||||
"A",
|
||||
] as const;
|
||||
|
||||
export type Unit = (typeof units)[number];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
import {
|
||||
processMarkdownImages,
|
||||
createImageImports,
|
||||
} from "@/utils/markdownImages";
|
||||
} from "@/lib/markdown/images";
|
||||
|
||||
/**
|
||||
* Hook for processing markdown content with image imports
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// 30 FPS is about 33.3333 ms between refreshes
|
||||
const FPS_30: number = 1000 / 30;
|
||||
export { FPS_30 };
|
||||
@@ -0,0 +1,12 @@
|
||||
export function downloadJson(data: any, filename: string) {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export function deepEquals<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a || typeof a !== "object" || !b || typeof b !== "object") {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEquals(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
const aRec = a as Record<string, any>;
|
||||
const bRec = b as Record<string, any>;
|
||||
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of aKeys) {
|
||||
if (!(key in bRec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!deepEquals(aRec[key], bRec[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { machineIdentification } from "@/machines/types";
|
||||
import { z } from "zod";
|
||||
|
||||
export type PresetSchema = z.ZodTypeAny;
|
||||
|
||||
export const presetSchema = <S extends PresetSchema>(dataSchema: S) =>
|
||||
z.object({
|
||||
id: z.number().int().nonnegative().optional(),
|
||||
name: z.string().nonempty(),
|
||||
lastModified: z.coerce.date(),
|
||||
machineIdentification,
|
||||
schemaVersion: z.number().int().positive(),
|
||||
data: dataSchema,
|
||||
});
|
||||
|
||||
export type Preset<S extends PresetSchema> = z.infer<
|
||||
ReturnType<typeof presetSchema<S>>
|
||||
>;
|
||||
|
||||
export type PresetData<S extends PresetSchema> = z.infer<S>;
|
||||
@@ -0,0 +1,186 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, PersistStorage, StorageValue } from "zustand/middleware";
|
||||
import { Preset, PresetSchema, presetSchema } from "./preset";
|
||||
import { MachineIdentification } from "@/machines/types";
|
||||
import { z } from "zod";
|
||||
import { toastError } from "@/components/Toast";
|
||||
|
||||
const persistedPresetSchema = presetSchema(z.any()).extend({
|
||||
id: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export type PersistedPreset = z.infer<typeof persistedPresetSchema>;
|
||||
|
||||
const persistedStateSchema = z.object({
|
||||
presets: z.array(persistedPresetSchema),
|
||||
latestPresetIds: z.map(z.string(), z.number()),
|
||||
});
|
||||
|
||||
type PersistedState = z.infer<typeof persistedStateSchema>;
|
||||
|
||||
const localStoreItemSchema = z.object({
|
||||
state: z.object({
|
||||
presets: z.array(z.any()).default([]),
|
||||
latestPresetIds: z.array(z.tuple([z.string(), z.number()])).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
type LocalStoreItem = z.infer<typeof localStoreItemSchema>;
|
||||
|
||||
type PresetStoreData = {
|
||||
presets: PersistedPreset[];
|
||||
latestPresetIds: Map<string, number>;
|
||||
};
|
||||
|
||||
export type PresetStore = PresetStoreData & {
|
||||
insert: <T extends PresetSchema>(preset: Preset<T>) => PersistedPreset;
|
||||
update: <T extends PresetSchema>(preset: Preset<T>) => void;
|
||||
remove: <T extends PresetSchema>(preset: Preset<T>) => void;
|
||||
getById: (id: number) => PersistedPreset | undefined;
|
||||
|
||||
setLatestPresetId: (
|
||||
machineIdentification: MachineIdentification,
|
||||
id: number,
|
||||
) => void;
|
||||
getLatestPresetId: (
|
||||
machineIdentification: MachineIdentification,
|
||||
) => number | undefined;
|
||||
};
|
||||
|
||||
const storage: PersistStorage<PersistedState> = {
|
||||
getItem: (name: string): StorageValue<PersistedState> | null => {
|
||||
const str = localStorage.getItem(name);
|
||||
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(str);
|
||||
const { state: item } = localStoreItemSchema.parse(json);
|
||||
|
||||
const latestPresetIds = new Map(item.latestPresetIds);
|
||||
|
||||
const state: PersistedState = {
|
||||
presets: item.presets,
|
||||
latestPresetIds,
|
||||
};
|
||||
|
||||
return { state };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
setItem: (name: string, newValue: StorageValue<PersistedState>) => {
|
||||
const latestPresetIds = Array.from(
|
||||
newValue.state?.latestPresetIds?.entries(),
|
||||
);
|
||||
|
||||
const item: LocalStoreItem = {
|
||||
state: {
|
||||
...newValue.state,
|
||||
latestPresetIds,
|
||||
},
|
||||
};
|
||||
|
||||
const json = JSON.stringify(item);
|
||||
|
||||
localStorage.setItem(name, json);
|
||||
},
|
||||
|
||||
removeItem: (name: string) => localStorage.removeItem(name),
|
||||
};
|
||||
|
||||
export const usePresetStore = create<PresetStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
presets: [],
|
||||
latestPresetIds: new Map(),
|
||||
|
||||
insert: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const { presets } = state;
|
||||
const ids = presets.map((p: PersistedPreset) => p.id);
|
||||
|
||||
const id = Math.max(0, ...ids) + 1;
|
||||
|
||||
const persistedPreset: PersistedPreset = { ...preset, id };
|
||||
presets.push(persistedPreset);
|
||||
|
||||
set({ ...state, presets });
|
||||
|
||||
return persistedPreset;
|
||||
},
|
||||
|
||||
update: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const presets: PersistedPreset[] = state.presets.map(
|
||||
(p: PersistedPreset) =>
|
||||
p.id === preset.id ? (preset as PersistedPreset) : p,
|
||||
);
|
||||
set({ ...state, presets });
|
||||
},
|
||||
|
||||
remove: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const presets = state.presets.filter(
|
||||
(p: Preset<any>) => p.id !== preset.id,
|
||||
);
|
||||
set({ ...state, presets });
|
||||
},
|
||||
|
||||
getById: (id: number) => {
|
||||
return get().presets.find((preset: Preset<any>) => preset.id === id);
|
||||
},
|
||||
|
||||
setLatestPresetId: (
|
||||
machineIdentification: MachineIdentification,
|
||||
id: number,
|
||||
) => {
|
||||
const state = get();
|
||||
const { latestPresetIds } = state;
|
||||
const key = `${machineIdentification.vendor}:${machineIdentification.machine}`;
|
||||
latestPresetIds.set(key, id);
|
||||
set({ ...state, latestPresetIds });
|
||||
},
|
||||
|
||||
getLatestPresetId: (machineIdentification: MachineIdentification) => {
|
||||
const state = get();
|
||||
const { latestPresetIds } = state;
|
||||
const key = `${machineIdentification.vendor}:${machineIdentification.machine}`;
|
||||
return latestPresetIds.get(key);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "preset-storage",
|
||||
|
||||
merge: (persisted: any, store: PresetStore): PresetStore => {
|
||||
if (!persisted) {
|
||||
return store;
|
||||
}
|
||||
|
||||
try {
|
||||
const persistedState = persistedStateSchema.parse(persisted);
|
||||
|
||||
return {
|
||||
...store,
|
||||
...persistedState,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastError(
|
||||
`Loading presets failed`,
|
||||
`Could not load presets from storage.`,
|
||||
);
|
||||
}
|
||||
|
||||
return store;
|
||||
},
|
||||
|
||||
storage,
|
||||
},
|
||||
),
|
||||
);
|
||||