diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 399858af..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1,3 +0,0 @@
-.png filter=lfs diff=lfs merge=lfs -text
-.jpg filter=lfs diff=lfs merge=lfs -text
-.jpeg filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..78e024c7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,40 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: 'BUG: '
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Navigate to '...'
+2. Select/Press '....'
+3. Execute operation '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots/Photos**
+If applicable, add screenshots from the HMI display or photos of the machine screen to help explain your problem.
+
+**Machine Information (please complete the following):**
+ - Machine Type: [Winder2, Laser, Aquapath...]
+ - Control Software Version: [e.g. 2.4.1]
+ - Settings: [e.g. Different Gear Ratios, PID Settings, …]
+
+**Device Information (if issue occurs on external device):**
+ - Device Type: [e.g. Tablet, PLC, Remote Control Panel]
+ - OS: [e.g. Windows 10, Linux, etc.]
+ - Connection Type: [e.g. Ethernet, USB]
+
+**Error Messages/Codes**
+If applicable, provide exact error codes or messages displayed or found in the troubleshooting terminal.
+
+**Additional context**
+Add any other context about the problem here (e.g., recent software updates, environmental factors).
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..a352136f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,33 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'Feat: '
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. During operation, I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions, workarounds, or features you've considered.
+
+**Use Case/Application:**
+ - Machine Type(s): [e.g. All machines, Winder, Aquapath, ...]
+ - Frequency of Use: [e.g. Daily, During setup, Occasionally]
+
+**Expected Benefits:**
+ - Efficiency Improvement: [e.g. Reduces setup time by X minutes, Eliminates manual steps]
+ - Safety Enhancement: [e.g. Prevents operator error, Adds safety checks]
+ - Production Impact: [e.g. Increases throughput, Reduces downtime, Improves quality]
+ - User Experience: [e.g. Simplifies workflow, Reduces training time]
+
+**Mockups/Sketches**
+If applicable, add sketches, mockups, or photos showing how the feature would appear on the HMI display or integrate with the machine.
+
+**Additional context**
+Add any other context, industry standards, or examples from other control systems about the feature request here.
diff --git a/.github/workflows/update-npm-hash.yml b/.github/workflows/update-npm-hash.yml
new file mode 100644
index 00000000..cbf4b52c
--- /dev/null
+++ b/.github/workflows/update-npm-hash.yml
@@ -0,0 +1,80 @@
+name: Update npm deps hash
+
+on:
+ pull_request:
+ paths:
+ - 'electron/package-lock.json'
+ - 'electron/package.json'
+ workflow_dispatch:
+
+env:
+ NIX_PATH: nixpkgs=channel:nixos-25.05
+
+permissions:
+ contents: write
+ pull-requests: write
+ actions: write
+
+jobs:
+ update-hash:
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.head.repo.full_name == github.repository
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Install Nix
+ uses: nixbuild/nix-quick-install-action@v34
+ with:
+ nix_conf: |
+ keep-env-derivations = true
+ keep-outputs = true
+
+ - name: Calculate new npm deps hash
+ id: calc-hash
+ run: |
+ # Calculate the hash using nix run with nixpkgs
+ NEW_HASH=$(nix run 'nixpkgs#prefetch-npm-deps' electron/package-lock.json)
+ echo "hash=$NEW_HASH" >> $GITHUB_OUTPUT
+ echo "Calculated hash: $NEW_HASH"
+
+ - name: Update npmDepsHash in electron.nix
+ run: |
+ NEW_HASH="${{ steps.calc-hash.outputs.hash }}"
+ sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$NEW_HASH\";|" nixos/packages/electron.nix
+
+ - name: Check if hash changed
+ id: check-changes
+ run: |
+ if git diff --quiet nixos/packages/electron.nix; then
+ echo "changed=false" >> $GITHUB_OUTPUT
+ echo "No changes needed - hash is already correct"
+ else
+ echo "changed=true" >> $GITHUB_OUTPUT
+ echo "Hash updated in electron.nix"
+ fi
+
+ - name: Commit and push if changed
+ if: steps.check-changes.outputs.changed == 'true'
+ run: |
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
+ git add nixos/packages/electron.nix
+ git commit -m "chore: update npmDepsHash to ${{ steps.calc-hash.outputs.hash }}"
+ git push
+
+ - name: Comment on PR
+ if: steps.check-changes.outputs.changed == 'true'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: 'Automatically updated `npmDepsHash` to `${{ steps.calc-hash.outputs.hash }}`\n\n**Next step:** Trigger the checks by pushing an empty commit:\n```bash\ngit commit --allow-empty -m "trigger ci" && git push\n```'
+ })
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index e69de29b..00000000
diff --git a/Cargo.lock b/Cargo.lock
index 7d4fcca9..d14e02ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -204,7 +204,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -221,7 +221,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -232,9 +232,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
-version = "0.8.7"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
+checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"axum-macros",
@@ -291,7 +291,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -390,9 +390,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -539,7 +539,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -682,7 +682,7 @@ dependencies = [
"deluxe-macros",
"once_cell",
"proc-macro2",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -695,7 +695,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -710,7 +710,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -721,7 +721,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -962,7 +962,7 @@ dependencies = [
"deluxe",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -1011,14 +1011,14 @@ dependencies = [
"criterion",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
name = "euclid"
-version = "0.22.11"
+version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
+checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63"
dependencies = [
"num-traits",
]
@@ -1875,7 +1875,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -1905,7 +1905,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2218,7 +2218,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2248,20 +2248,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
name = "serde_json"
-version = "1.0.145"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
- "ryu",
"serde",
"serde_core",
+ "zmij",
]
[[package]]
@@ -2571,9 +2571,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.111"
+version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -2625,7 +2625,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2685,7 +2685,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2847,7 +2847,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2884,9 +2884,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.20"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"chrono",
"matchers",
@@ -3078,7 +3078,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"wasm-bindgen-shared",
]
@@ -3143,7 +3143,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3154,7 +3154,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3430,5 +3430,11 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
+
+[[package]]
+name = "zmij"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
diff --git a/README.md b/README.md
index 14acc03f..cb92a47d 100644
--- a/README.md
+++ b/README.md
@@ -2,109 +2,67 @@
# QiTech Control
-QiTech Control is an open-source framework designed to bring modern software paradigms to certified industrial hardware.
+QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
It frees developers from proprietary, license-heavy PLC ecosystems and rigid "point-and-click" workflows that are no longer adequate for today's complex automation challenges.
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
+## Documentation
-[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
-*Click here to watch a video demo of our software.*
+**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
-[](https://youtu.be/55egCAkQgyM)
-*Click here to watch a full explainer Video of our Software.*
+- [Getting Started](https://github.com/qitechgmbh/control/wiki/Getting-Started)
+- [Architecture Overview](https://github.com/qitechgmbh/control/wiki/Architecture-Overview)
+- [EtherCAT Basics](https://github.com/qitechgmbh/control/wiki/Ethercat-Basics)
+- [Device Examples](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)
+- [REST API Reference](https://github.com/qitechgmbh/control/wiki/Rest-Api)
-# Repo Structure
+## Videos
-**Frontend**
+| Software Demo | Full Explainer |
+|---------------|----------------|
+| [](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click to watch a demo of the control software in action* | [](https://youtu.be/55egCAkQgyM)
*Click to watch a complete overview of QiTech Control* |
-- `/electron`: Frontend code for the control software built with React and Electron.
+## Repository Structure
-**Backend**
+- **`/electron`** - React + Electron frontend
+- **`/server`** - Rust backend implementing machine logic
+- **`/ethercat-hal`** - Hardware abstraction layer for EtherCAT devices
+- **`/control-core`** - Core control logic
+- **`/nixos`** - Custom Linux OS with realtime kernel
-- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
-- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
-- `/ethercat-hal-derive`: Macros for `ethercat-hal`
-- `/control-core`: Core control logic for the server.
+## Technology Stack
-**Operating System**
+**Backend:** Rust with [Ethercrab](https://github.com/ethercrab-rs/ethercrab) for EtherCAT, [SocketIO](https://socket.io/) for real-time communication, [axum](https://docs.rs/axum/latest/axum/) for REST API
-- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
-
-**Other**
-
-- `/docs`: Documentation for the project.
-
-# Technology Choices
-
-## Backend
-
-To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master software. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
-
-[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
-
-We use [Smol](https://github.com/smol-rs/smol) for EtherCAT IO in the control loop for it's performance and [Tokio](https://tokio.rs/) for server IO because of it's ecosystem and maturity.
-
-## Frontend
-
-We could combine the code of the frontend and backend using [Doxius](https://dioxuslabs.com/) but it lacks good Linux support. We chose [Electron](https://www.electronjs.org/) with [React](https://react.dev/) for it's maturity and ecosystem. For the UI we use [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) for styling. For routing we use [TanStack Router](https://tanstack.com/router/v1).
-
-# Dev Setup
-
-[Developer Documentation](./docs/developer-docs/)
-
-[API Documentation](https://github.com/qitechgmbh/control/blob/master/docs/rest-api.md)
-
--> Backend
-- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
-- `rust-analyzer` extension for VSCode
-- Set your interface in `server/src/ethercat/init.rs` like `en10`
-- Connect a Beckhoff EK1100 to your interface
-- run `cd server && cargo run` to start the server (localhost:3001)
-
--> Frontend
-- nodejs and npm installed
-- run `cd electron && npm i && npm run start` to start the frontend
+**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
-## Minimal Hardware Examples
+## Hardware Examples
-For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples).
+QiTech Control supports a wide range of EtherCAT hardware from WAGO and Beckhoff. We provide complete step-by-step tutorials with wiring diagrams and software configuration for many common modules.
-| Example | Vendor | Hardware | Description | How-To Video | Docs |
-| :----------------- | :------- | :------------ | :-------------------------------------- | :----------- | :------------------------------------------------------------ |
-| **Digital Input** | WAGO | 750-402 | 4-Channel Digital Input | [ ] | [Docs](./docs/developer-docs/minimal-example-wago750-402.md) |
-| **Digital Output** | WAGO | 750-753 | 8-Channel Digital Output | [ ] | - |
-| **Analog Input** | WAGO | 750-455 | 4-Channel Analog Input | [ ] | [Docs](./docs/developer-docs/minimal-example-wago-750-455.md)|
-| **Analog Output** | WAGO | 750-553 | 4-Channel Analog Output | [ ] | - |
-| **Serial Comms** | WAGO | 750-652 | RS-485 Modbus Module | [ ] | - |
-| **Stepper Drive** | WAGO | 750-67x | Stepper Controller | [ ] | - |
-| **Power Supply** | WAGO | 2787-214 | Power Supply Unit | [ ] | - |
-| **LED Control** | Beckhoff | EL2004 | Digital output control (simplest setup) | [ ] | [Docs](./docs/developer-docs/minimal-example-el2004.md) |
-| **Analog Input** | Beckhoff | EL3021 | Reading analog current measurements | [ ] | [Docs](./docs/developer-docs/minimal-example-el3021.md) |
+**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
-# QiTech Machines
+Quick links to popular examples:
+- [EL2004 - LED Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El2004) (simplest setup for beginners)
+- [EL3021 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El3021)
+- [EL7031 - Stepper Motor Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El7031-Motor)
+- [WAGO 750-402 - Digital Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago750-402)
+- [WAGO 750-455 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago-750-455)
-| Machine Name | Description | Machine ID | Implemented | Video | Docs |
-| :---------------- | :-------------------------- | :--------- | :---------- | :-------------------------------------------------------------- | :------------------------------------ |
-| Winder V1 | Winding Filaments & Similar | 1 | Legacy | [Video](https://youtu.be/4aE4cFhioKA?si=Xdj_LVnFrAYWnLm6) | - |
-| Winder V2 | Winding Filaments & Similar | 2 | Yes | [Video](https://youtu.be/f2kzh6kpQWE?si=HEYcLOaC9gWSp2Wo) | - |
-| XL Winder V1 | Large Scale Winder | 3 | Yes | [Video](https://youtu.be/ynI6ioWIQQY?si=4EDVKXbgIqrvGGtY) | - |
-| Buffer V1 | Filament buffering system | 4 | In Progress | [Video](https://youtu.be/VR5mEdZDPA0?si=eip7dQXU4zaXK7Ev) | - |
-| Extruder V1 | Single Screw Extruder | 5 | Legacy | [Video](https://youtu.be/gchoG-yGczI?si=bEM4daf5eOVaU5_2) | - |
-| Extruder V2 | Single Screw Extruder | 6 | Yes | [Video](https://youtu.be/mexRYGDNWa4?si=h7JBz_XKMwKLPenG) | - |
-| Waterway V1 | Filament Water Cooling | 7 | In Progress | [Video](https://youtu.be/_T5z1J8bl_k?si=uOC1hIQ1EAP0cHIF) | - |
-| Laser V1 | Diameter Measuring Laser | 8 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | [Docs](./docs/machines/laser-DRE.md) |
-| 2-Axis-Laser V1 | Dual Axis Laser | 9 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | - |
-| Mock | Mock Machine for Testing | 10 | Yes | | - |
-| Extruder V3 | Single Screw Extruder | 11 | Yes | [Video](https://youtu.be/ipHHuPzCvn4?si=hkZ1b93rVuwDxhFD) | - |
-| Mini Schredder V1 | mini plastic crusher | 12 | Legacy | [Video](https://youtu.be/m8NplNqdu2Q?si=x6zjDktJtpbSMSmu) | - |
-| Pro Schredder V1 | large plastic crusher | 13 | Legacy | [Video](https://youtu.be/pSaVMqp06pU?si=y61enutRpxKscBm4) | - |
-| Dryer V1 | polymer dryer | 14 | Reserved | [Video](https://youtu.be/6hdmUUAdZp0?si=eKOW1WlgkqTpnfdS) | - |
-| Pelletizier V1 | Filament chopper | 15 | Reserved | | - |
+## QiTech Machines
-# Current Restructuring Efforts
+QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
-[](https://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
-*Video: Live stream of software dev meeting*
+**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
+
+
+## Contributing
+
+This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
+
+## License
+
+See [LICENSE](LICENSE) file for details.
diff --git a/control-core/Cargo.toml b/control-core/Cargo.toml
index ecb72df0..21203230 100644
--- a/control-core/Cargo.toml
+++ b/control-core/Cargo.toml
@@ -9,7 +9,7 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
-axum = "0.8.6"
+axum = "0.8.8"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
diff --git a/electron/package-lock.json b/electron/package-lock.json
index d3ab8f9a..80b01511 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "qitech-control-electron",
- "version": "1.0.0",
+ "version": "2.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qitech-control-electron",
- "version": "1.0.0",
+ "version": "2.4.1",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
diff --git a/electron/package.json b/electron/package.json
index 521cb0ce..9f5c9bd2 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -1,8 +1,8 @@
{
"name": "qitech-control-electron",
"productName": "QiTech Control",
- "version": "1.0.0",
- "description": "",
+ "version": "2.4.1",
+ "description": "Next Generation Machine Interface ",
"main": ".vite/build/main.js",
"private": true,
"scripts": {
diff --git a/ethercat-hal/src/devices/el5152.rs b/ethercat-hal/src/devices/el5152.rs
index eac5f6fa..66797d39 100644
--- a/ethercat-hal/src/devices/el5152.rs
+++ b/ethercat-hal/src/devices/el5152.rs
@@ -303,6 +303,7 @@ impl EL5152ChannelConfiguration {
device
.sdo_write(base_index, 0x11, self.frequency_window)
.await?;
+
device
.sdo_write(base_index, 0x13, self.frequency_scaling)
.await?;
diff --git a/hooks/README.md b/hooks/README.md
deleted file mode 100644
index 3d2296a1..00000000
--- a/hooks/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Git Hooks
-
-This directory contains Git hooks for the project.
-
-## Installation
-
-To install the hooks, run the following command from the repository root:
-
-```bash
-# Copy the pre-commit hook to your local .git/hooks directory
-cp hooks/pre-commit .git/hooks/pre-commit
-chmod +x .git/hooks/pre-commit
-```
\ No newline at end of file
diff --git a/hooks/pre-commit b/hooks/pre-commit
deleted file mode 100755
index ceb5b0d6..00000000
--- a/hooks/pre-commit
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-#
-# Pre-commit hook to generate Rust documentation
-#
-# This hook runs the rustdoc.sh script to update documentation
-# before each commit.
-
-echo "Running rustdoc generation..."
-
-# Get the repository root
-REPO_ROOT="$(git rev-parse --show-toplevel)"
-
-# iteralte all directories thst contain a Cargo.toml file
-for dir in $(find "$REPO_ROOT" -name "Cargo.toml" -exec dirname {} \;); do
- echo "Generating documentation in $dir"
-
- # cargotoml conatains workspace members, skip this directory
- # HAS_MEMBERS=$(grep -c '\[workspace\]' "$dir/Cargo.toml")
- # if [ "$HAS_MEMBERS" -gt 0 ]; then
- # echo "Skipping workspace directory $dir"
- # continue
- # fi
-
- # get package name from Cargo.toml using cargo metadata
- PACKAGE_NAME=$(cargo metadata --format-version 1 --no-deps --manifest-path "$dir/Cargo.toml" | jq -r '.packages[0].name')
- if [ -z "$PACKAGE_NAME" ]; then
- echo "Could not determine package name for $dir"
- continue
- fi
-
- # go into the directory
- cd "$dir" || continue
-
- # generate the documentation using rustdoc-md
- cargo install rustdoc-md --git https://github.com/qitechgmbh/rustdoc-md --rev 6dc34f9c9d878799d9c1b4f2d2bcbb1f2757ede7
- RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="-Z unstable-options --output-format json" cargo doc --no-deps
- rustdoc-md --path "${REPO_ROOT}/target/doc/${PACKAGE_NAME}.json" --output ./rustdoc.md
-
- # stage the generated documentation file
- git add "rustdoc.md"
-
- # go back to the repository root
- cd "$REPO_ROOT" || exit
-done
-
-git rm --cached rustdoc.md 2>/dev/null
-rm rustdoc.md
-
-exit 0
diff --git a/machines/Cargo.toml b/machines/Cargo.toml
index cf2a1a57..9a08a68b 100644
--- a/machines/Cargo.toml
+++ b/machines/Cargo.toml
@@ -15,13 +15,13 @@ anyhow = "1.0.100"
bitvec = "1.0.1"
lazy_static = "1.5.0"
-euclid = "0.22.11"
+euclid = "0.22.13"
# concurrency
smol = "2.0.2"
# web
-serde_json = "1.0.143"
+serde_json = "1.0.149"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
# serial
diff --git a/nixos/packages/electron.nix b/nixos/packages/electron.nix
index 7ab0ac75..250f2698 100644
--- a/nixos/packages/electron.nix
+++ b/nixos/packages/electron.nix
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
- npmDepsHash = "sha256-XFDCYY4/9GKssEnGhnrmBT3kZ/qTR2XPD+ofR/TcQ+k=";
+ npmDepsHash = "sha256-rnaAsSkoTRoBgLCTF8ZFpi7uUQ+8W+u3qoMxsptX6NU=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
diff --git a/server/Cargo.toml b/server/Cargo.toml
index f558d10e..09df666f 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -27,10 +27,10 @@ tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
regex = "1.11.3"
# web
-serde_json = "1.0.143"
+serde_json = "1.0.149"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
-axum = { version = "0.8.6", features = ["macros"] }
+axum = { version = "0.8.8", features = ["macros"] }
# serial
serialport = "4.7.3"
@@ -38,7 +38,7 @@ serialport = "4.7.3"
#tracing
tracing = { version = "0.1.44", features = ["attributes"] }
tracing-journald = { version = "0.3.1", optional = true }
-tracing-subscriber = { version = "0.3.20", features = [
+tracing-subscriber = { version = "0.3.22", features = [
"env-filter",
"chrono",
"registry",
diff --git a/server/src/ethercat/init.rs b/server/src/ethercat/init.rs
index 1ecce41e..1c5c5cc1 100644
--- a/server/src/ethercat/init.rs
+++ b/server/src/ethercat/init.rs
@@ -1,12 +1,7 @@
+use crate::metrics::io::set_ethercat_iface;
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::Timer;
-use std::{sync::Arc, time::Duration};
-
-use crate::{
- app_state::SharedState,
- ethercat::{ethercat_discovery_info::send_ethercat_found, setup::setup_loop},
- metrics::io::set_ethercat_iface,
-};
+use std::time::Duration;
pub async fn find_ethercat_interface() -> String {
loop {
@@ -23,22 +18,3 @@ pub async fn find_ethercat_interface() -> String {
}
}
}
-
-pub async fn start_interface_discovery(app_state: Arc) {
- let interface = find_ethercat_interface().await;
-
- tracing::info!("Calling setup_loop");
- let res = setup_loop(&interface, app_state.clone()).await;
- match res {
- Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
- Err(e) => {
- tracing::error!(
- "[{}::main] Failed to initialize EtherCAT network \n{:?}",
- module_path!(),
- e
- );
- }
- }
-
- send_ethercat_found(app_state, &interface).await;
-}
diff --git a/server/src/ethercat/setup.rs b/server/src/ethercat/setup.rs
index f3dd3678..e97acc56 100644
--- a/server/src/ethercat/setup.rs
+++ b/server/src/ethercat/setup.rs
@@ -24,7 +24,9 @@ use smol::stream::StreamExt;
use ta::Next;
use ta::indicators::ExponentialMovingAverage;
+#[cfg(not(feature = "development-build"))]
use crate::utils::stop_dnsmasq;
+
use ethercrab::std::ethercat_now;
use ethercrab::{
DcSync, MainDevice, MainDeviceConfig, PduStorage, RegisterAddress, RetryBehaviour,
@@ -178,17 +180,20 @@ pub async fn set_ethercat_devices,
) -> Result {
tracing::info!("Starting Ethercat PDU loop");
- const TICK_INTERVAL: Duration = Duration::from_micros(700);
- let res = stop_dnsmasq();
- match res {
- Ok(_) => (),
- Err(e) => tracing::error!("Failed to stop dnsmasq: {:?}", e),
- };
- // Small Timeout to ensure interfaces get released
- smol::Timer::after(Duration::from_millis(1500)).await;
+ #[cfg(not(feature = "development-build"))]
+ {
+ let res = stop_dnsmasq();
+ match res {
+ Ok(_) => tracing::info!("Stopped dnsmasq"),
+ Err(e) => tracing::error!("Failed to stop dnsmasq: {:?}", e),
+ };
+ // Small Timeout to ensure interfaces get released
+ smol::Timer::after(Duration::from_millis(1500)).await;
+ }
// Setup ethercrab tx/rx task
let pdu_storage = Box::leak(Box::new(PduStorage::::new()));
@@ -237,7 +242,7 @@ pub async fn setup_loop(
pdu,
Timeouts {
// Default 5000ms
- state_transition: Duration::from_millis(5000),
+ state_transition: Duration::from_millis(10000),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
@@ -287,11 +292,6 @@ pub async fn setup_loop(
};
for mut subdevice in group_preop.iter_mut(&maindevice) {
- println!(
- "subdevice {} {:X?}",
- subdevice.name(),
- subdevice.configured_address()
- );
if subdevice.name() == "EL4002" {
// Sync mode 01 = SM Synchronous, says it does dc but actually doesnt, thx?
subdevice
@@ -308,7 +308,7 @@ pub async fn setup_loop(
.expect("Set sync mode");
subdevice
- .sdo_write(0x1c32, 0x02, TICK_INTERVAL.as_nanos() as u32)
+ .sdo_write(0x1c32, 0x02, cycle_target.as_nanos() as u32)
.await
.expect("Set cycle time");
@@ -472,14 +472,17 @@ pub async fn setup_loop(
.await?;
// Avoid potential Sdo write Contention on the bus, by just waiting a bit
+ // Timeout is not enough ... or rather a monstrous timeout would be required for all of the sdo writes we do ...
+ // TODO: Keep small timeout here and add Complete Access configuration for the stepper terminals and complex trerminals like (el5152)
smol::Timer::after(Duration::from_millis(4000)).await;
+
let mut now = Instant::now();
let start = Instant::now();
let mut averages = Vec::new();
for _ in 0..group_preop.len() {
averages.push(ExponentialMovingAverage::new(64).unwrap());
}
- let mut tick_interval = smol::Timer::interval(TICK_INTERVAL);
+ let mut tick_interval = smol::Timer::interval(cycle_target);
tracing::info!("Moving into PRE-OP with PDI");
let group = group_preop.into_pre_op_pdi(&maindevice).await?;
@@ -537,9 +540,9 @@ pub async fn setup_loop(
// Start SYNC0 100ms in the future
start_delay: Duration::from_millis(100),
// SYNC0 period should be the same as the process data loop in most cases
- sync0_period: TICK_INTERVAL,
+ sync0_period: cycle_target,
// Send process data half way through cycle
- sync0_shift: TICK_INTERVAL / 2,
+ sync0_shift: cycle_target / 2,
},
)
.await?;
diff --git a/server/src/loop.rs b/server/src/loop.rs
index ca709283..4ece463b 100644
--- a/server/src/loop.rs
+++ b/server/src/loop.rs
@@ -90,7 +90,6 @@ pub fn start_loop_thread(
match msg {
HotThreadMessage::NoMsg => {}
HotThreadMessage::AddEtherCatSetup(ethercat_setup) => {
- println!("EthercatSetup: {:?}", ethercat_setup.devices);
rt_loop_inputs.ethercat_setup = Some(Box::new(ethercat_setup));
}
HotThreadMessage::WriteMachineDeviceInfo(info_request) => {
@@ -115,7 +114,6 @@ pub fn start_loop_thread(
.retain(|m| m.get_machine_identification_unique() != unique_id);
}
HotThreadMessage::AddMachines(machine_vec) => {
- tracing::info!("received machines{:?}", machine_vec);
for new_machine in machine_vec {
let id = new_machine.get_machine_identification_unique();
if !rt_loop_inputs
diff --git a/server/src/main.rs b/server/src/main.rs
index da44e3f3..852c6813 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -15,9 +15,11 @@ use machines::{
serial::{devices::laser::Laser, init::SerialDetection},
winder2::api::GenericEvent,
};
-use socketio::main_namespace::{
- MainNamespaceEvents, ethercat_devices_event::EthercatDevicesEventBuilder,
+use socketio::{
+ main_namespace::{MainNamespaceEvents, ethercat_devices_event::EthercatDevicesEventBuilder},
+ queue::start_socketio_queue,
};
+
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
use utils::start_dnsmasq;
@@ -37,6 +39,12 @@ use smol::{
use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
+#[cfg(feature = "development-build")]
+const CYCLE_TARGET_TIME: Duration = Duration::from_micros(2000);
+
+#[cfg(not(feature = "development-build"))]
+const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
+
#[cfg(feature = "mock-machine")]
use mock_init::init_mock;
@@ -46,7 +54,6 @@ use crate::{
setup::setup_loop,
},
modbus_tcp::start_modbus_tcp_discovery,
- socketio::queue::socketio_queue_worker,
};
#[cfg(feature = "mock-machine")]
@@ -128,10 +135,8 @@ pub async fn add_serial_device(
pub async fn start_serial_discovery(app_state: Arc) {
loop {
let devices = SerialDetection::detect_devices();
-
// This allows detection of disconnected devices
handle_serial_device_hotplug(app_state.clone(), devices).await;
-
smol::Timer::after(Duration::from_secs(1)).await;
}
}
@@ -143,33 +148,34 @@ pub async fn start_interface_discovery(
let interface = find_ethercat_interface().await;
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
set_ethercat_iface(interface.clone());
- let res = setup_loop(&interface, app_state.clone()).await;
+ let res = setup_loop(&interface, CYCLE_TARGET_TIME, app_state.clone()).await;
match res {
Ok(setup) => {
let _ = sender.send(HotThreadMessage::AddEtherCatSetup(setup)).await;
tracing::info!("Successfully initialized EtherCAT devices");
- {
- // Notify client via socketio
- let app_state_clone = app_state.clone();
- let main_namespace = &mut app_state_clone
- .socketio_setup
- .namespaces
- .write()
- .await
- .main_namespace;
- let event = EthercatDevicesEventBuilder()
- .build(app_state_clone.clone())
- .await;
- main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
- }
+ // Notify client via socketio
+ let app_state_clone = app_state.clone();
+ let main_namespace = &mut app_state_clone
+ .socketio_setup
+ .namespaces
+ .write()
+ .await
+ .main_namespace;
+ let event = EthercatDevicesEventBuilder()
+ .build(app_state_clone.clone())
+ .await;
+ main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
- let res = start_dnsmasq();
- match res {
- Ok(o) => o,
- Err(e) => tracing::error!("Failed to start dnsmasq: {:?}", e),
- };
+ #[cfg(not(feature = "development-build"))]
+ {
+ let res = start_dnsmasq();
+ match res {
+ Ok(o) => o,
+ Err(e) => tracing::error!("Failed to start dnsmasq: {:?}", e),
+ };
+ }
}
Err(e) => {
tracing::error!(
@@ -178,6 +184,7 @@ pub async fn start_interface_discovery(
e
);
// if this doesnt work, unlucky
+ #[cfg(not(feature = "development-build"))]
let _ = start_dnsmasq();
}
}
@@ -317,15 +324,6 @@ async fn handle_async_requests(recv: Receiver, shared_state:
tracing::warn!("Async handler task finished");
}
-pub async fn start_socketio_queue(app_state: Arc) {
- let app_state = app_state.as_ref();
- loop {
- let res = socketio_queue_worker(app_state).await;
- tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
- tracing::error!("Restarting SocketIO...");
- }
-}
-
#[cfg(feature = "development-build")]
fn setup_ctrlc_handler() -> Arc {
let running = Arc::new(AtomicBool::new(true));
@@ -355,8 +353,6 @@ fn main() {
#[cfg(feature = "development-build")]
let running = setup_ctrlc_handler();
- const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
-
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();
let (main_sender, main_receiver) = smol::channel::unbounded();
@@ -380,7 +376,6 @@ fn main() {
#[cfg(not(feature = "mock-machine"))]
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
-
smol::spawn(start_modbus_tcp_discovery(app_state.clone())).detach();
smol::block_on(async {
diff --git a/server/src/socketio/queue.rs b/server/src/socketio/queue.rs
index ea654431..4091e598 100644
--- a/server/src/socketio/queue.rs
+++ b/server/src/socketio/queue.rs
@@ -1,6 +1,6 @@
use crate::app_state::SharedState;
-use std::{sync::Arc, time::Instant};
-use tracing::{debug, info, instrument, trace};
+use std::sync::Arc;
+use tracing::{error, info, instrument, trace};
/// Send a single event with retry logic
#[instrument(skip_all)]
@@ -12,7 +12,7 @@ async fn send_event_with_retry(
loop {
// check if socket is still connected
if !socket.connected() {
- trace!(
+ info!(
socket_id = ?socket.id,
event = %event.name,
"Socket disconnected, skipping event"
@@ -32,7 +32,7 @@ async fn send_event_with_retry(
}
Err(e) => match e {
socketioxide::SendError::Serialize(serialize_error) => {
- trace!(
+ info!(
socket_id = ?socket.id,
event = %event.name,
error = %serialize_error,
@@ -45,7 +45,7 @@ async fn send_event_with_retry(
continue; // Retry sending the event
}
socketioxide::SocketError::Closed => {
- trace!(
+ info!(
socket_id = ?socket.id,
event = %event.name,
"Socket closed, skipping event"
@@ -58,40 +58,16 @@ async fn send_event_with_retry(
}
}
-pub async fn socketio_queue_worker(app_state: &SharedState) {
- tracing::info!("SocketIO global queue listener started");
- let mut event_count = 0;
- let mut batch_start = Instant::now();
-
- while let Ok((socket, event)) = app_state.socketio_setup.socket_queue_rx.recv().await {
- event_count += 1;
-
- send_event_with_retry(&socket, &event).await;
-
- if batch_start.elapsed().as_secs() >= 5 {
- let elapsed = batch_start.elapsed();
- if event_count > 0 {
- debug!(
- "[{}::socketio_queue_worker] Processed {} events in {:.2?} ({:.1} events/s)",
- module_path!(),
- event_count,
- elapsed,
- event_count as f64 / elapsed.as_secs_f64(),
- );
- }
- event_count = 0;
- batch_start = Instant::now();
- }
- }
-
- info!("SocketIO global queue listener stopped");
-}
-
pub async fn start_socketio_queue(app_state: Arc) {
let app_state = app_state.as_ref();
loop {
- let res = socketio_queue_worker(app_state).await;
- tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
- tracing::error!("Restarting SocketIO...");
+ let res = app_state.socketio_setup.socket_queue_rx.recv().await;
+ match res {
+ Ok((socket, event)) => send_event_with_retry(&socket, &event).await,
+ Err(e) => {
+ error!("SocketIO global queue listener stopped: {:?}", e);
+ break;
+ }
+ }
}
}