Compare commits

..
Author SHA1 Message Date
snacj dc78ab80cb Fixed Formatting 2025-10-01 14:55:14 +02:00
snacj af21d05e6a Fixed merge issues 2025-10-01 14:49:51 +02:00
snacj aa54f94e94 fixed merge issues 2025-10-01 14:14:53 +02:00
snacj e00b0d2c3e Added Buffer Logic 2025-10-01 14:13:22 +02:00
snacj b338d71874 Changed Buffer Winder Mode 2025-10-01 14:13:22 +02:00
snacj fb650216d2 Added Buffer Mode to Winder frontend 2025-10-01 14:12:45 +02:00
snacj da823197d5 Finished Homing Routine 2025-10-01 14:12:45 +02:00
snacj fe5f109c89 Fixed step converter errors 2025-10-01 14:11:53 +02:00
snacj 1d86a42fb6 Fixed Merge Conflicts 2025-10-01 14:11:11 +02:00
snacj fb8eee4b8d Resolved merge conflicts 2025-10-01 14:09:59 +02:00
snacj 24cd1d857f Implemented Buffer state for Winder 2025-10-01 14:07:38 +02:00
snacj b7c4e1939d Changed Formatting and Removed unused imports 2025-10-01 14:06:35 +02:00
snacj 7d0a683c4b Added Puller frontend functions, hooks and Controlpage for Buffer 2025-10-01 14:04:25 +02:00
snacj fde51a95c8 Refactored to clean Act function in Winder2 2025-10-01 14:00:58 +02:00
snacj d88f45f81f Implemented Buffer Business Logic 2025-10-01 13:59:51 +02:00
snacj 8e0005d447 Added start of business logic for buffer lift 2025-10-01 13:56:22 +02:00
snacj 2bfa75810f Fixed Formatting 2025-10-01 13:30:52 +02:00
snacj b86130ec0d Fixed Rebase Issues and removed unused imports 2025-10-01 12:41:34 +02:00
snacj ad9305e1fd Fixed Formatting 2025-10-01 12:33:02 +02:00
snacj 191d022270 Fixed target speed communication between buffer and winder. 2025-10-01 12:33:02 +02:00
snacj 2d3a69852f Fixed merge issue 2025-10-01 12:33:02 +02:00
snacj d145bf547e Finished Homing Routine 2025-10-01 12:33:02 +02:00
snacj ed90b253f7 Removed unused top and bottom limits 2025-10-01 12:32:48 +02:00
snacj 4e78c6c912 Removed "Traverse Bar" 2025-10-01 12:32:48 +02:00
snacj 1efebc4a27 Fixed mode 2025-10-01 12:32:48 +02:00
snacj ea7d799cfc Fixed step converter errors 2025-10-01 12:32:48 +02:00
snacj 54ac5678ad Added Capability checks for moving 2025-10-01 12:32:48 +02:00
snacj 19ebbb77ac Fixed Comments 2025-10-01 12:32:48 +02:00
snacj 4bb2a73f6c Fixed State 2025-10-01 12:32:48 +02:00
snacj 972562b1d6 Added Control Functions 2025-10-01 12:32:48 +02:00
snacj 2a68a2da8f Adjusted Values and restructured controls 2025-10-01 12:32:48 +02:00
snacj 2d7b71ef5d Added Lift State frontend 2025-10-01 12:32:48 +02:00
snacj 0398bd0242 Added Lift Control functions 2025-10-01 12:32:48 +02:00
snacj 74e83d7cf5 Added Lift State and Mutations 2025-10-01 12:32:48 +02:00
snacj 25a1d3515d Added Lift State and Mutations 2025-10-01 12:32:48 +02:00
snacj 81d4b162e9 Fixed update speed function 2025-10-01 12:32:48 +02:00
snacj a790f15db9 Added Homing Routine Functions 2025-10-01 12:32:48 +02:00
snacj 50d7373f02 Fixed Merge Conflicts 2025-10-01 12:32:48 +02:00
snacj f1cd0c6216 Resolved merge conflicts 2025-10-01 12:32:32 +02:00
snacj eb7724a756 Implemented Buffer state for Winder 2025-10-01 12:31:56 +02:00
snacj df88eded18 Updated End Switch Logic 2025-10-01 12:31:34 +02:00
snacj 503a6b493e Fixed Formatting 2025-10-01 12:31:34 +02:00
snacj 4a7bb2abfd Added Digital Input switch for end Switch (Top) at Buffer 2025-10-01 12:31:34 +02:00
snacj 12a42eb0a6 Changed Formatting and Removed unused imports 2025-10-01 12:31:34 +02:00
snacj d3981d7276 Changed Mode names for Buffer + Added Hold Mode 2025-10-01 12:30:49 +02:00
snacj 89e702275e Added Comment 2025-10-01 12:30:49 +02:00
snacj 4a1239fa27 Update sync of puller speed and buffer input speed & Added description to helper function 2025-10-01 12:30:49 +02:00
snacj bd22c39b4a Added settings page to buffer route 2025-10-01 12:30:49 +02:00
snacj 96edca9f66 Added helper function to get Connected Machine reference. --Needs testing 2025-10-01 12:30:28 +02:00
snacj 2d198e0a44 Added Puller frontend functions, hooks and Controlpage for Buffer 2025-10-01 12:30:28 +02:00
snacj 9107e1372a Implemented Puller functions in Buffer Mod 2025-10-01 12:30:01 +02:00
snacj 8268eff39b Added pullerSpeed State Event 2025-10-01 12:30:01 +02:00
snacj 63910ab5be Renamed Function 2025-10-01 12:30:01 +02:00
snacj 91fdd7eb1e Added call to winder on buffering 2025-10-01 12:30:01 +02:00
snacj 8aa86a91ff Fixed Formatting 2025-10-01 12:30:01 +02:00
snacj 6f125ff0ab Refactored to clean Act function in Winder2 2025-10-01 12:30:01 +02:00
snacj 256c032b7c Changed updating speed logic to use JerkSpeedController 2025-10-01 12:29:36 +02:00
snacj 46a794d323 Implemented Buffer Business Logic 2025-10-01 12:29:36 +02:00
snacj a18b10bf50 Updated Act function to implement puller sync and buffer input speed 2025-10-01 12:28:01 +02:00
snacj 405a1f3c2f Added Puller Speed Controller to Buffer 2025-10-01 12:28:01 +02:00
snacj 3e09a00000 Added start of business logic for buffer lift 2025-10-01 12:28:01 +02:00
snacj b4ee86a88c Added current_input_speed state to test the calculation of buffer_lift_speed 2025-10-01 12:27:15 +02:00
snacj 1eeb3f8190 Changed name buffer_tower_controller -> buffer-lift_controller 2025-10-01 12:27:15 +02:00
534 changed files with 16387 additions and 35763 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
-40
View File
@@ -1,40 +0,0 @@
---
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).
-33
View File
@@ -1,33 +0,0 @@
---
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.
-14
View File
@@ -1,14 +0,0 @@
## What does this PR do / add / change?
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
## Screenshots
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
## Checklist before opening the pr
- [ ] Tested locally
- [ ] Tested on the machine (if hardware interaction is involved)
- [ ] No format errors (`cargo fmt` / `npm run format`)
- [ ] No build errors (`cargo build` / `npm run build`)
+28 -54
View File
@@ -3,76 +3,50 @@ name: Electron
on:
push:
branches:
- master
- "**"
pull_request:
branches:
- master
permissions:
contents: read
defaults:
run:
working-directory: electron
- "*"
jobs:
build:
build-electron:
runs-on: ubuntu-latest
defaults:
run:
working-directory: electron
steps:
- &checkout
name: Checkout
uses: actions/checkout@v4
- &setup-node
name: Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
cache-dependency-path: |
electron/package-lock.json
node-version: "18" # or '20' for latest LTS
- &install
name: Install dependencies
run: npm ci
- uses: actions/checkout@v4
- name: Compile
run: tsc
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules/
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install
run: npm i
- name: Build
run: npm run build
test:
runs-on: ubuntu-latest
needs: build
steps:
- *checkout
- *setup-node
- *install
- name: Test
run: npm run test
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-node
- *install
run: tsc
- name: Format
run: npm run format
lint:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-node
- *install
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Package
run: npm run package
-213
View File
@@ -1,213 +0,0 @@
name: Stale Assigned Issue Check
on:
schedule:
# Run every Sunday at 9:00 AM UTC
- cron: '0 9 * * 0'
workflow_dispatch: # Allow manual triggering for testing
jobs:
check-stale-issues:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Check assigned issues for status updates
uses: actions/github-script@v7
with:
script: |
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const now = new Date();
// Helper function to check rate limits
async function checkRateLimit() {
try {
const { data: rateLimit } = await github.rest.rateLimit.get();
const remaining = rateLimit.resources.core.remaining;
const reset = new Date(rateLimit.resources.core.reset * 1000);
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
return remaining > 10; // Stop if fewer than 10 requests remaining
} catch (error) {
console.log(`Could not check rate limit: ${error.message}`);
return true; // Continue on error
}
}
// Get all open issues
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
console.log(`Found ${issues.length} open issues`);
// Helper function to check if issue has linked PRs using GraphQL
async function hasLinkedPRs(issueNumber) {
try {
const query = `
query($owner: String!, $repo: String!, $issueNumber: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
nodes {
... on ConnectedEvent {
subject {
... on PullRequest {
number
state
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: issueNumber
});
const linkedPRs = result.repository.issue.timelineItems.nodes
.filter(node => node.subject && node.subject.state === 'OPEN')
.map(node => node.subject.number);
return linkedPRs.length > 0;
} catch (error) {
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
return false; // Assume no linked PRs on error to avoid false positives
}
}
let processedCount = 0;
let commentedCount = 0;
let errorCount = 0;
for (const issue of issues) {
try {
// Check rate limit periodically
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
break;
}
// Skip pull requests (they appear in issues list too)
if (issue.pull_request) {
continue;
}
// Skip if not assigned
if (!issue.assignees || issue.assignees.length === 0) {
console.log(`Issue #${issue.number}: Not assigned, skipping`);
continue;
}
console.log(`Checking issue #${issue.number}: ${issue.title}`);
processedCount++;
// Check if issue has linked PRs
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
if (hasOpenLinkedPR) {
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
continue;
}
// Get all comments on the issue
let comments = [];
try {
comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
errorCount++;
continue;
}
// Find the most recent comment or update
let lastActivity = new Date(issue.created_at);
if (issue.updated_at) {
lastActivity = new Date(issue.updated_at);
}
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
const lastCommentDate = new Date(lastComment.created_at);
if (lastCommentDate > lastActivity) {
lastActivity = lastCommentDate;
}
}
// Check if last activity was more than a week ago
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
if (daysSinceActivity >= 7) {
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
// Check if we already posted a status request in the last 7 days
const recentComments = comments.filter(comment => {
const commentDate = new Date(comment.created_at);
return (now - commentDate) < ONE_WEEK_MS;
});
const alreadyAsked = recentComments.some(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('status update')
);
if (alreadyAsked) {
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
continue;
}
// Post a status request comment
try {
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Hi ${assigneeNames}! 👋\n\n` +
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
`Could you please provide a status update? If you're:\n` +
`- ✅ Working on it: Let us know your progress\n` +
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
`Thank you!`
});
console.log(`Issue #${issue.number}: Posted status request comment`);
commentedCount++;
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
errorCount++;
// Continue to next issue
}
} else {
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
}
} catch (error) {
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
errorCount++;
// Continue to next issue instead of failing the entire workflow
continue;
}
}
console.log(`\nStale issue check complete!`);
console.log(`Processed: ${processedCount} issues`);
console.log(`Commented on: ${commentedCount} issues`);
console.log(`Errors: ${errorCount}`);
-99
View File
@@ -1,99 +0,0 @@
name: Nix CI
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
contents: read
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
jobs:
electron:
runs-on: ubuntu-latest
steps:
- &checkout
name: Check out repository
uses: actions/checkout@v4
- &nix-install
uses: nixbuild/nix-quick-install-action@v34
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-electron-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './electron/package-lock.json') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
purge: true
purge-prefixes: nix-electron-${{ runner.os }}-
- name: Build Electron in Nix
run: nix build ".#packages.x86_64-linux.electron"
rust:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-server-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
purge: true
purge-prefixes: nix-server-${{ runner.os }}-
- name: Build Server in Nix
run: nix build ".#packages.x86_64-linux.server"
build:
runs-on: ubuntu-latest
needs: [electron, rust]
steps:
- *checkout
- *nix-install
- name: Use both Electron and Server Nix Store
uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock', './electron/package-lock.json') }}
# if no hit on the primary key, merge individual caches
restore-prefixes-all-matches: |
nix-electron-${{ runner.os }}-
nix-server-${{ runner.os }}-
- name: Build Flake and System Configuration
run: nix build ".#nixosConfigurations.nixos.config.system.build.toplevel" --impure
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- name: Format
run: nix run 'nixpkgs#nixfmt-classic' $(find . nixos -name '*.nix')
check:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- name: Build Flake and Configuration.nix
run: nix flake check "." --impure
+9 -51
View File
@@ -3,41 +3,27 @@ name: Rust
on:
push:
branches:
- master
- "**"
pull_request:
branches:
- master
branches: "*"
env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.86"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- &udev-install
name: Install libudev development package
- name: Install libudev development package
run: sudo apt-get update && sudo apt-get install -y libudev-dev
- &checkout
uses: actions/checkout@v4
- &setup-rust
name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
profile: minimal
override: true
components: rustfmt
- &cargo-cache
name: Cache Rust dependencies
- 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:
path: |
@@ -55,36 +41,8 @@ jobs:
RUSTFLAGS: "-D warnings"
run: cargo build --verbose
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-rust
- name: Format
run: cargo fmt --check --verbose
test:
runs-on: ubuntu-latest
steps:
- *udev-install
- *checkout
- *setup-rust
- *cargo-cache
- name: Run tests
run: cargo test --verbose
mock:
runs-on: ubuntu-latest
steps:
- *udev-install
- *checkout
- *setup-rust
- *cargo-cache
- name: Mock Machine
run: cargo build --features "development-build,mock-machine"
-80
View File
@@ -1,80 +0,0 @@
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```'
})
+1 -7
View File
@@ -1,11 +1,5 @@
target/
dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
.idea/
electron/dist-electron/
View File
Whitespace-only changes.
-96
View File
@@ -1,99 +1,3 @@
# `2.14.1`
_26.01.2026_
## Laser
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
# `2.14.0`
_22.01.2026_
## General
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
# `2.13.0`
_07.01.2026_
## General
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
## Extruder V3
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
## Laser
- [#924](https://github.com/qitechgmbh/control/pull/924) Roundness is now displayed as a percentage
- [#885](https://github.com/qitechgmbh/control/pull/885) Added Min and Max Diameter
- [#849](https://github.com/qitechgmbh/control/pull/849) Added a Warning if Filament is out of Spec
## Aquapath
- [#976](https://github.com/qitechgmbh/control/pull/976) Improved Stability
# `2.12.0`
_17.11.2025_
## General
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
## Laser
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
## Extruder V2
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
# `2.11.0`
_21.10.2025_
## General
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
## Winder V2
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
## Extruder V2
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
# `2.10.0`
_16.10.2025_
## Winder V2
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
## General
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
# `2.9.1`
_10.10.2025_
## Winder V2
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
## General
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
# `2.9.0`
_06.10.2025_
## Winder V2
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
## General
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
# `2.8.0`
_30.09.2025_
Generated
+1099 -523
View File
File diff suppressed because it is too large. Load diff
+1 -4
View File
@@ -4,11 +4,8 @@ members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"machines",
#"ethercat-eeprom-dump",
"ethercat-eeprom-dump",
"control-core-derive",
"units",
"utils",
]
exclude = []
resolver = "2"
+118 -44
View File
@@ -2,76 +2,150 @@
# QiTech Control
QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation 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.
[![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
# Repo Structure
## Documentation
Frontend
**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
- `/electron`: Frontend code for the control software built with React and Electron.
- [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)
Backend
## Videos
- `/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.
| Software Demo | Full Explainer |
|---------------|----------------|
| [![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Watch a demo of the control software in action* | [![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)<br/>*Watch a complete overview of QiTech Control* |
Operating System
## Repository Structure
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
- **`/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
Other
## Technology Stack
- `/docs`: Documentation for the project.
**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
# Technology Choices
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
## Hardware Examples
[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.
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.
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.
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
## Frontend
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)
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).
## QiTech Machines
# Dev Setup
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
[Developer Documentation](./docs/developer-docs/)
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
## 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)
## Contributing
## Frontend
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
- nodejs and npm installed
- run `cd electron && npm i && npm run start` to start the frontend
# Machines
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
## Additional Videos
# More Docs
| Episode Highlights | |
|--------------------|--------------------|
| [![](https://img.youtube.com/vi/U2wNN6sF4to/maxresdefault.jpg)](https://www.youtube.com/watch?v=U2wNN6sF4to&t=163s)<br/>**Day in the Software Team**<br/>*Behind the scenes with Robin and how the team ships control software.* | [![](https://img.youtube.com/vi/R5WGAY3WRWA/maxresdefault.jpg)](https://www.youtube.com/watch?v=R5WGAY3WRWA&t=1s)<br/>**Software Stack Day**<br/>*A fast tour through the tools, stack, and daily workflow.* |
| [![](https://img.youtube.com/vi/xyaHwxXguw4/maxresdefault.jpg)](https://www.youtube.com/watch?v=xyaHwxXguw4&t=32s)<br/>**First Client**<br/>*A milestone week building the first real client project.* | [![](https://img.youtube.com/vi/moI7z2PVdF4/maxresdefault.jpg)](https://www.youtube.com/watch?v=moI7z2PVdF4)<br/>**Steelworks Week**<br/>*A deep-dive week on a demanding steelworks order.* |
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
## License
- [x] Example Winder V2
See [LICENSE](LICENSE) file for details.
- [ ] Electron
- Folder Structure
- Routing with TanStack Router
- Design with Tailwind & Shadcn
- ...
- [ ] Interfacing with Electron/Server
- [ ] SocketIO
- Machine Namespace
- Main Namespace
- [ ] REST
- Machine Mutations
- Write Device Identification
- Server
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
- Maindevice
- Group
- Extracting Device Identifications
- Identifying Groups
- Validating Machines
- Run Control Loop
- [x] [Control Loop](./docs/control-loop.md)
- [x] [Machine/Device Identification](./docs/identification.md)
- [ ] Machines
- When to create a new Machine?
- Versioning
- Code sharing
- Creating/Validating a Machine
- Validation
- Configuration
- [ ] Machine Implementation Guide
- Link: How to create a Device
- Link: How to create an Actor
- Link: How to create a Machine
- API (SocketIO + REST)
- Creation/Validation Logic
- Optional/Mandatory Devices
- Validate Devices
- Business Logic
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
- Forward `act` in winder.
- [ ] Control Core
- [x] [Actors](./docs/actors.md)
- [ ] SocketIO
- Namespaces & Caching
- Joining leaving namespaces
- NamespaceId
- Caching
- Serverside Caching
- Clientside Caching
- [ ] REST
- [x] Ethercat HAL
- [x] [Devices](./docs/devices.md)
- [x] [Configuration (CoE)](./docs/coe.md)
- [x] [IO](./docs/io.md)
- [x] [PDO](./docs/pdo.md)
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
+4 -6
View File
@@ -15,18 +15,16 @@ if [[ "$1" == "release" ]]; then
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE --features development-build
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_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
if compgen -G "/dev/ttyUSB*" > /dev/null; then
echo "Setting permissions for serial ports"
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
echo "Setting permissions for serial ports"
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
# run
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BUILD_DIR="${BUILD_DIR:-target}"
CSV_FILE="${CSV_FILE:-compile_metrics.csv}"
DEFAULT_PKG="server"
if [ "$#" -eq 0 ]; then
set -- cargo build
fi
if [ "${1:-}" = "cargo" ] && [ "${2:-}" = "build" ]; then
cargo clean
BUILD_CMD=(cargo rustc -p "$DEFAULT_PKG")
shift 2
BUILD_CMD+=("$@")
BUILD_CMD+=(-- --emit=llvm-ir -C debuginfo=0)
else
BUILD_CMD=("$@")
fi
compile_start_ns=$(date +%s%N)
"${BUILD_CMD[@]}"
compile_end_ns=$(date +%s%N)
compile_ms=$(( (compile_end_ns - compile_start_ns) / 1000000 ))
if [ -d "$BUILD_DIR" ]; then
llvm_ir_lines=$(
find "$BUILD_DIR" -type f -name '*.ll' -print0 \
| xargs -0 cat \
| wc -l
)
else
llvm_ir_lines=0
fi
bin_path=""
if [ -n "${BINARY_PATH:-}" ] && [ -f "$BINARY_PATH" ]; then
bin_path="$BINARY_PATH"
elif [ -f "./target/release/server" ]; then
bin_path="./target/release/server"
elif [ -f "./target/debug/server" ]; then
bin_path="./target/debug/server"
elif [ -f "./server/target/release/server" ]; then
bin_path="./server/target/release/server"
elif [ -f "./server/target/debug/server" ]; then
bin_path="./server/target/debug/server"
fi
if [ -n "$bin_path" ]; then
binary_size_bytes=$(stat -c '%s' "$bin_path")
else
binary_size_bytes=0
fi
finished_timestamp_ms=$(($(date +%s%N) / 1000000))
finished_datetime=$(date -Iseconds)
if [ ! -f "$CSV_FILE" ]; then
echo "datetime,finished_timestamp_ms,compile_ms,llvm_ir_lines,binary_size_bytes" > "$CSV_FILE"
fi
echo "${finished_datetime},${finished_timestamp_ms},${compile_ms},${llvm_ir_lines},${binary_size_bytes}" \
>> "$CSV_FILE"
echo "Metrics recorded in $CSV_FILE"
+1 -6
View File
@@ -40,15 +40,10 @@ fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
}
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {}
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
fn as_any(&self) -> &dyn std::any::Any {
+6 -4
View File
@@ -9,19 +9,21 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
axum = "0.8.8"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.2"
uom = "0.36.0"
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
tracing = "0.1.44"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.8"
core_affinity = "0.8.3"
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularVelocity},
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Velocity},
velocity::meter_per_second,
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
@@ -199,8 +199,8 @@ impl AngularAccelerationPositionController {
/// # Example
/// ```ignore
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
/// use units::f64::Angle;
/// use units::angle::degree;
/// use uom::si::f64::Angle;
/// use uom::si::angle::degree;
///
/// let mut controller = AngularAccelerationPositionController::new_simple(
/// Some(Angle::new::<degree>(180.0)),
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
angular_acceleration::radian_per_second_squared,
angular_jerk::radian_per_second_cubed,
angular_velocity::radian_per_second,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -65,7 +65,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -127,7 +127,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -185,7 +185,7 @@ impl AngularJerkSpeedController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
///
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Length, Velocity},
length::meter,
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
///
/// # Example
/// ```ignore
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(10.0);
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
@@ -101,8 +101,8 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
@@ -150,7 +150,7 @@ impl LinearAccelerationPositionController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use units::{length::meter, f64::Length};
/// use uom::si::{length::meter, f64::Length};
///
/// let mut controller = LinearAccelerationPositionController::new_simple(
/// Some(Length::new::<meter>(10.0)),
@@ -375,7 +375,7 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
///
/// let new_position = Length::new::<meter>(0.0);
///
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Jerk, Velocity},
jerk::meter_per_second_cubed,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
+32 -14
View File
@@ -1,4 +1,4 @@
use units::{angle::radian, f64::Angle};
use uom::si::angle::radian;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
@@ -63,7 +63,7 @@ impl AngleConverter {
}
/// Convert angle from mathematical system to this system (f32)
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
pub fn degrees_encode(&self, math_angle_degrees: f32) -> f32 {
let normalized_input = self.normalize_angle(math_angle_degrees);
let mut result = normalized_input;
@@ -85,7 +85,7 @@ impl AngleConverter {
}
/// Convert angle from this system to mathematical system (f32)
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
let normalized_input = self.normalize_angle(system_angle_degrees);
let mut result = normalized_input;
@@ -107,14 +107,14 @@ impl AngleConverter {
}
/// Convert angle in radians from mathematical system to this system (f32)
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
pub fn radians_encode(&self, math_angle_radians: f32) -> f32 {
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode(degrees);
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f32)
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode(degrees);
converted_degrees.to_radians()
@@ -179,7 +179,7 @@ impl AngleConverter {
}
/// Normalize angle to [0, 360) range (f32)
fn normalize_angle(&self, angle: f64) -> f64 {
fn normalize_angle(&self, angle: f32) -> f32 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
@@ -207,12 +207,30 @@ impl AngleConverterUom {
Self { angle_converter }
}
pub fn encode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_encode(angle.get::<radian>()),
)
}
pub fn decode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_decode(angle.get::<radian>()),
)
}
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_encode_f64(angle.get::<radian>()),
)
}
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_decode_f64(angle.get::<radian>()),
)
}
}
@@ -279,8 +297,8 @@ mod tests {
let converter = AngleConverter::screen();
// Test radians_encode
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
let result = converter.radians_encode(PI / 2.0); // 90° in radians
let expected = 270.0 * PI / 180.0; // 270° in radians
assert!((result - expected).abs() < 1e-6);
}
@@ -288,10 +306,10 @@ mod tests {
fn test_roundtrip_conversion() {
let converter = AngleConverter::new(true, false, true); // X-flip + CW
let original = 75.0_f64;
let original = 75.0_f32;
let converted = converter.degrees_encode(original);
let roundtrip = converter.degrees_decode(converted);
assert!((original - roundtrip as f64).abs() < 1e-6);
assert!((original - roundtrip).abs() < 1e-6);
}
}
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
angle::revolution,
angular_acceleration::radian_per_second_squared,
angular_velocity::revolution_per_second,
@@ -90,7 +90,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use units::{
use uom::si::{
angle::degree, angular_acceleration::degree_per_second_squared,
angular_velocity::degree_per_second,
};
@@ -14,14 +14,14 @@
//!
//! ```rust
//! use control_core::converters::circular_converter::CircularConverter;
//! use units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//! use uom::si::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//!
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
//! let velocity = Velocity::new::<meter_per_second>(1.0);
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
//! ```
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
angle::radian,
angular_acceleration::radian_per_second_squared,
@@ -199,7 +199,7 @@ impl CircularConverter {
#[cfg(test)]
mod tests {
use super::*;
use units::length::centimeter;
use uom::si::length::centimeter;
#[test]
fn test_constructors() {
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
angle::revolution,
angular_velocity::revolution_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
@@ -251,7 +251,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use units::{
use uom::si::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
-36
View File
@@ -1,36 +0,0 @@
use anyhow::anyhow;
use smol::lock::Mutex;
use std::any::Any;
use std::sync::Arc;
pub trait Downcast<T> {
fn downcast(&self) -> Result<T, anyhow::Error>;
}
pub trait DowncastRef<T> {
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
}
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let lock = self.lock_blocking();
// Check if the inner type can be downcasted to T
let any: &dyn Any = &*lock;
if any.is::<T>() {
// Clone the Arc and return it as the desired type
let cloned: Arc<Mutex<dyn Any>> = self.clone();
// Transmute the Arc to the desired type
unsafe {
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
Ok(arc)
}
} else {
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
}
}
}
@@ -1,146 +1,3 @@
use std::fmt::Display;
use ethercat_hal::devices::wago_750_354::WAGO_750_354_IDENTITY_A;
use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::IP20_EC_DI8_DO8_IDENTITY;
use serde::Deserialize;
use serde::Serialize;
/// Identifies a spacifi machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentificationUnique {
pub machine_identification: MachineIdentification,
pub serial: u16,
}
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
pub vendor: u16,
pub machine: u16,
}
impl MachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
pub fn vendor_str(&self) -> String {
match self.vendor {
x if x == VENDOR_QITECH => "QiTech".to_string(),
_ => "N/A".to_string(),
}
}
pub fn slug(&self) -> String {
match self.machine {
x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(),
x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(),
x if x == MACHINE_LASER_V1 => "laser_v1".to_string(),
x if x == MACHINE_MOCK => "mock".to_string(),
x if x == MACHINE_AQUAPATH_V1 => "aquapath_v1".to_string(),
x if x == MACHINE_BUFFER_V1 => "buffer_v1".to_string(),
x if x == MACHINE_EXTRUDER_V2 => "extruder_v2".to_string(),
x if x == MACHINE_WAGO_POWER_V1 => "wago_power_v1".to_string(),
x if x == TEST_MACHINE => "test_machine".to_string(),
x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(),
x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(),
x if x == WAGO_AI_TEST_MACHINE => "wago_ai_test_machine".to_string(),
x if x == WAGO_DO_TEST_MACHINE => "wago_do_test_machine".to_string(),
_ => unreachable!("Unknown machine id"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
}
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
.machine_identification
.machine
!= 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
type Error = anyhow::Error;
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
let device_machine_identification =
value.device_machine_identification.ok_or(anyhow::anyhow!(
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
module_path!()
))?;
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
use anyhow::Error;
use anyhow::anyhow;
use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
@@ -176,20 +33,9 @@ use ethercat_hal::helpers::ethercrab_types::{
use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
use crate::ANALOG_INPUT_TEST_MACHINE;
use crate::IP20_TEST_MACHINE;
use crate::MACHINE_AQUAPATH_V1;
use crate::MACHINE_BUFFER_V1;
use crate::MACHINE_EXTRUDER_V1;
use crate::MACHINE_EXTRUDER_V2;
use crate::MACHINE_LASER_V1;
use crate::MACHINE_MOCK;
use crate::MACHINE_WAGO_POWER_V1;
use crate::MACHINE_WINDER_V1;
use crate::TEST_MACHINE;
use crate::VENDOR_QITECH;
use crate::WAGO_AI_TEST_MACHINE;
use crate::WAGO_DO_TEST_MACHINE;
use crate::machines::identification::DeviceMachineIdentification;
use crate::machines::identification::MachineIdentification;
use crate::machines::identification::MachineIdentificationUnique;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
@@ -249,7 +95,7 @@ pub async fn machine_device_identification<'maindevice>(
let addresses = match get_identification_addresses(&subdevice.identity(), subdevice.name()) {
Ok(x) => x,
Err(e) => {
// u16dump(subdevice, maindevice, 0, 128).await?;
u16dump(subdevice, maindevice, 0, 128).await?;
return Err(e);
}
};
@@ -389,8 +235,6 @@ pub fn get_identification_addresses(
let identity_tuple = subdevice_identity_to_tuple(subdevice_identity);
Ok(match identity_tuple {
WAGO_750_354_IDENTITY_A => MachineIdentificationAddresses::default(),
IP20_EC_DI8_DO8_IDENTITY => MachineIdentificationAddresses::default(),
EK1100_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
@@ -429,7 +273,61 @@ pub fn get_identification_addresses(
})
}
async fn u16dump<'maindevice>(
subdevice: &'maindevice EthercrabSubDevicePreoperational<'maindevice>,
maindevice: &MainDevice<'maindevice>,
start_byte: u16,
end_byte: u16,
) -> Result<(), Error> {
let mut words: Vec<u16> = Vec::new();
for word in start_byte..end_byte {
words.push(subdevice.eeprom_read(maindevice, word).await?);
}
println!(
"EEPROM dump for {} from 0x{:04x} to 0x{:04x}",
subdevice.name(),
start_byte / 2,
end_byte / 2
);
u16print(start_byte, end_byte, words);
Ok(())
}
fn u16print(start_byte: u16, end_byte: u16, data: Vec<u16>) {
let table_start_word = start_byte & 0xfff0;
let table_end_word = (end_byte & 0xfff0_u16) + 0x10_u16;
let rows = (table_end_word - table_start_word) >> 4;
for row in 0..rows {
print!("0x{:04x} | ", (table_start_word + row * 0x10) / 2);
for word in 0..8 {
let word_address = row * 8 + word;
if word_address < start_byte {
print!(" ");
} else {
let i = (word_address - start_byte) as usize;
if i > data.len() - 1 {
print!(" ");
} else {
print!("{:04x} ", data[i]);
}
}
}
println!();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hexprint() {
let data = vec![0x0000, 0x1ced];
u16print(0x01, 0x40, data);
}
}
@@ -1,9 +1,10 @@
use std::{sync::Arc, time::Duration};
use ethercrab::{
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
std::{ethercat_now, tx_rx_task},
};
use interfaces::Interface;
use std::{process::Command, sync::Arc, time::Duration};
// Constants for EtherCAT configuration
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
@@ -11,46 +12,14 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
/// Sets a network interface to unmanaged by NetworkManager.
/// Returns true if the command succeeded.
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
let managed_str = match managed {
true => "yes",
false => "no",
};
tracing::info!(
"set_interface_managed for {} managed was set to: {}",
interface,
managed_str
);
let status = Command::new("nmcli")
.args(["dev", "set", interface, "managed", managed_str])
.status();
matches!(status, Ok(s) if s.success())
}
/// Returns true if the given network interface is Ethernet.
/// Prevents testing wlan,loopback and other non ethernet devices
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
#[cfg(target_os = "linux")]
{
let base_path = format!("/sys/class/net/{}", interface);
let type_path = std::path::Path::new(&base_path).join("type");
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
// 1 means Ethernet
let reports_as_ethernet = iface_type == "1";
// Double-check that it's not a wireless interface
let uevent_path = std::path::Path::new(&base_path).join("uevent");
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
// If "DEVTYPE=wlan" appears, it's lying
let actually_wifi = uevent.contains("DEVTYPE=wlan");
return Ok(reports_as_ethernet && !actually_wifi);
}
#[cfg(not(target_os = "linux"))]
return Ok(true);
}
/// Finds an ethernet interface that is suitable for EtherCAT communication.
///
/// ```ignore
/// match discover_ethercat_interface().await {
/// Ok(interface) => println!("Found working interface: {}", interface),
/// Err(_) => println!("No working interface found"),
/// }
/// ```
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
tracing::info!("Discovering EtherCAT interface...");
@@ -68,6 +37,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
} else {
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
}
// Don't call the default hook, which would print the backtrace
}));
// Get eligible interfaces
@@ -78,7 +48,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
iface.is_up()
&& iface.is_running()
&& !iface.is_loopback()
&& is_ethernet(&iface.name).unwrap_or(false)
&& !iface.name.starts_with("bridge")
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
@@ -87,39 +56,57 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
})
.collect::<Vec<_>>();
// Sort interfaces by name
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
let mut interface: Option<&str> = None;
for i in 0..interfaces.len() {
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
interface = Some(&interfaces[i].name);
break;
}
Err(_) => (),
}
}
// Try all interfaces concurrently
let tasks = interfaces
.iter()
.map(|iface| {
let name = iface.name.clone();
std::thread::Builder::new()
.name(format!("ethercat-test-{}", name))
.spawn(move || {
std::panic::catch_unwind(|| {
smol::block_on(async {
match test_interface(&name) {
Ok(_) => {
tracing::info!("Found working EtherCAT interface: {}", name);
Some(name.clone())
}
Err(_) => {
tracing::debug!("Interface {} failed", name);
None
}
}
})
})
.unwrap_or(None)
})
.expect("Failed to spawn thread")
})
.collect::<Vec<_>>();
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
set_interface_managed(&interfaces[i].name, false);
} else {
set_interface_managed(&interfaces[i].name, true);
}
}
// Wait for all tasks to complete and collect successful results
let successful_interfaces: Vec<String> = tasks
.into_iter()
.filter_map(|task| task.join().expect("Should join thread"))
.collect();
// Return the first successful interface (they're sorted by name)
let result = successful_interfaces.into_iter().next();
// Restore the default panic hook
std::panic::set_hook(default_hook);
match interface {
Some(interface) => return Ok(interface.to_string()),
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
match result {
Some(name) => Ok(name),
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
}
}
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
tracing::info!("Testing interface: {}", interface);
tracing::trace!("Testing interface: {}", interface);
let pdu_storage = Box::leak(Box::new(PduStorage::<
IFACE_DISCOVERY_MAX_FRAMES,
@@ -157,7 +144,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
// Default 10000
dc_static_sync_iterations: 10000,
// Default None
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
},
));
+1
View File
@@ -1 +1,2 @@
pub mod eeprom_identification;
pub mod interface_discovery;
-65
View File
@@ -1,65 +0,0 @@
use std::process::Command;
use anyhow::Result;
use interfaces::Interface;
pub mod modbus_tcp_discovery;
/// Returns true if the given network interface is Ethernet.
/// Prevents testing wlan,loopback and other non ethernet devices
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
#[cfg(target_os = "linux")]
{
let base_path = format!("/sys/class/net/{}", interface);
let type_path = std::path::Path::new(&base_path).join("type");
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
// 1 means Ethernet
let reports_as_ethernet = iface_type == "1";
// Double-check that it's not a wireless interface
let uevent_path = std::path::Path::new(&base_path).join("uevent");
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
// If "DEVTYPE=wlan" appears, it's lying
let actually_wifi = uevent.contains("DEVTYPE=wlan");
Ok(reports_as_ethernet && !actually_wifi)
}
#[cfg(not(target_os = "linux"))]
return Ok(true);
}
pub fn get_interfaces() -> Result<Vec<Interface>> {
let vec = Interface::get_all()
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
.into_iter()
.filter(|iface| {
iface.is_up()
&& iface.is_running()
&& !iface.is_loopback()
&& is_ethernet(&iface.name).unwrap_or(false)
&& !iface.name.starts_with("bridge")
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
})
.collect::<Vec<_>>();
Ok(vec)
}
/// Sets a network interface to unmanaged by NetworkManager.
/// Returns true if the command succeeded.
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
let managed_str = match managed {
true => "yes",
false => "no",
};
tracing::info!(
"set_interface_managed for {} managed was set to: {}",
interface,
managed_str
);
let status = Command::new("nmcli")
.args(["dev", "set", interface, "managed", managed_str])
.status();
matches!(status, Ok(s) if s.success())
}
@@ -1,78 +0,0 @@
use crate::ethernet::get_interfaces;
use crate::{futures::FutureIteratorExt, modbus::tcp::ModbusTcpDevice};
use anyhow::{Result, bail};
use interfaces::Interface;
use std::cmp::min;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
pub struct ModbusTcpProbe {
pub addr: SocketAddr,
pub serial: u16,
}
pub async fn probe_modbus_tcp() -> Vec<ModbusTcpProbe> {
let interfaces = match get_interfaces() {
Ok(x) => x,
Err(_) => return vec![],
};
interfaces
.into_iter()
.map(probe_modbus_tcp_addresses)
.join_all()
.await
.into_iter()
.flatten()
.collect()
}
async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe> {
let out: Vec<ModbusTcpProbe> = interface
.addresses
.iter()
.filter_map(|addr| {
let a = addr.addr.map(|a| a.ip());
let m = addr.mask.map(|a| a.ip());
match (a, m) {
(Some(IpAddr::V4(addr)), Some(IpAddr::V4(mask))) => Some((addr, mask)),
_ => None,
}
})
.flat_map(|(addr, mask)| {
let a = u32::from(addr);
let m = u32::from(mask);
let network = a & m;
let prefix = min(24, m.count_ones()); // mask → prefix length
let size = 1u32 << (32 - prefix); // number of addresses
(0..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
})
.map(|addr| smol::spawn(ping_modbus_device(addr)))
.join_all()
.await
.into_iter()
.filter_map(|x| x.ok())
.collect();
out
}
async fn ping_modbus_device(addr: SocketAddr) -> Result<ModbusTcpProbe> {
let mut device = ModbusTcpDevice::new(addr).await?;
let module_number1 = device.get_u32(0x2).await?;
let module_number2 = device.get_u32(0x4).await?;
if module_number1 != 0x2787_2144 || module_number2 != 0x0000_0000 {
bail!("Unknown modbus tcp device!");
}
let serial = device.get_u32(0x000A).await?;
Ok(ModbusTcpProbe {
addr,
serial: serial as u16,
})
}
-26
View File
@@ -1,26 +0,0 @@
use core::future::Future;
// Extension trait for iterators of futures
pub trait FutureIteratorExt<F>: Iterator<Item = F> + Sized
where
F: Future,
{
/// Await all futures and return their outputs
fn join_all(self) -> impl Future<Output = Vec<F::Output>>;
}
impl<I, F> FutureIteratorExt<F> for I
where
I: Iterator<Item = F> + Sized,
F: Future,
{
async fn join_all(self) -> Vec<F::Output> {
let mut out = Vec::new();
for fut in self {
out.push(fut.await);
}
out
}
}
+6 -3
View File
@@ -1,16 +1,19 @@
pub mod controllers;
pub mod converters;
pub mod downcast;
pub mod ethercat;
pub mod ethernet;
pub mod futures;
pub mod helpers;
pub mod irq_handling;
pub mod machines;
pub mod modbus;
pub mod realtime;
pub mod rest;
pub mod serial;
pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
+25
View File
@@ -0,0 +1,25 @@
use serde_json::Value;
use crate::socketio::namespace::Namespace;
pub trait MachineApi {
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> &mut Namespace;
/// Returns a list of available video stream identifiers for this machine
#[cfg(feature = "video-streaming")]
fn api_video_streams(&self) -> Vec<String> {
Vec::new()
}
/// Get a video stream receiver for the specified stream ID
/// Returns None if the stream doesn't exist
#[cfg(feature = "video-streaming")]
fn api_get_video_stream(
&mut self,
stream_id: &str,
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
let _ = stream_id;
None
}
}
+113
View File
@@ -0,0 +1,113 @@
use std::fmt::Display;
use serde::Deserialize;
use serde::Serialize;
/// Identifies a spacifi machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentificationUnique {
pub machine_identification: MachineIdentification,
pub serial: u16,
}
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
pub vendor: u16,
pub machine: u16,
}
impl MachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
}
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
.machine_identification
.machine
!= 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
type Error = anyhow::Error;
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
let device_machine_identification =
value.device_machine_identification.ok_or(anyhow::anyhow!(
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
module_path!()
))?;
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
+275
View File
@@ -0,0 +1,275 @@
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use crate::{
machines::{
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
},
serial::SerialDevice,
socketio::event::GenericEvent,
};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
pub serial_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl Default for MachineManager {
fn default() -> Self {
Self::new()
}
}
impl MachineManager {
pub fn new() -> Self {
Self {
ethercat_machines: HashMap::new(),
serial_machines: HashMap::new(),
}
}
pub fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: usize>(
&mut self,
device_identifications: &Vec<DeviceIdentification>,
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
// empty ethercat machines
self.ethercat_machines.clear();
// group devices by machine device identification
let device_grouping_result = group_devices_by_identification(device_identifications);
tracing::info!(
"Device Groups {}",
device_grouping_result.device_groups.len()
);
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
// iterate over all identified device groups but ignore unaffected machines
for device_group in device_grouping_result.device_groups.iter() {
// get the machine identification
let machine_identification = match device_group.first() {
Some(device_identification) => &device_identification.device_machine_identification,
None => continue, // Skip this group if empty
};
// create the machine
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group,
hardware: &machine_new_hardware,
socket_queue_tx: socket_queue_tx.clone(),
machine_manager: machine_manager.clone(),
});
// insert the machine into the ethercat machines map
self.ethercat_machines.insert(
machine_identification.machine_identification_unique.clone(),
new_machine,
);
}
}
pub fn get(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
self.ethercat_machines
.get(machine_identification)
.or_else(|| self.serial_machines.get(machine_identification))
}
pub fn add_serial_device(
&mut self,
device_identification: &DeviceIdentification,
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
let hardware = MachineNewHardwareSerial { device };
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group: &vec![device_identification_identified.clone()],
hardware: &MachineNewHardware::Serial(&hardware),
socket_queue_tx,
machine_manager,
});
tracing::info!("Adding serial machine {:?}", new_machine);
self.serial_machines.insert(
device_identification_identified
.device_machine_identification
.machine_identification_unique,
new_machine,
);
}
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
tracing::info!(
"Removing serial machine {:?}",
device_identification_identified
);
self.serial_machines.remove(
&device_identification_identified
.device_machine_identification
.machine_identification_unique,
);
}
pub fn get_machine_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.ethercat_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
Some(machine_weak)
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.serial_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
Some(machine_weak)
}
}
/// Groups devices by machine identification
///
/// Returns a DeviceGroupingResult containing:
/// - device_groups: a vector of devices grouped by machine identification
/// - unidentified_devices: a vector of devices that could not be identified
pub fn group_devices_by_identification(
device_identifications: &Vec<DeviceIdentification>,
) -> DeviceGroupingResult {
let mut device_groups: Vec<Vec<DeviceIdentificationIdentified>> = Vec::new();
let mut unidentified_devices: Vec<DeviceIdentification> = Vec::new();
for device_identification in device_identifications {
// if vendor or serial or machine is 0, it is not a valid machine device
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
if !device_machine_identification.is_valid() {
unidentified_devices.push(device_identification.clone());
continue;
}
} else {
unidentified_devices.push(device_identification.clone());
continue;
}
// scan over all deice groups
// get the first DeviceMachineIdentification
// compare and append to the group
let mut found = false;
for check_group in device_groups.iter_mut() {
// get first device in group
let first_device = check_group.first().expect("group to not be empty");
let first_device_machine_identification = &first_device
.device_machine_identification
.machine_identification_unique;
// chek if it has machine identification
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
// compare with the current device
if first_device_machine_identification
== &device_machine_identification.machine_identification_unique
{
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
check_group.push(device_identification_identified);
found = true;
break;
}
}
}
if !found {
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
device_groups.push(vec![device_identification_identified]);
}
}
DeviceGroupingResult {
device_groups,
unidentified_devices,
}
}
/// Structure to hold the result of grouping devices by identification
#[derive(Debug)]
pub struct DeviceGroupingResult {
/// Devices grouped by machine identification
pub device_groups: Vec<Vec<DeviceIdentificationIdentified>>,
/// Devices that could not be identified
pub unidentified_devices: Vec<DeviceIdentification>,
}
+82
View File
@@ -0,0 +1,82 @@
use std::{collections::HashMap, sync::Arc};
use smol::lock::Mutex;
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
pub enum MachineManagerIter<'a> {
EthercatMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
Done,
}
pub struct MachineManagerIterator<'a> {
iter: MachineManagerIter<'a>,
serial_machines:
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl<'a> Iterator for MachineManagerIterator<'a> {
type Item = (
&'a MachineIdentificationUnique,
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
);
fn next(&mut self) -> Option<Self::Item> {
loop {
match &mut self.iter {
MachineManagerIter::EthercatMachines { iter } => {
// Try to get the next item from ethercat_machines
if let Some(item) = iter.next() {
return Some(item);
} else {
// If ethercat_machines is exhausted, move to serial_machines
self.iter = MachineManagerIter::SerialMachines {
iter: self.serial_machines.iter(),
};
// Continue to the next iteration of the loop
continue;
}
}
MachineManagerIter::SerialMachines { iter } => {
// Return the next item from serial_machines or None if exhausted
if let Some(item) = iter.next() {
return Some(item);
} else {
// If serial_machines is also exhausted, we're done
self.iter = MachineManagerIter::Done;
return None;
}
}
MachineManagerIter::Done => {
// We've exhausted both collections
return None;
}
}
}
}
}
impl MachineManager {
// Returns an iterator over all machines (both ethercat and serial)
pub fn iter(&'_ self) -> MachineManagerIterator<'_> {
MachineManagerIterator {
iter: MachineManagerIter::EthercatMachines {
iter: self.ethercat_machines.iter(),
},
serial_machines: &self.serial_machines,
}
}
}
+83
View File
@@ -0,0 +1,83 @@
use anyhow::anyhow;
use api::MachineApi;
use new::MachineNewTrait;
use serde::Serialize;
use smol::lock::Mutex;
use std::any::Any;
use std::fmt::Debug;
use std::sync::{Arc, Weak};
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::new::MachineAct;
pub mod api;
pub mod identification;
pub mod manager;
pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + AnyGetters + Debug + Send {}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let read_lock = machine.lock().await;
// Check if the inner type can be downcasted to T
if read_lock.as_any().is::<T>() {
// Clone the Arc and return it as the desired type
let cloned_machine = Arc::clone(&machine);
// Transmute the Arc to the desired type
unsafe {
Ok(Arc::from_raw(
Arc::into_raw(cloned_machine) as *const Mutex<T>
))
}
} else {
Err(anyhow!(
"[{}::downcast_machine] Downcast failed",
module_path!()
))
}
}
/// Trait to get strong count to see if Weak has reference
pub trait GetStrongCount {
fn get_strong_count(&self) -> usize;
}
impl<T> GetStrongCount for Weak<Mutex<T>> {
fn get_strong_count(&self) -> usize {
self.strong_count()
}
}
#[derive(Debug)]
pub struct ConnectedMachine<T: GetStrongCount> {
pub machine_identification_unique: MachineIdentificationUnique,
pub machine: T,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ConnectedMachineData {
pub machine_identification_unique: MachineIdentificationUnique,
pub is_available: bool,
}
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
where
T: GetStrongCount,
{
fn from(value: &ConnectedMachine<T>) -> Self {
Self {
machine_identification_unique: value.machine_identification_unique.clone(),
is_available: value.machine.get_strong_count() != 0,
}
}
}
+259
View File
@@ -0,0 +1,259 @@
use crate::{machines::manager::MachineManager, serial::SerialDevice};
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
use ethercat_hal::{
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use socketioxide::extract::SocketRef;
use std::{
sync::{Arc, Weak},
time::Instant,
};
use crate::socketio::event::GenericEvent;
pub trait MachineNewTrait {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant);
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
> where
'maindevice: 'machine_new_hardware,
'subdevices: 'machine_new_hardware,
'ethercat_devices: 'machine_new_hardware,
'machine_new_hardware_etehrcat: 'machine_new_hardware,
{
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
pub hardware: &'machine_new_hardware MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub machine_manager: Weak<RwLock<MachineManager>>,
}
pub enum MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
> where
'maindevice: 'machine_new_hardware_etehrcat,
'subdevices: 'machine_new_hardware_etehrcat,
'ethercat_devices: 'machine_new_hardware_etehrcat,
{
Ethercat(
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
'maindevice,
'subdevices,
'ethercat_devices,
>,
),
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
}
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
pub subdevices:
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
}
pub struct MachineNewHardwareSerial {
pub device: Arc<RwLock<dyn SerialDevice>>,
}
// validates that all devices in the group have the same machine identification
pub fn validate_same_machine_identification_unique(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let machine_identification_unique = &identified_device_group
.first()
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::validate_same_machine_identification] No devices in group",
module_path!()
)
})?
.device_machine_identification
.machine_identification_unique;
for device in identified_device_group.iter() {
if device
.device_machine_identification
.machine_identification_unique
!= *machine_identification_unique
{
return Err(anyhow::anyhow!(
"[{}::validate_same_machine_identification] Different machine identifications",
module_path!()
));
}
}
Ok(())
}
/// validates that every role is unique
pub fn validate_no_role_dublicates(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let mut roles = vec![];
for device in identified_device_group.iter() {
if roles.contains(&device.device_machine_identification.role) {
return Err(anyhow::anyhow!(
"[{}::validate_no_role_dublicates] Role dublicate",
module_path!(),
));
}
roles.push(device.device_machine_identification.role);
}
Ok(())
}
// Inside control_core::machines::new module:
pub fn get_device_identification_by_role(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
role: u16,
) -> Result<&DeviceIdentificationIdentified, Error> {
for device in identified_device_group.iter() {
if device.device_machine_identification.role == role {
return Ok(device);
}
}
Err(anyhow::anyhow!(
"[{}::get_device_identification_by_role] Role {} not found",
module_path!(),
role
))
}
pub fn get_device_by_index<'maindevice>(
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_device_by_index] Index {} out of bounds for devices",
module_path!(),
subdevice_index
))?
.clone())
}
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
subdevice_index: usize,
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
"Index {} out of bounds for subdevices",
subdevice_index
))?)
}
pub fn get_ethercat_device_by_index<'maindevice>(
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(ethercat_devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
module_path!(),
subdevice_index
))?
.clone())
}
#[cfg(test)]
mod tests {
use crate::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
};
pub use super::*;
#[test]
fn test_get_device_identification_by_role() {
let device_identifications = vec![
// role 0
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 1,
machine: 1,
},
serial: 1,
},
role: 0,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 0 },
),
},
// role 1
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 2,
machine: 2,
},
serial: 2,
},
role: 2,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 1 },
),
},
// role 2
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 3,
machine: 3,
},
serial: 3,
},
role: 1,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 2 },
),
},
];
// search for role 0
let result = get_device_identification_by_role(&device_identifications, 0);
assert_eq!(result.unwrap().device_machine_identification.role, 0,);
// search for role 1
let result = get_device_identification_by_role(&device_identifications, 1);
assert_eq!(result.unwrap().device_machine_identification.role, 1,);
// search for role 2
let result = get_device_identification_by_role(&device_identifications, 2);
assert_eq!(result.unwrap().device_machine_identification.role, 2,);
}
}
+74
View File
@@ -0,0 +1,74 @@
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
use anyhow::Error;
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
}
impl Default for MachineRegistry {
fn default() -> Self {
Self::new()
}
}
impl MachineRegistry {
pub fn new() -> Self {
Self {
type_map: HashMap::new(),
}
}
pub fn register<T: Machine + 'static>(
&mut self,
machine_identficiation: MachineIdentification,
) {
self.type_map.insert(
TypeId::of::<T>(),
(
machine_identficiation.clone(),
// create a machine construction closure
Box::new(|machine_new_params| {
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
}),
),
);
}
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
.device_group
.first()
.ok_or(anyhow::anyhow!(
"[{}::MachineConstructor::new_machine] No device in group",
module_path!()
))?;
// find machine new function by comparing MachineIdentification
let (_, machine_new_closure) = self
.type_map
.values()
.find(|(mi, _)| {
mi == &device_identification
.device_machine_identification
.machine_identification_unique
.machine_identification
})
.ok_or(anyhow::anyhow!(
"[{}::MachineConstructor::new_machine] Machine not found",
module_path!()
))?;
// call machine new function by reference
(machine_new_closure)(machine_new_params)
}
}
-1
View File
@@ -1,5 +1,4 @@
pub mod modbus_serial_interface;
pub mod tcp;
use anyhow::Error;
use crc::{CRC_16_MODBUS, Crc};
-263
View File
@@ -1,263 +0,0 @@
use std::fmt::Debug;
use std::io::ErrorKind;
use std::net::SocketAddr;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use smol::Timer;
use smol::future::FutureExt;
use smol::io;
use smol::io::AsyncReadExt;
use smol::io::AsyncWriteExt;
use smol::net::TcpStream;
const PROTOCOL_ID: u16 = 0;
const UNIT_ID: u8 = 0;
const READ_HOLDING_FUNCTION_CODE: u8 = 3;
const WRITE_HOLDING_FUNCTION_CODE: u8 = 16;
const READ_HOLDING_LENGTH: u16 = 6; // Unit ID 00 + Func Code 03 + Start Address XXXX + Quantity XXXX = 6 bytes
const WRITE_HOLDING_LENGTH_WITHOUT_DATA: u16 = 7; // Unit ID 00 + Func Code 16 + Start Address XXXX + Quantity XXXX + Num Data Bytes XX = 7 bytes
struct Packet {
buf: Vec<u8>,
}
impl Packet {
pub const fn new() -> Self {
Self { buf: Vec::new() }
}
pub fn add_u8(&mut self, x: u8) {
self.buf.push(x);
}
pub fn add_u16(&mut self, x: u16) {
let buf = x.to_be_bytes();
self.buf.push(buf[0]);
self.buf.push(buf[1]);
}
pub fn as_bytes(&self) -> &[u8] {
&self.buf
}
}
pub struct ModbusTcpDevice {
stream: TcpStream,
transactions: u16,
}
impl Debug for ModbusTcpDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.stream.peer_addr() {
Ok(addr) => write!(f, "ModbusTcpDevice({:?})", addr),
Err(_) => std::fmt::Result::Err(std::fmt::Error),
}
}
}
impl ModbusTcpDevice {
pub async fn new(addr: SocketAddr) -> Result<Self> {
let timeout = async {
Timer::after(Duration::from_millis(10)).await;
Err(io::Error::from(ErrorKind::TimedOut))
};
let stream = TcpStream::connect(addr)
.or(timeout)
.await
.context("Could not connect to modbus device!")?;
Ok(Self {
stream,
transactions: 0,
})
}
async fn read_and_check_responce_header(&mut self, func_code: u8) -> Result<()> {
if self.read_u16().await? != self.transactions {
bail!("Modbus device sent unexpected transaction id!");
}
if self.read_u16().await? != PROTOCOL_ID {
bail!("Modbus device sent unexpected protocol id!");
}
let length = self.read_u16().await?;
if length < 2 {
bail!("Modbus device will send too few bytes to understand the response!");
}
if self.read_u8().await? != UNIT_ID {
bail!("Modbus device sent unexpected unit id!");
}
let func_code_res = self.read_u8().await?;
if func_code != func_code_res {
let error_code = self.read_u8().await?;
bail!(
"Modbus device sent unexpected function code 0x{:x}! It probably encountered an error. Error code: 0x{:x}",
func_code_res,
error_code
);
}
Ok(())
}
async fn send_read_holding_request(&mut self, addr: u16, count: u16) -> Result<u16> {
let mut packet = Packet::new();
self.transactions += 1;
packet.add_u16(self.transactions);
packet.add_u16(PROTOCOL_ID);
packet.add_u16(READ_HOLDING_LENGTH);
packet.add_u8(UNIT_ID);
packet.add_u8(READ_HOLDING_FUNCTION_CODE);
packet.add_u16(addr);
packet.add_u16(count);
self.send(packet).await?;
self.read_and_check_responce_header(READ_HOLDING_FUNCTION_CODE)
.await?;
let num_data_bytes = self.read_u8().await? as u16;
if count * 2 != num_data_bytes {
bail!(
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
);
}
Ok(num_data_bytes)
}
pub async fn get_holding_registers(&mut self, addr: u16, count: u16) -> Result<Vec<u16>> {
self.send_read_holding_request(addr, count).await?;
let mut res: Vec<u16> = Vec::new();
while res.len() < count as usize {
res.push(self.read_u16().await?);
}
Ok(res)
}
pub async fn set_holding_registers(&mut self, addr: u16, values: &[u16]) -> Result<()> {
let count = values.len();
let num_bytes = count * 2;
assert!(
count <= 128,
"Cannot send that many bytes to modbus device in one go! Trying to send {} bytes of data.",
num_bytes
);
let mut packet = Packet::new();
self.transactions += 1;
packet.add_u16(self.transactions);
packet.add_u16(PROTOCOL_ID);
packet.add_u16(WRITE_HOLDING_LENGTH_WITHOUT_DATA + num_bytes as u16);
packet.add_u8(UNIT_ID);
packet.add_u8(WRITE_HOLDING_FUNCTION_CODE);
packet.add_u16(addr);
packet.add_u16(count as u16);
packet.add_u8(num_bytes as u8);
for value in values {
packet.add_u16(value.to_owned());
}
self.send(packet).await?;
self.read_and_check_responce_header(WRITE_HOLDING_FUNCTION_CODE)
.await?;
if self.read_u16().await? != addr {
bail!("Modbus device wrote to wrong register address!");
}
if self.read_u16().await? != count as u16 {
bail!("Modbus device wrote wrong number of registers!");
}
Ok(())
}
pub async fn get_string<const N: usize>(&mut self, addr: u16) -> Result<String> {
assert!(N % 2 == 0, "Strings are always of even length!");
assert!(N <= 256, "Cannot read long strings in a single swoop!");
let count = N / 2;
self.send_read_holding_request(addr, count as u16).await?;
let buf = self.read_bytes::<N>().await?;
let s = String::from_utf8_lossy(&buf);
let s = match s.split_once('\0') {
None => s.to_string(),
Some((l, _)) => l.to_string(),
};
Ok(s)
}
pub async fn set_string<const N: usize>(&mut self, _addr: u16, s: &str) -> Result<()> {
assert!(s.len() <= N, "String is too long to fit!");
todo!()
}
pub async fn get_u16(&mut self, addr: u16) -> Result<u16> {
let length = self.send_read_holding_request(addr, 1).await?;
assert_eq!(
length, 2,
"Expected modbus device to send exactly 2 bytes of data!"
);
self.read_u16().await
}
pub async fn get_u32(&mut self, addr: u16) -> Result<u32> {
let length = self.send_read_holding_request(addr, 2).await?;
assert_eq!(
length, 4,
"Expected modbus device to send exactly 4 bytes of data!"
);
let buf = self.read_bytes::<4>().await?;
Ok(u32::from_be_bytes(buf))
}
// We have to send the request in a single packet,
// otherwise the device may fail to respond correctly.
async fn send(&mut self, packet: Packet) -> Result<()> {
self.stream
.write_all(packet.as_bytes())
.await
.context("Could write to modbus device!")
}
async fn read_u8(&mut self) -> Result<u8> {
let buf = self.read_bytes::<1>().await?;
Ok(u8::from_be_bytes(buf))
}
async fn read_u16(&mut self) -> Result<u16> {
let buf = self.read_bytes::<2>().await?;
Ok(u16::from_be_bytes(buf))
}
async fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
let mut buf = [0; N];
self.stream
.read_exact(&mut buf)
.await
.context("Could not read from modbus device!")?;
Ok(buf)
}
}
+1
View File
@@ -0,0 +1 @@
pub mod mutation;
@@ -2,7 +2,7 @@ use std::fmt::Debug;
use serde::Serialize;
use machines::machine_identification::MachineIdentificationUnique;
use crate::machines::identification::MachineIdentificationUnique;
#[derive(Debug, Serialize, Clone)]
pub struct MutationResponse {
+9 -5
View File
@@ -1,10 +1,14 @@
/*use std::{any::Any, sync::Arc};
use std::{any::Any, sync::Arc};
use smol::lock::RwLock;
use smol::{channel::Sender, lock::RwLock};
use std::fmt::Debug;
use crate::machines::identification::DeviceIdentification;
use crate::{
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
};
pub mod panic;
pub mod registry;
pub mod serial_detection;
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
@@ -23,11 +27,11 @@ pub trait SerialDeviceThread {
pub struct SerialDeviceNewParams {
pub path: String,
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
*/
+53
View File
@@ -0,0 +1,53 @@
use smol::channel::Sender;
use crate::serial::serial_detection::SerialDeviceRemoval;
/// Device-level panic handler for individual device crashes
/// Sends device identifier and error information for cleanup
/// Used by serial devices and other individual components that need to be removed on panic
pub fn send_serial_device_panic<T>(
device_identifier: T,
thread_panic_tx: Sender<SerialDeviceRemoval<T>>,
) where
T: Send + Sync + Clone + 'static,
{
// Ensure backtrace is enabled for panics
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "full");
}
}
std::panic::set_hook(Box::new(move |panic_info| {
let thread_name = std::thread::current()
.name()
.unwrap_or("unnamed")
.to_string();
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
});
let message = panic_info
.payload()
.downcast_ref::<&str>()
.unwrap_or(&"Box<dyn Any>")
.to_string();
let panic_message = format!(
"Device panicked in thread '{}' at {}: {}",
thread_name, location, message
);
// Send device panic info through channel
smol::block_on(async {
let _ = thread_panic_tx
.send(SerialDeviceRemoval::Error(
device_identifier.clone(),
anyhow::anyhow!("{}", panic_message),
))
.await;
});
// Note: We don't call old_hook to avoid duplicate logging
// All logging is handled in the main thread when receiving panic details
}));
}
@@ -1,14 +1,8 @@
use lazy_static::lazy_static;
use super::SerialDeviceNewParams;
use crate::{
SerialDevice, SerialDeviceIdentification, machine_identification::DeviceIdentification,
serial::devices::laser::Laser,
machines::identification::DeviceIdentification,
serial::{SerialDevice, SerialDeviceIdentification},
};
#[cfg(feature = "mock-machine")]
use crate::serial::devices::mock::MockSerialDevice;
use crate::SerialDeviceNewParams;
use anyhow::{Error, Result};
use smol::lock::RwLock;
use std::{any::TypeId, collections::HashMap, sync::Arc};
@@ -100,22 +94,3 @@ impl SerialDeviceRegistry {
}
}
}
lazy_static! {
pub static ref SERIAL_DEVICE_REGISTRY: SerialDeviceRegistry = {
let mut sdr = SerialDeviceRegistry::new();
sdr.register::<Laser>(SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
});
// Register MockSerialDevice when mock-machine feature is enabled
#[cfg(feature = "mock-machine")]
sdr.register::<MockSerialDevice>(SerialDeviceIdentification {
vendor_id: 0x0001, // VENDOR_QITECH
product_id: 0x0007, // MACHINE_MOCK
});
sdr
};
}
+173 -49
View File
@@ -7,70 +7,194 @@
*/
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
use std::collections::HashMap;
use smol::{
channel::{Receiver, Sender, unbounded},
lock::RwLock,
};
use std::{collections::HashMap, sync::Arc};
use super::SerialDeviceIdentification;
/// Handles scanning and caching of connected USB serial devices.
pub struct SerialDetection {}
use crate::{
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
};
impl SerialDetection {
/// Returns all serial ports on the system (minus macOS "cu" noise).
fn get_ports() -> Vec<SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports
.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect(),
Err(_) => Vec::new(),
use super::{
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
};
pub enum SerialDeviceRemoval<T> {
Disconnect(T),
Error(T, anyhow::Error),
}
pub struct SerialDetection<'serialdeviceregistry> {
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
pub ports: HashMap<
String,
(
UsbPortInfo,
DeviceIdentification,
Arc<RwLock<dyn SerialDevice>>,
),
>,
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
}
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
SerialDetection {
serial_device_registry,
ports: HashMap::new(),
device_removal_signal_rx,
device_removal_signal_tx,
}
}
/// Extracts only USB serial devices from the port list.
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
let mut map = HashMap::new();
/*
*@return: Option::None if there are no available ports on device
*@return: Option::Some(Vec) with all available ports
*
*@description: detects all available ports and returns them as list
*/
fn get_ports() -> Vec<SerialPortInfo> {
let all = match serialport::available_ports() {
Ok(ports) => ports,
Err(_) => return Vec::new(),
};
// exclude ports that contai "cu" in their name
all.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect::<Vec<_>>()
}
/* @param: port_list -> list of available ports
*
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
*
* @description: This function extracts the USB serial devices from the given list of ports.
*/
fn extract_usb_serial_devices(
&self,
port_list: Vec<SerialPortInfo>,
) -> Vec<(String, UsbPortInfo)> {
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
for port in port_list {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
map.insert(port.port_name, usb_info);
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
};
}
usb_ports
}
pub async fn check_ports(&mut self) -> CheckPortsResult {
// get available ports
let ports = Self::get_ports();
// extract all ports that are usb ports
let usb_ports = self.extract_usb_serial_devices(ports);
let last_usb_ports = self
.ports
.iter()
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
.collect::<Vec<_>>();
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
let mut result = CheckPortsResult {
added: Vec::new(),
removed: Vec::new(),
};
// reamove ports
for removed in usb_ports_diff.removed {
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
result.removed.push(device_identification);
}
tracing::trace!("Removed port: {}", removed.0);
}
// add new ports
for added in usb_ports_diff.added {
// add the port to the list
let serial_device_identification = SerialDeviceIdentification {
vendor_id: added.1.vid,
product_id: added.1.pid,
};
let device_result = self.serial_device_registry.new_serial_device(
&SerialDeviceNewParams {
path: added.0.clone(),
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
},
&serial_device_identification,
);
// only if created device driver sucessfully
if let Ok((device_identification, device)) = device_result {
// add the device to the ports list
self.ports.insert(
added.0.clone(),
(
added.1.clone(),
device_identification.clone(),
device.clone(),
),
);
// add to result list
result.added.push((device_identification, device.clone()));
tracing::trace!("Added port: {}", added.0);
}
}
map
result
}
/// Scans for connected USB serial devices and updates the internal map.
/// Returns a clone of the map for quick access.
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
let ports = Self::get_ports();
let usb_devices = Self::extract_usb_serial_devices(ports);
usb_devices
}
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
pub fn device_port_exists(device_path: &str) -> bool {
let ports = Self::get_ports();
return ports.iter().any(|p| p.port_name == device_path);
}
let mut add_removal_signal = |path: &String| {
if let Some(info) = self.ports.get(path) {
removed_signals.push(info.1.clone());
};
self.ports.remove(path);
};
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
let ports = Self::get_ports();
for port in ports {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
return true;
}
if self.device_removal_signal_rx.is_empty() {
return removed_signals;
}
let removal = self
.device_removal_signal_rx
.recv()
.await
.expect("Failed to receive removal signals");
match removal {
SerialDeviceRemoval::Disconnect(path) => {
add_removal_signal(&path);
tracing::trace!("Removed serial device after disconnect: {:?}", path);
}
SerialDeviceRemoval::Error(path, error) => {
add_removal_signal(&path);
tracing::error!("Removed erroring serial device: {:?}: {}", path, error);
}
}
return false;
}
pub fn get_path_by_id(
sdevid: SerialDeviceIdentification,
map: HashMap<String, UsbPortInfo>,
) -> Option<String> {
for (port, usbport) in map.iter() {
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
return Some(port.clone());
}
}
return None;
removed_signals
}
}
#[derive(Debug)]
pub struct CheckPortsResult {
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
pub removed: Vec<DeviceIdentification>,
}
+1
View File
@@ -1,2 +1,3 @@
pub mod event;
pub mod namespace;
pub mod namespace_id;
+3 -2
View File
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tracing::instrument;
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Namespace {
pub sockets: Vec<SocketRef>,
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
@@ -118,6 +118,7 @@ impl Namespace {
) {
// cache the event
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
for socket in self.sockets.clone() {
@@ -167,7 +168,7 @@ impl Namespace {
impl Drop for Namespace {
fn drop(&mut self) {
//self.disconnect_all();
self.disconnect_all();
}
}
@@ -3,7 +3,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use machines::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::machines::identification::{MachineIdentification, MachineIdentificationUnique};
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum NamespaceId {
+1 -1
View File
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::acceleration;
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
"meters per minute per second";
}
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::angular_acceleration;
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
"revolutions per minute per second";
}
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::angular_jerk;
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
"revolution per minute per second squared";
}
+7
View File
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::jerk;
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
"meters per minute per second squared";
}
+5
View File
@@ -0,0 +1,5 @@
pub mod acceleration;
pub mod angular_acceleration;
pub mod angular_jerk;
pub mod jerk;
pub mod velocity;
@@ -0,0 +1,7 @@
unit! {
system: uom::si;
quantity: uom::si::velocity;
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
"meters per minute";
}
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
# Generate 18 random alphanumeric characters base64 encode It
password=$(head -c 18 /dev/urandom | base64)
echo "Basic Authentication; user=machine password=$password"
echo $password > /tmp/machine_password
caddy_hash=$(caddy hash-password --plaintext $password)
echo "(machine_basic_auth) {
basic_auth {
machine $caddy_hash
}
}" | sudo tee /var/lib/caddy/auth_snippet.conf
# Transfer ownership to caddy user
sudo chown caddy:caddy /var/lib/caddy/auth_snippet.conf
# Tigther permissions (Only readable by root or caddy user)
sudo chmod 600 /var/lib/caddy/auth_snippet.conf
sudo systemctl restart caddy.service
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Loaded 100 of 534 files, more files were not shown because too many files have changed in this diff. Show more