Compare commits

..
398 changed files with 10960 additions and 85182 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
-29
View File
@@ -1,29 +0,0 @@
This is a machine control software project.
## Repository structure
### Frontend
`/electron`: Frontend code for the control software built with React and Electron.
### Backend
`/server`: Implements machine setup & loop. Exposes a REST & Socket.io API for the electron frontend.
`/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
`/ethercat-hal-derive`: Macros for ethercat-hal
`/control-core`: Generic logic for the server.
### HMI Panel Operating System
`/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
### Other
/docs: Documentation for the project.
## Key Guidelines
1. Follow Rust & Typescript best practices and idiomatic patterns
2. Maintain existing code structure and organization
3. Write unit tests for new functionality where applicable.
4. Document public APIs and complex logic in code. Suggest changes to the `docs/` folder when appropriate
+11 -14
View File
@@ -1,19 +1,16 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm"
directory: "/electron"
- package-ecosystem: "npm" # See documentation for possible values
directory: "/electron" # Location of package manifests
schedule:
interval: "monthly"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-minor"
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/"
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-minor"
interval: "weekly"
+1 -7
View File
@@ -36,17 +36,11 @@ jobs:
- name: Install
run: npm i
- name: Build
run: tsc
- name: Format
run: npm run format
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Package
- name: Build
run: npm run package
-7
View File
@@ -21,8 +21,6 @@ jobs:
- uses: actions/checkout@v4
- name: Set Rust version
run: rustup override set ${{ env.RUST_VERSION }}
- name: Install Formatter
run: rustup component add rustfmt
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
@@ -37,12 +35,7 @@ jobs:
${{ runner.os }}-${{ env.RUST_VERSION }}-cargo-
- name: Build
env:
RUSTFLAGS: "-D warnings"
run: cargo build --verbose
- name: Format
run: cargo fmt --check --verbose
- name: Run tests
run: cargo test --verbose
-49
View File
@@ -1,49 +0,0 @@
# Canonical identities on the left, aliases on the right.
# Format options:
# Canon Name <canon@example.com> <alias@example.com>
# Canon Name <canon@example.com> Alias Name <alias@example.com>
# Icedsoda -> Philipp
Philipp <29167146+1cedsoda@users.noreply.github.com> <29167146+1cedsoda@users.noreply.github.com>
# kraemr / Robin Krämer -> Robin
Robin <93094049+kraemr@users.noreply.github.com> <93094049+kraemr@users.noreply.github.com>
Robin <93094049+kraemr@users.noreply.github.com> Robin Krämer <93094049+kraemr@users.noreply.github.com>
# Oshgnacknak -> Josh
Josh <osh@oshgnacknak.de> <osh@oshgnacknak.de>
# Milan
Milan <61654753+MilanVDB@users.noreply.github.com>
# dependabot stays as bot
Dependabot <49699333+dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
# Medformatik / Florian -> Florian
Florian <dev@fwcr.de> <dev@fwcr.de>
Florian <dev@fwcr.de> Medformatik <dev@fwcr.de>
# Anatomik5 -> Alisher (merge noreply + gmail under Alisher)
Alisher <115714693+Anatomik5@users.noreply.github.com> <115714693+Anatomik5@users.noreply.github.com>
Alisher <115714693+Anatomik5@users.noreply.github.com> Anatomik5 <darmenovalisher@gmail.com>
Alisher <115714693+Anatomik5@users.noreply.github.com> Darmenov Alisher <115714693+Anatomik5@users.noreply.github.com>
# snacj -> Melvin
Melvin <0xsnacj@proton.me> <0xsnacj@proton.me>
Melvin <0xsnacj@proton.me> snacj <bbmelvin@gmx.de>
Melvin <0xsnacj@proton.me> Melvin Spieß <0xsnacj@proton.me>
# Janhegerq -> Jan
Jan <219994445+Janhegerq@users.noreply.github.com> <219994445+Janhegerq@users.noreply.github.com>
Jan <219994445+Janhegerq@users.noreply.github.com> <19994445+Janhegerq@users.noreply.github.com>
# Christian / TheBest6337 -> christian
Christian <christian.zschoegner@outlook.de> <christian.zschoegner@outlook.de>
Christian <christian.zschoegner@outlook.de> TheBest6337 <christian.zschoegner@outlook.de>
Christian <christian.zschoegner@outlook.de> Christian <80114847+TheBest6337@users.noreply.github.com>
# nixos-install.sh
nixos-install.sh <nixos-install.sh@localhost> <nixos-install.sh@localhost>
# copilot-swe-agent bot
Copilot <198982749+Copilot@users.noreply.github.com> <198982749+Copilot@users.noreply.github.com>
+4 -126
View File
@@ -1,126 +1,4 @@
# `2.6.0`
_03.09.2025_
## Extruder V2
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
## Laser V1
- [#642](https://github.com/qitechgmbh/control/pull/642) Improved stability of laser connection
## General
- [#657](https://github.com/qitechgmbh/control/pull/657) Time frame for graphs is now remembered
- [#641](https://github.com/qitechgmbh/control/pull/641) Removed duplicate scrollbar from manuals
# `2.5.0`
_27.08.2025_
## Extruder V2
- [#595](https://github.com/qitechgmbh/control/pull/600) Simplified power graphs
- [#593](https://github.com/qitechgmbh/control/pull/599) Added identifiers for temperature sensors in temperature graph
## Winder V2
- [#598](https://github.com/qitechgmbh/control/pull/601) Images now load properly in the manual
## General
- [#540](https://github.com/qitechgmbh/control/pull/592) Improved readability of the presets
- [#591](https://github.com/qitechgmbh/control/pull/626) Significantly improved user interface performance and stability
# `2.4.0`
_14.08.2025_
## Winder V2
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
## General
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
- [#588](https://github.com/qitechgmbh/control/pull/589) Improved frontend performance
# `2.3.0`
_06.08.2025_
## Extruder V2
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
## Winder V2
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
## Laser V1
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
## General
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
# `2.2.0`
_15.07.2025_
## Extruder V2
- [#455](https://github.com/qitechgmbh/control/pull/458), Greatly improved stability and precision of pressure-based regulation for the motor
- [#467](https://github.com/qitechgmbh/control/pull/468), [#448](https://github.com/qitechgmbh/control/pull/449), [#485](https://github.com/qitechgmbh/control/pull/486) Improved the Responsiveness for motor control and monitoring
- [#138](https://github.com/qitechgmbh/control/pull/495), Added a Warning in case the Pressure sensor is disconnected or incorrectly wired
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
## Winder V2
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
## General
- [#121](https://github.com/qitechgmbh/control/pull/121), Use Linux realtime threads for EtherCAT and control loop (PREEMPT_RT)
- [#138](https://github.com/qitechgmbh/control/pull/495), Updated Quick Start Guide
# `2.1.0`
_30.06.2025_
## Extruder V2
- Intial Stable Release of **Extruder V2**
## Winder V2
- [#420](https://github.com/qitechgmbh/control/pull/429), [#401](https://github.com/qitechgmbh/control/pull/401) Add _Adaptive_ spool speed algorithm, the old algorithm is now called _Minmax_ ans tillavailable.
- [#427](https://github.com/qitechgmbh/control/pull/427) Add _Estimated Spool Diameter_ graph
- [#419](https://github.com/qitechgmbh/control/pull/419) Fix Issue where traverse won't work at low spool speed
## Laser V1
- Initial Stable Release of **Laser V1**
## HMI Panel
- [#339](https://github.com/qitechgmbh/control/pull/338) Don't go into sleep Mmde
- [#410](https://github.com/qitechgmbh/control/pull/410) Ship with Wireshark
- [#424](https://github.com/qitechgmbh/control/pull/424) Updates can be canceled
## General
- [#269](https://github.com/qitechgmbh/control/pull/269), [#370](https://github.com/qitechgmbh/control/pull/370) Improved performace of live values
- [#358](https://github.com/qitechgmbh/control/pull/358), [#343](https://github.com/qitechgmbh/control/pull/343), [#324](https://github.com/qitechgmbh/control/pull/324), [#390](https://github.com/qitechgmbh/control/pull/390), [#299](https://github.com/qitechgmbh/control/pull/299) Improved functionality for graphs
- [#405](https://github.com/qitechgmbh/control/pull/405) Remove weird color behaviour on tocuhscreens
- [#369](https://github.com/qitechgmbh/control/pull/369) More failsafe EtherCAT loop
# `2.0.1`
_10.06.2025_
# `2.0.0`
_10.06.2025_
## General
- Initial stable release of QiTech Control.
## Winder V2
- Initial stable release of **Winder V2**
## Features
- Stabilized **Winder V2**
# 2.0.0
Initial stable release of QiTech Control.
## Features
- Stabilized **Winder V2**
Generated
+87 -153
View File
@@ -99,9 +99,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "approx"
@@ -256,7 +256,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -273,7 +273,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -345,7 +345,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -539,7 +539,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -581,34 +581,21 @@ dependencies = [
"approx",
"axum",
"bitvec",
"control_core_derive",
"core_affinity",
"crc",
"erased-serde",
"ethercat_hal",
"ethercrab",
"interfaces",
"libc",
"serde",
"serde_json",
"serial",
"serialport",
"smol",
"socketioxide",
"textplots",
"tracing",
"uom",
]
[[package]]
name = "control_core_derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@@ -625,17 +612,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_affinity"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342"
dependencies = [
"libc",
"num_cpus",
"winapi",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -738,7 +714,7 @@ dependencies = [
"deluxe-macros",
"once_cell",
"proc-macro2",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -751,7 +727,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -766,7 +742,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -777,7 +753,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -814,7 +790,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -1016,7 +992,6 @@ dependencies = [
"bitvec",
"ethercat_hal_derive",
"ethercrab",
"futures",
"rand",
"smol",
"uom",
@@ -1029,7 +1004,7 @@ dependencies = [
"deluxe",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -1081,7 +1056,7 @@ dependencies = [
"criterion",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -1210,7 +1185,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -1746,9 +1721,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
@@ -1825,11 +1800,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
@@ -1953,11 +1928,12 @@ checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"windows-sys 0.52.0",
"overload",
"winapi",
]
[[package]]
@@ -1969,16 +1945,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi 0.5.0",
"libc",
]
[[package]]
name = "object"
version = "0.36.7"
@@ -2085,6 +2051,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.1"
@@ -2178,7 +2150,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -2209,7 +2181,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -2345,7 +2317,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -2377,9 +2349,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
@@ -2415,14 +2387,23 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.2"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
@@ -2433,9 +2414,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -2570,7 +2557,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -2590,14 +2577,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
name = "serde_json"
version = "1.0.143"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -2671,9 +2658,9 @@ dependencies = [
[[package]]
name = "serialport"
version = "4.7.3"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acaf3f973e8616d7ceac415f53fc60e190b2a686fbcf8d27d0256c741c5007b"
checksum = "2daa7abb9b965493e3c8f4184c6f46435484ff2538a332b886788cf6768b927b"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
@@ -2697,13 +2684,11 @@ dependencies = [
"axum",
"bitvec",
"control_core",
"control_core_derive",
"crc",
"dhat",
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
@@ -2712,6 +2697,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
"serial",
"serialport",
"smol",
"socketioxide",
@@ -2844,9 +2830,9 @@ dependencies = [
[[package]]
name = "socketioxide"
version = "0.17.2"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "476190583b592f1e3d55584269600f2d3c4f18af36adad03c41c27e82dcb6bd5"
checksum = "85fe8a90fd0cdac0bcb7b3c40c1084f5658f8737c62dfb277f5620cf78f3849a"
dependencies = [
"bytes",
"engineioxide",
@@ -2951,9 +2937,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.105"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
@@ -2977,7 +2963,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -3022,7 +3008,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -3033,16 +3019,16 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread-priority"
version = "2.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5"
checksum = "cfe075d7053dae61ac5413a34ea7d4913b6e6207844fd726bdd858b37ff72bf5"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"libc",
"log",
"rustversion",
"windows",
"winapi",
]
[[package]]
@@ -3117,9 +3103,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@@ -3139,7 +3125,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -3301,7 +3287,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -3366,15 +3352,15 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
@@ -3558,7 +3544,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
"wasm-bindgen-shared",
]
@@ -3593,7 +3579,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3658,28 +3644,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -3693,17 +3657,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -3712,7 +3665,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -3723,24 +3676,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
name = "windows-link"
version = "0.1.3"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
@@ -3843,15 +3786,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4031,7 +3965,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
"synstructure",
]
@@ -4052,7 +3986,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
[[package]]
@@ -4072,7 +4006,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
"synstructure",
]
@@ -4106,5 +4040,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.105",
"syn 2.0.101",
]
+1 -1
View File
@@ -4,7 +4,7 @@ members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"ethercat-eeprom-dump", "control-core-derive",
"ethercat-eeprom-dump",
]
exclude = []
resolver = "2"
+3 -8
View File
@@ -4,9 +4,6 @@
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
[![](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.*
# Repo Structure
Frontend
@@ -44,8 +41,6 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
## Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
@@ -71,6 +66,7 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
# More Docs
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -94,9 +90,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- Write Device Identification
- Server
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [X] [Threading](./docs/control-loop.md)
- [X] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
+5 -23
View File
@@ -1,31 +1,13 @@
#!/bin/bash
set -e
FEATURE=""
# Check if "mock-machine" was passed as an argument
if [[ "$1" == "mock-machine" ]]; then
echo "Building Debug Code with mock-machine feature"
FEATURE="--features mock-machine"
fi
if [[ "$1" == "release" ]]; then
if [ $1 == "release" ]; then
echo "building Release Code"
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE
echo "building Debug Code"
cargo build
fi
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
echo "Setting permissions for serial ports"
sudo chown "root:$USER" /dev/ttyUSB*
fi
# set cap for server executable
sudo setcap cap_net_raw=eip ./target/debug/server
# run
./target/debug/server
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "control_core_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
File diff suppressed because it is too large. Load diff
-31
View File
@@ -1,31 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DeriveInput, Error};
#[proc_macro_derive(BuildEvent)]
pub fn build_event_derive(item: TokenStream) -> TokenStream {
build_event_derive2(item.into()).unwrap().into()
}
fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
// Parse the input tokens into a syntax tree
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
// Generate the BuildEvent impl
let expanded = quote! {
impl #impl_generics control_core::socketio::event::BuildEvent for #ident #ty_generics #where_clause {
#[doc = "Implemented by the BuildEvent derive macro"]
fn build(&self) -> control_core::socketio::event::Event<Self> {
control_core::socketio::event::Event::new(stringify!(#ident), self.clone())
}
}
};
Ok(expanded)
}
+4 -12
View File
@@ -4,29 +4,21 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.99"
anyhow = "1.0.98"
axum = "0.8.4"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
serialport = "4.7.3"
serde_json = "1.0.140"
serialport = "4.7.1"
smol = "2.0.2"
socketioxide = "0.17.2"
socketioxide = "0.17.1"
uom = "0.36.0"
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.6"
core_affinity = "0.8.3"
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
[target.'cfg(unix)'.dependencies]
libc = "0.2.175"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8"
@@ -0,0 +1,91 @@
use super::Actor;
use ethercat_hal::io::analog_output::AnalogOutput;
use std::{
f64::consts::PI,
future::Future,
pin::Pin,
time::{Duration, Instant},
};
use uom::si::{angle::radian, f64::Angle, ratio::ratio};
pub type AnalogFunction = Box<dyn Fn(Duration) -> f64 + Send + Sync>;
/// Can module analog output with a function
pub struct AnalogFunctionGenerator {
output: AnalogOutput,
function: AnalogFunction,
start_ts: Instant,
}
impl AnalogFunctionGenerator {
pub fn new(output: AnalogOutput, function: AnalogFunction) -> Self {
Self {
output,
function,
start_ts: Instant::now(),
}
}
}
impl Actor for AnalogFunctionGenerator {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let diff = now - self.start_ts;
let value = (self.function)(diff);
(self.output.write)((value as f32).into()).await;
})
}
}
pub fn analog_sine(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
// Convert to seconds as f64 for calculation
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let angle = Angle::new::<radian>(2.0 * PI * phase_secs / period_secs);
let value = angle.sin().get::<ratio>();
value * (amplitude / 2.0) + normal + (amplitude / 2.0)
})
}
pub fn analog_multiply<const N: usize>(functions: [AnalogFunction; N]) -> AnalogFunction {
Box::new(move |dt: Duration| functions.iter().fold(1.0, |acc, func| acc * func(dt)))
}
pub fn analog_sawtooth(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = phase_secs / period_secs;
value * amplitude + normal
})
}
pub fn analog_square(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
1.0
} else {
-1.0
};
value * amplitude + normal
})
}
pub fn analog_triangle(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
phase_secs / period_secs * 2.0
} else {
2.0 - phase_secs / period_secs * 2.0
};
value * amplitude + normal
})
}
@@ -0,0 +1,40 @@
use super::Actor;
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a analog input
#[derive(Debug)]
pub struct AnalogInputGetter {
input: AnalogInput,
normalized: Option<f32>,
}
impl AnalogInputGetter {
pub fn new(input: AnalogInput) -> Self {
Self {
input,
normalized: None,
}
}
/// Value from -1.0 to 1.0
pub fn get_normalized(&self) -> Option<f32> {
self.normalized
}
pub fn get_physical(&self) -> Option<AnalogInputValue> {
match self.get_normalized() {
Some(normalized) => Some(self.input.range.normalized_to_physical(normalized)),
None => return None,
}
}
}
impl Actor for AnalogInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
self.normalized = Some(state.input.normalized);
})
}
}
@@ -0,0 +1,35 @@
use super::Actor;
use ethercat_hal::io::digital_input::DigitalInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a digital input
#[derive(Debug)]
pub struct DigitalInputGetter {
input: DigitalInput,
value: bool,
}
impl DigitalInputGetter {
pub fn new(input: DigitalInput) -> Self {
Self {
input,
value: false,
}
}
/// Get the current value of the digital input
pub fn value(&self) -> bool {
self.value
}
}
impl Actor for DigitalInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)()
.await
.expect("Failed to get digital input state");
self.value = state.input.value;
})
}
}
@@ -0,0 +1,25 @@
use super::Actor;
use ethercat_hal::io::digital_input::DigitalInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a digital input
pub struct DigitalInputLogger {
input: DigitalInput,
}
impl DigitalInputLogger {
pub fn new(input: DigitalInput) -> Self {
Self { input }
}
}
impl Actor for DigitalInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)()
.await
.expect("Failed to get digital input state");
println!("DigitalInputLogger: {}", state.input.value);
})
}
}
@@ -0,0 +1,51 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputBlinker {
last_toggle: Instant,
output: DigitalOutput,
interval: Duration,
enabled: bool,
}
impl DigitalOutputBlinker {
pub fn new(output: DigitalOutput, interval: Duration) -> Self {
Self {
last_toggle: Instant::now(),
output,
interval,
enabled: true,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinker {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
let state = (self.output.state)().await;
if now - self.last_toggle > self.interval {
match state.output.into() {
true => (self.output.write)(false.into()).await,
false => (self.output.write)(true.into()).await,
}
self.last_toggle = now;
}
}
})
}
}
@@ -0,0 +1,57 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a series of digital outputs high in sequence with a given interval
pub struct DigitalOutputBlinkers {
last_toggle: Instant,
outputs: Vec<Option<DigitalOutput>>,
index: usize,
interval: Duration,
enabled: bool,
amount: usize,
}
impl DigitalOutputBlinkers {
pub fn new(outputs: Vec<Option<DigitalOutput>>, interval: Duration, amount: usize) -> Self {
Self {
last_toggle: Instant::now(),
outputs,
interval,
enabled: true,
index: 0,
amount,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinkers {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if now - self.last_toggle > self.interval {
if let Some(output) = &self.outputs[self.index] {
(output.write)(true.into()).await;
}
let index_end =
(self.index + self.outputs.len() - self.amount) % self.outputs.len();
if let Some(output) = &self.outputs[index_end] {
(output.write)(false.into()).await;
}
self.last_toggle = now;
self.index = (self.index + 1) % self.outputs.len();
}
})
}
}
@@ -0,0 +1,38 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{future::Future, pin::Pin, time::Instant};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputSetter {
output: DigitalOutput,
enabled: bool,
}
impl DigitalOutputSetter {
pub fn new(output: DigitalOutput) -> Self {
Self {
output,
enabled: false,
}
}
pub fn set(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn get(&self) -> bool {
self.enabled
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
}
impl Actor for DigitalOutputSetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
(self.output.write)(self.enabled.into()).await;
}
})
}
}
@@ -0,0 +1,666 @@
use super::Actor;
use crate::modbus::{
ModbusFunctionCode, ModbusRequest, ModbusResponse, calculate_modbus_rtu_timeout,
};
use bitvec::{order::Lsb0, slice::BitSlice};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
pin::Pin,
time::{Duration, Instant},
u16,
};
use uom::{
ConstZero,
si::{f64::Frequency, frequency::centihertz},
};
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Resuest we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub enum OperationMode {
PU,
EXT,
NET,
}
impl MitsubishiInverterRS485Actor {
pub fn convert_hz_float_to_word(&mut self, value: Frequency) -> u16 {
let scaled = value.get::<centihertz>(); // Convert Hz to 0.01 Hz units
scaled.round() as u16
}
pub fn set_frequency_target(&mut self, frequency: Frequency) {
let mut request: MitsubishiModbusRequest =
MitsubishiControlRequests::WriteRunningFrequency.into();
let result: u16 = self.convert_hz_float_to_word(frequency); // convert hz float to short
request.request.data[2] = result.to_le_bytes()[1];
request.request.data[3] = result.to_le_bytes()[0];
self.add_request(request);
}
pub fn read_running_frequency() {
// Check if the current element pushed to front is freq event
}
pub fn switch_operation_mode() {}
}
/// Specifies all System environmet Variables
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
enum MitsubishiSystemRegister {
/// Register 40002
InverterReset,
/// Register 40003
ParameterClear,
/// Register 40004
AllParameterClear,
/// Register 40006
ParamClearNonCommunication,
/// Register 40007
AllParameterClearNonCommunication,
/// Register 40009
InverterStatusAndControl,
/// Register 40010
OperationModeAndSetting,
/// Register 40014
RunningFrequencyRAM,
/// Register 40015
RunningFrequencyEEPROM,
/// Register 40201
MotorFrequency,
}
impl MitsubishiControlRequests {
fn get_system_register(value: MitsubishiSystemRegister) -> u16 {
match value {
MitsubishiSystemRegister::InverterReset => 0x1,
MitsubishiSystemRegister::ParameterClear => 0x2,
MitsubishiSystemRegister::AllParameterClear => 0x3,
MitsubishiSystemRegister::ParamClearNonCommunication => 0x5,
MitsubishiSystemRegister::AllParameterClearNonCommunication => 0x6,
MitsubishiSystemRegister::InverterStatusAndControl => 0x8,
MitsubishiSystemRegister::OperationModeAndSetting => 0x9,
MitsubishiSystemRegister::RunningFrequencyRAM => 0x0d,
MitsubishiSystemRegister::RunningFrequencyEEPROM => 0x0e,
MitsubishiSystemRegister::MotorFrequency => 0x00C8,
}
}
}
/*
MitsubishiModbusRequest get executed by their priority
Start and StopMotor are highest priority while writeRunningFrequency and readMotorFrequency are one lower
lets say we had StartMotor and readMotorFrequency the order of execution is:
1. StartMotor
2. readMotorFrequency
this is because StartMotor is higher priority
Since the events do not need to be pushed into a queue this makes the inverter operation more stable
Lets say one request "A" with priority 1 and one with 2 "B" are queued up, assume that request B is frequently used
1. Request "B" is executed due to higher priority
2. When B is added again request A has the same priority because it was ignored. B is executed once again
3. B is added again, now A has an effective priority of 3, which is higher then B
*/
impl From<MitsubishiControlRequests> for MitsubishiModbusRequest {
fn from(request: MitsubishiControlRequests) -> Self {
match request {
MitsubishiControlRequests::WriteRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::WriteFrequency,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::WriteRunningFrequency,
ignored_times: 0,
}
}
MitsubishiControlRequests::ReadInverterStatus => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::InverterStatus,
// Priority is -6 because we do not want to know the status as frequently as the frequency, which is why its priority is lower
// In essence this means for every fifth ReadMotorFrequency request an InverterStatus request is sent
priority: u16::MAX - 6,
control_request_type: MitsubishiControlRequests::ReadInverterStatus,
ignored_times: 0,
}
}
MitsubishiControlRequests::StopMotor => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::InverterControl,
priority: u16::MAX, // StopMotor should have highest priority
control_request_type: MitsubishiControlRequests::StopMotor,
ignored_times: 0,
}
}
MitsubishiControlRequests::StartForwardRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::InverterControl,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::StartForwardRotation,
ignored_times: 0,
}
}
MitsubishiControlRequests::StartReverseRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::InverterControl,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::StartReverseRotation,
ignored_times: 0,
}
}
MitsubishiControlRequests::ReadRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::ReadFrequency,
priority: u16::MAX - 4,
control_request_type: MitsubishiControlRequests::ReadRunningFrequency,
ignored_times: 0,
}
}
MitsubishiControlRequests::ReadMotorFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::MotorFrequency,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
},
request_type: RequestType::OperationCommand,
expected_response_type: ResponseType::ReadMotorFrequency,
priority: u16::MAX - 2,
control_request_type: MitsubishiControlRequests::ReadMotorFrequency,
ignored_times: 0,
}
}
MitsubishiControlRequests::ResetInverter => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterReset,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
},
request_type: RequestType::Reset,
expected_response_type: ResponseType::NoResponse,
priority: u16::MAX,
control_request_type: MitsubishiControlRequests::ResetInverter,
ignored_times: 0,
}
}
MitsubishiControlRequests::ClearAllParameters => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameter => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameters => todo!(),
MitsubishiControlRequests::None => todo!(),
}
}
}
/// These Requests Serve as Templates for controling the inverter
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub enum MitsubishiControlRequests {
None,
/// Register 40002, Reset/Restart the Inverter
ResetInverter,
/// Register 40004, Clear ALL parameters
ClearAllParameters,
/// Register 40006, Clear a non communication parameter
ClearNonCommunicationParameter,
/// Register 40007, Clear all Non Communication related Parameters
ClearNonCommunicationParameters,
/// Register 40009, Read Inverter Status
ReadInverterStatus,
/// Register 40009, Stops the Motor
StopMotor,
/// Register 40009, Starts the Motor in Forward Rotation
StartForwardRotation,
/// Register 40009, Starts the Motor in Reverse Rotation
StartReverseRotation,
/// Register 40014, Read the current frequency the motor runs at (RAM)
ReadRunningFrequency,
/// Register 40014, Write the frequency
WriteRunningFrequency,
/// Read Register 40201, This contains the actual output frequency
ReadMotorFrequency,
}
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
#[derive(Debug, Clone)]
pub struct MitsubishiModbusRequest {
request: ModbusRequest,
control_request_type: MitsubishiControlRequests,
request_type: RequestType,
expected_response_type: ResponseType,
priority: u16,
ignored_times: u32,
}
#[derive(Debug)]
pub enum RotationDirection {
Forward,
Backwards,
Stopped,
}
#[derive(Debug, Clone, Copy)]
pub enum ResponseType {
NoResponse,
ReadFrequency,
ReadMotorFrequency, // Motor Frequency is the actual frequency, that the motor is running at right now
WriteFrequency,
InverterStatus,
InverterControl,
}
#[derive(Debug, Default)]
pub struct MitsubishiInverterStatus {
pub running: bool,
pub forward_running: bool,
pub reverse_running: bool,
pub su: bool,
pub ol: bool,
pub no_function: bool,
pub fu: bool,
pub abc_: bool,
pub fault_occurence: bool,
}
#[derive(Debug)]
pub struct MitsubishiInverterRS485Actor {
// Communication
pub serial_interface: SerialInterface,
pub baudrate: Option<u32>,
pub encoding: Option<SerialEncoding>,
pub request_map: HashMap<MitsubishiControlRequests, MitsubishiModbusRequest>,
pub response: Option<ModbusResponse>,
pub inverter_status: MitsubishiInverterStatus,
// State
pub last_ts: Instant,
pub last_message_size: usize,
pub last_request_type: RequestType,
pub last_control_request_type: MitsubishiControlRequests,
pub state: State,
pub next_response_type: ResponseType,
pub frequency: Frequency,
}
impl MitsubishiInverterRS485Actor {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
last_ts: Instant::now(),
state: State::Uninitialized,
request_map: HashMap::new(),
response: None,
next_response_type: ResponseType::NoResponse,
last_request_type: RequestType::OperationCommand,
last_message_size: 0,
baudrate: None,
encoding: None,
frequency: Frequency::ZERO,
last_control_request_type: MitsubishiControlRequests::ResetInverter,
inverter_status: MitsubishiInverterStatus::default(),
}
}
/// This would get called by the api to add a new request to the inverter
pub fn add_request(&mut self, request: MitsubishiModbusRequest) {
// If a request already exists and its values get replaced, then use the olde requests ignored_times, to ensure its executed at some point
// Otherwise requests that are added frequently are never called, because new requests have ignored_times = 0
if self.request_map.contains_key(&request.control_request_type) {
// unwrap is safe here
let old_request = self.request_map.get(&request.control_request_type).unwrap();
let mut new_request = request.clone();
new_request.ignored_times = old_request.ignored_times;
self.request_map
.insert(request.control_request_type.clone(), new_request);
} else {
self.request_map
.insert(request.control_request_type.clone(), request);
}
}
/// This is used internally to read the receive buffer of the el6021
fn read_modbus_response(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<ModbusResponse, anyhow::Error>> + Send + '_>> {
Box::pin(async move {
let res: Option<Vec<u8>> = (self.serial_interface.read_message)().await;
let raw_response = match res {
Some(res) => res,
None => {
vec![]
}
};
let response: Result<ModbusResponse, _> =
ModbusResponse::try_from(raw_response.clone());
match response {
Ok(result) => {
self.last_message_size = result.clone().data.len() + 4;
self.state = State::WaitingForReceiveAccept;
Ok(result)
}
Err(_) => {
self.last_message_size = 22;
// self.state = State::WaitingForReceiveAccept;
Err(anyhow::anyhow!("error"))
}
}
})
}
fn set_ignored_times_modbus_requests(&mut self) {
for (_, value) in self.request_map.iter_mut() {
value.ignored_times += 1;
}
}
/// This is used internally to fill the write buffer of the el6021 with the modbus request
/// Decides what requests to send first by finding the one with the highest priority
/// For example Highest Priority requests: ResetInverter StopMotor
fn send_modbus_request(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if self.request_map.len() == 0 {
return;
};
let mut highest_prio_request: Option<&mut MitsubishiModbusRequest> = None;
let mut highest_priority: u32 = 0;
for (_, value) in self.request_map.iter_mut() {
// borrowchecker complaining
let priority = value.priority as u32;
let ignored_times = value.ignored_times;
let effective_priority: u32 = priority as u32 + ignored_times;
if effective_priority > highest_priority {
highest_prio_request = Some(value);
highest_priority = effective_priority;
}
}
let request = match highest_prio_request {
Some(request) => request,
None => return,
};
let modbus_request: Vec<u8> = request.request.clone().into();
let res = (self.serial_interface.write_message)(modbus_request.clone()).await;
match res {
Ok(_) => {
self.next_response_type = request.expected_response_type;
self.last_request_type = request.request_type;
self.last_control_request_type = request.control_request_type.clone();
}
Err(_) => tracing::error!("ERROR: serial_interface.write_message has failed"),
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum RequestType {
None,
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
OperationCommand,
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
ReadWrite,
/// Less than 5 seconds timeout for Response
ParamClear,
/// Supposedly no waiting time, however inverter takes a while to start ~300ms should be more than enough
Reset,
}
impl RequestType {
fn timeout_duration(self) -> Duration {
match self {
RequestType::OperationCommand => Duration::from_millis(12),
RequestType::ReadWrite => Duration::from_millis(30),
RequestType::ParamClear => Duration::from_millis(5000),
RequestType::Reset => Duration::from_millis(300),
RequestType::None => Duration::from_millis(12),
}
}
}
pub enum MitsubishiModbusExceptionCode {
IllegalFunction,
IllegalDataAddress,
IllegalDataValue,
None,
}
impl From<MitsubishiModbusExceptionCode> for u8 {
fn from(value: MitsubishiModbusExceptionCode) -> Self {
match value {
MitsubishiModbusExceptionCode::None => 0,
MitsubishiModbusExceptionCode::IllegalFunction => 1,
MitsubishiModbusExceptionCode::IllegalDataAddress => 2,
MitsubishiModbusExceptionCode::IllegalDataValue => 3,
}
}
}
impl From<u8> for MitsubishiModbusExceptionCode {
fn from(value: u8) -> Self {
match value {
1 => MitsubishiModbusExceptionCode::IllegalFunction,
2 => MitsubishiModbusExceptionCode::IllegalDataAddress,
3 => MitsubishiModbusExceptionCode::IllegalDataValue,
_ => MitsubishiModbusExceptionCode::None,
}
}
}
// Handle different Response types
impl MitsubishiInverterRS485Actor {
fn handle_motor_frequency(&mut self, resp: ModbusResponse) {
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
let raw_frequency = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f64;
self.frequency = Frequency::new::<centihertz>(raw_frequency);
}
fn handle_read_inverter_status(&mut self, resp: ModbusResponse) {
let status_bytes: [u8; 2] = resp.data[1..3].try_into().unwrap();
let bits: &BitSlice<u8, Lsb0> = BitSlice::<_, Lsb0>::from_slice(&status_bytes);
self.inverter_status = MitsubishiInverterStatus {
running: bits[8],
forward_running: bits[9],
reverse_running: bits[10],
su: bits[11],
ol: bits[12],
no_function: bits[13],
fu: bits[14],
abc_: bits[15],
fault_occurence: bits[7],
};
}
async fn handle_no_response(&mut self) {}
async fn handle_response(&mut self, resp: ModbusResponse) {
match self.next_response_type {
ResponseType::ReadFrequency => (),
ResponseType::WriteFrequency => (),
ResponseType::ReadMotorFrequency => self.handle_motor_frequency(resp),
ResponseType::InverterStatus => self.handle_read_inverter_status(resp),
ResponseType::InverterControl => (),
ResponseType::NoResponse => self.handle_no_response().await,
}
}
}
impl Actor for MitsubishiInverterRS485Actor {
fn act(&mut self, now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let elapsed: Duration = now_ts.duration_since(self.last_ts);
// State is uninitialized until serial interface init returns true, which takes a few cycles on the el6021
if let State::Uninitialized = self.state {
let res = (self.serial_interface.initialize)().await;
if res == true {
self.state = State::ReadyToSend;
// every time when our inverter is "Uninitialzed" reset it first to clear any error states it may have
self.add_request(MitsubishiControlRequests::ResetInverter.into());
self.baudrate = (self.serial_interface.get_baudrate)().await;
self.encoding = (self.serial_interface.get_serial_encoding)().await;
self.last_ts = now_ts;
return;
} else {
return;
}
}
let encoding = match self.encoding {
Some(encoding) => encoding,
None => return,
};
let baudrate = match self.baudrate {
Some(baudrate) => baudrate,
None => return,
};
let timeout = calculate_modbus_rtu_timeout(
encoding.total_bits(),
self.last_request_type.timeout_duration(),
baudrate,
self.last_message_size,
);
if elapsed < timeout {
return;
}
self.add_request(MitsubishiControlRequests::ReadMotorFrequency.into());
self.add_request(MitsubishiControlRequests::ReadInverterStatus.into());
self.last_ts = now_ts;
match self.state {
State::WaitingForResponse => {
let ret = self.read_modbus_response().await;
match ret {
Ok(ret) => {
self.handle_response(ret).await;
}
Err(_) => {
if let MitsubishiControlRequests::ResetInverter =
self.last_control_request_type
{
self.state = State::ReadyToSend;
}
}
}
self.next_response_type = ResponseType::NoResponse;
}
State::ReadyToSend => {
self.send_modbus_request().await;
self.request_map.remove(&self.last_control_request_type);
self.set_ignored_times_modbus_requests();
}
State::WaitingForReceiveAccept => {
_ = (self.serial_interface.read_finished)().await;
self.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
let res = (self.serial_interface.write_finished)().await;
if res == true {
self.state = State::WaitingForResponse;
}
}
_ => (),
}
self.response = None;
})
}
}
+19
View File
@@ -0,0 +1,19 @@
pub mod analog_function_generator;
pub mod analog_input_getter;
pub mod digital_input_getter;
pub mod digital_input_logger;
pub mod digital_output_blinker;
pub mod digital_output_blinkers;
pub mod digital_output_setter;
pub mod mitsubishi_inverter_rs485;
pub mod stepper_driver_el70x1;
pub mod stepper_driver_max_speed;
pub mod stepper_driver_pulse_train;
pub mod temperature_input_getter;
pub mod temperature_input_logger;
use std::{pin::Pin, time::Instant};
pub trait Actor: Send + Sync {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
@@ -0,0 +1,91 @@
use super::Actor;
use core::panic;
use ethercat_hal::{
helpers::el70xx_velocity_converter::EL70x1VelocityConverter,
io::stepper_velocity_el70x1::StepperVelocityEL70x1, shared_config::el70x1::EL70x1SpeedRange,
};
use std::{future::Future, pin::Pin, time::Instant};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverEL70x1 {
stepper: StepperVelocityEL70x1,
enabled: bool,
velocity: i16,
position: i128,
set_position: Option<i128>,
pub converter: EL70x1VelocityConverter,
}
impl StepperDriverEL70x1 {
pub fn new(stepper: StepperVelocityEL70x1, speed_range: &EL70x1SpeedRange) -> Self {
Self {
stepper,
enabled: false,
velocity: 0,
converter: EL70x1VelocityConverter::new(speed_range),
position: 0,
set_position: None,
}
}
/// Set the speed in steps per second
pub fn set_speed(&mut self, steps_per_second: i32) {
self.velocity = self.converter.steps_to_velocity(steps_per_second, true)
}
/// Get the speed in steps per second
pub fn get_speed(&self) -> i32 {
self.converter.velocity_to_steps(self.velocity, true) as i32
}
/// Enable or disable the stepper
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Get the enabled state of the stepper
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Get the current position of the stepper
pub fn get_position(&self) -> i128 {
self.position
}
/// Set the position of the stepper
pub fn set_position(&mut self, position: i128) {
self.set_position = Some(position);
}
}
impl Actor for StepperDriverEL70x1 {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = match (self.stepper.state)().await {
Ok(state) => state,
Err(e) => {
panic!("Error while reading StepperVelocity {:?}", e);
}
};
// set the input
self.position = state.input.counter_value;
let mut output = state.output.clone();
// set the output
output.enable = self.enabled;
output.velocity = self.velocity;
output.set_counter = self.set_position.take();
// write the output
match (self.stepper.write)(output).await {
Ok(_) => {}
Err(e) => {
panic!("Error while writing StepperVelocity {:?}", e);
}
}
})
}
}
@@ -0,0 +1,37 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock},
time::Instant,
};
/// Set a digital output high and low with a given interval
pub struct StepperDriverMaxSpeed {
pulse: DigitalOutput,
}
impl StepperDriverMaxSpeed {
pub fn new(output: DigitalOutput) -> Self {
Self { pulse: output }
}
}
impl Actor for StepperDriverMaxSpeed {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
match state.output.into() {
true => (self.pulse.write)(false.into()).await,
false => (self.pulse.write)(true.into()).await,
}
})
}
}
impl From<StepperDriverMaxSpeed> for Arc<RwLock<StepperDriverMaxSpeed>> {
fn from(actor: StepperDriverMaxSpeed) -> Self {
Arc::new(RwLock::new(actor))
}
}
@@ -0,0 +1,222 @@
/// Translates the encoder value to a larger space and handle underflow/overflow situations
#[derive(Debug)]
pub struct Encoder {
pub position: i64,
pub last_encoder_position: u32,
}
impl Encoder {
pub fn new() -> Self {
Self {
position: 0,
last_encoder_position: 0,
}
}
pub fn update(&mut self, counter_value: u32, counter_overflow: bool, counter_underflow: bool) {
// Handle overflow case
if counter_overflow {
// When overflow occurs, the counter wraps from MAX to 0
// Calculate how much it moved past MAX
let delta = counter_value as i64;
// Add the remaining distance from last position to MAX, plus the overflow amount
self.position += (u32::MAX - self.last_encoder_position) as i64 + 1 + delta;
}
// Handle underflow case
else if counter_underflow {
// When underflow occurs, the counter wraps from 0 to MAX
// Calculate how much it moved below 0
let delta = (u32::MAX - counter_value) as i64;
// Subtract the distance from last position to 0, plus the underflow amount
self.position -= self.last_encoder_position as i64 + 1 + delta;
}
// Normal case - no overflow/underflow
else {
// Calculate the signed difference between current and last position
let delta = counter_value as i64 - self.last_encoder_position as i64;
// Update the position
self.position += delta;
}
// Update the last encoder position for next time
self.last_encoder_position = counter_value;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests that the position tracker correctly handles normal forward movement
/// without any overflow conditions.
///
/// This test verifies:
/// 1. Initial state is zeroed
/// 2. Moving forward updates both position and last_encoder_position
/// 3. Consecutive forward movements accumulate correctly
#[test]
fn test_normal_forward_movement() {
let mut tracker = Encoder::new();
// Initial state
assert_eq!(tracker.position, 0);
assert_eq!(tracker.last_encoder_position, 0);
// Move forward by 100
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
assert_eq!(tracker.last_encoder_position, 100);
// Move forward by another 50
tracker.update(150, false, false);
assert_eq!(tracker.position, 150);
assert_eq!(tracker.last_encoder_position, 150);
}
/// Tests that the position tracker correctly handles normal backward movement
/// without any underflow conditions.
///
/// This test verifies:
/// 1. Starting from a non-zero position
/// 2. Moving backward updates both position and last_encoder_position
/// 3. Consecutive backward movements accumulate correctly
#[test]
fn test_normal_backward_movement() {
let mut tracker = Encoder::new();
// Start at position 1000
tracker.position = 1000;
tracker.last_encoder_position = 1000;
// Move backward by 100
tracker.update(900, false, false);
assert_eq!(tracker.position, 900);
assert_eq!(tracker.last_encoder_position, 900);
// Move backward by another 50
tracker.update(850, false, false);
assert_eq!(tracker.position, 850);
assert_eq!(tracker.last_encoder_position, 850);
}
/// Tests that the position tracker correctly handles overflow conditions
/// where the encoder counter wraps from near its maximum value to a small value.
///
/// This test verifies:
/// 1. When overflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_overflow() {
let mut tracker = Encoder::new();
// Set position near the maximum u32 value
tracker.position = 100;
tracker.last_encoder_position = u32::MAX - 10;
// Overflow: counter goes from (MAX-10) to 5
// This means we moved forward by 10 steps to MAX, then 1 step to overflow,
// then 5 more steps to reach the current position
tracker.update(5, true, false);
// Expected: position += (10 + 1 + 5) = position += 16
assert_eq!(tracker.position, 116);
assert_eq!(tracker.last_encoder_position, 5);
}
/// Tests that the position tracker correctly handles underflow conditions
/// where the encoder counter wraps from near zero to a value near its maximum.
///
/// This test verifies:
/// 1. When underflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled (in negative direction)
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_underflow() {
let mut tracker = Encoder::new();
// Set position near zero
tracker.position = 100;
tracker.last_encoder_position = 10;
// Underflow: counter goes from 10 to (MAX-5)
// This means we moved backward by 10 steps to 0, then 1 step to underflow,
// then 5 more steps to reach the current position
tracker.update(u32::MAX - 5, false, true);
// Expected: position -= (10 + 1 + 5) = position -= 16
assert_eq!(tracker.position, 84);
assert_eq!(tracker.last_encoder_position, u32::MAX - 5);
}
/// Tests that the position tracker maintains correct behavior across
/// a sequence of different operations including normal movement,
/// overflow, and underflow.
///
/// This test verifies:
/// 1. The tracker maintains state correctly between operations
/// 2. Different types of movements can be handled in sequence
/// 3. Position calculations remain accurate throughout various scenarios
#[test]
fn test_multiple_operations() {
let mut tracker = Encoder::new();
// Normal forward
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
// Normal backward
tracker.update(50, false, false);
assert_eq!(tracker.position, 50);
// Overflow
tracker.last_encoder_position = u32::MAX - 5;
tracker.position = 50; // Reset for clarity
tracker.update(10, true, false);
assert_eq!(tracker.position, 66); // 50 + (5 + 1 + 10)
// Underflow
tracker.last_encoder_position = 5;
tracker.position = 100; // Reset for clarity
tracker.update(u32::MAX - 10, false, true);
assert_eq!(tracker.position, 84); // 100 - (5 + 1 + 10)
}
/// Tests that the position tracker can handle multiple overflow and underflow
/// events, maintaining an accurate position count that exceeds the u32 range.
///
/// This test verifies:
/// 1. Multiple overflow events accumulate correctly in the i64 position
/// 2. Multiple underflow events correctly subtract from the position
/// 3. The i64 position field properly extends the range beyond u32 limits
#[test]
fn test_large_movements() {
let mut tracker = Encoder::new();
// Test tracking beyond u32 limits with multiple overflows
for _ in 0..5 {
// Simulate overflow: move from (MAX-100) to 100
tracker.last_encoder_position = u32::MAX - 100;
tracker.update(100, true, false);
// Each iteration adds (100 + 1 + 100) = 201 to position
// We move 100 steps to MAX, 1 step to overflow, and 100 more steps to reach 100
}
// After 5 iterations, position should be 5 * 201 = 1005
assert_eq!(tracker.position, 1005);
// Test tracking with multiple underflows
for _ in 0..3 {
// Simulate underflow: move from 100 to (MAX-100)
tracker.last_encoder_position = 100;
tracker.update(u32::MAX - 100, false, true);
// Each iteration subtracts (100 + 1 + 100) = 201 from position
// We move 100 steps to 0, 1 step to underflow, and 100 more steps to reach MAX-100
}
// Final position: 1005 - (3 * 201) = 1005 - 603 = 402
assert_eq!(tracker.position, 402);
}
}
@@ -0,0 +1,51 @@
use super::Actor;
use encoder::Encoder;
use ethercat_hal::io::pulse_train_output::PulseTrainOutput;
use std::{future::Future, pin::Pin, time::Instant};
pub mod encoder;
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverPulseTrain {
pulse: PulseTrainOutput,
encoder: Encoder,
frequency: i32,
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn new(output: PulseTrainOutput) -> Self {
Self {
pulse: output,
encoder: Encoder::new(),
frequency: 0,
}
}
}
impl<'ptodevice> Actor for StepperDriverPulseTrain {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
let mut output = state.output.clone();
// sync the counter to the encoder
self.encoder.update(
state.input.counter_value,
state.input.counter_overflow,
state.input.counter_underflow,
);
// write frequency
output.frequency_value = self.frequency;
(self.pulse.write)(output).await;
})
}
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn set_frequency(&mut self, frequency: i32) {
self.frequency = frequency;
}
}
@@ -0,0 +1,43 @@
use std::{pin::Pin, time::Instant};
use ethercat_hal::io::temperature_input::TemperatureInput;
use super::Actor;
#[derive(Debug)]
pub struct TemperatureInputGetter {
input: TemperatureInput,
temperature: f64,
wiring_error: bool,
}
impl TemperatureInputGetter {
pub fn new(input: TemperatureInput) -> Self {
Self {
input,
temperature: 0.0,
wiring_error: false,
}
}
pub fn get_temperature(&self) -> f64 {
self.temperature
}
pub fn get_wiring_error(&self) -> bool {
self.wiring_error
}
}
impl Actor for TemperatureInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let input = (self.input.state)().await.input;
self.temperature = input.temperature as f64;
self.wiring_error = input.overvoltage | input.undervoltage;
if self.wiring_error {
self.temperature = 0.0;
}
})
}
}
@@ -0,0 +1,23 @@
use super::Actor;
use ethercat_hal::io::temperature_input::TemperatureInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a temperature input
pub struct TemperatureInputLogger {
input: TemperatureInput,
}
impl TemperatureInputLogger {
pub fn new(input: TemperatureInput) -> Self {
Self { input }
}
}
impl Actor for TemperatureInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
println!("TemperatureInputLogger: {:?}C", state.input.temperature);
})
}
}
@@ -1,241 +0,0 @@
use std::time::Instant;
#[derive(Debug)]
pub struct ClampingTimeagnosticPidController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Derivative error
ed: f64,
min_ep: Option<f64>,
max_ep: Option<f64>,
min_ei: Option<f64>,
max_ei: Option<f64>,
min_ed: Option<f64>,
max_ed: Option<f64>,
min_signal: Option<f64>,
max_signal: Option<f64>,
last: Option<Instant>,
}
impl ClampingTimeagnosticPidController {
pub fn new(
kp: f64,
ki: f64,
kd: f64,
min_ep: Option<f64>,
max_ep: Option<f64>,
min_ei: Option<f64>,
max_ei: Option<f64>,
min_ed: Option<f64>,
max_ed: Option<f64>,
min_signal: Option<f64>,
max_signal: Option<f64>,
) -> Self {
Self {
kp,
ki,
kd,
ep: 0.0,
ei: 0.0,
ed: 0.0,
last: None,
min_ep,
max_ep,
min_ei,
max_ei,
min_ed,
max_ed,
min_signal,
max_signal,
}
}
pub fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
kd,
ep: 0.0,
ei: 0.0,
ed: 0.0,
min_ep: None,
max_ep: None,
min_ei: None,
max_ei: None,
min_ed: None,
max_ed: None,
last: None,
min_signal: None,
max_signal: None,
}
}
pub fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_kd(&self) -> f64 {
self.kd
}
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let signal = match self.last {
// First update
None => {
// Calculate error
let ep = error;
// Calculate signal
let signal = self.kp * ep;
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
signal,
self.min_signal,
self.max_signal,
);
// Set values
self.ep =
ClampingTimeagnosticPidController::optional_clamp(ep, self.min_ep, self.max_ep);
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
clamped_signal
}
// Subsequent updates
Some(last) => {
// Calculate the time delta in seconds
let dt = t.duration_since(last).as_secs_f64();
// Calculate errors
let ep = ClampingTimeagnosticPidController::optional_clamp(
error,
self.min_ep,
self.max_ep,
);
let ei = ClampingTimeagnosticPidController::optional_clamp(
self.ei + ep * dt,
self.min_ei,
self.max_ei,
);
let ed = ClampingTimeagnosticPidController::optional_clamp(
(ep - self.ep) / dt,
self.min_ed,
self.max_ed,
);
// Make factors timeagnostic
let kp = self.kp * dt;
let ki = self.ki * dt;
let kd = self.kd * dt;
// Calculate signal
let signal = kp * ep + ki * ei + kd * ed;
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
signal,
self.min_signal,
self.max_signal,
);
// Set values
self.ep = ep;
self.ei = ei;
self.ed = ed;
self.last = Some(t);
clamped_signal
}
};
signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
self.ei = 0.0;
self.ed = 0.0;
self.last = None;
}
}
#[cfg(test)]
mod tests {
use crate::controllers::clamping_timeagnostic_pid::ClampingTimeagnosticPidController;
#[test]
fn test_optional_clamp_with_both_bounds() {
let val = 5.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(1.0), Some(4.0));
assert_eq!(clamped, 4.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(6.0), Some(10.0));
assert_eq!(clamped, 6.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), Some(6.0));
assert_eq!(clamped, 5.0);
}
#[test]
fn test_optional_clamp_with_only_min() {
let val = 3.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(5.0), None);
assert_eq!(clamped, 5.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), None);
assert_eq!(clamped, 3.0);
}
#[test]
fn test_optional_clamp_with_only_max() {
let val = 7.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(6.0));
assert_eq!(clamped, 6.0);
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(8.0));
assert_eq!(clamped, 7.0);
}
#[test]
fn test_optional_clamp_with_no_bounds() {
let val = 42.0;
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, None);
assert_eq!(clamped, 42.0);
}
}
@@ -44,10 +44,10 @@ impl AccelerationSpeedController {
/// No speed limits are applied.
pub fn new_simple(max_acceleration: f64, initial_speed: f64) -> Self {
Self::new(
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
initial_speed,
)
}
@@ -37,15 +37,12 @@ impl AngularAccelerationSpeedController {
/// Creates a new angular acceleration speed controller with simplified parameters.
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
/// No speed limits are applied.
pub fn new_simple(
max_acceleration: AngularAcceleration,
initial_speed: AngularVelocity,
) -> Self {
pub fn new_simple(max_acceleration: AngularAcceleration, initial_speed: AngularVelocity) -> Self {
Self::new(
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
None, // min_speed
None, // max_speed
-max_acceleration, // min_acceleration (deceleration)
max_acceleration, // max_acceleration
initial_speed,
)
}
@@ -38,10 +38,10 @@ impl LinearAccelerationLimitingController {
/// No speed limits are applied.
pub fn new_simple(acceleration: Acceleration, initial_speed: Velocity) -> Self {
Self::new(
None, // min_speed
None, // max_speed
acceleration, // acceleration
-acceleration, // deceleration (negative for deceleration)
None, // min_speed
None, // max_speed
acceleration, // acceleration
-acceleration, // deceleration (negative for deceleration)
initial_speed,
)
}
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod clamping_timeagnostic_pid;
pub mod first_degree_motion;
pub mod pid;
pub mod second_degree_motion;
pub mod timeagnostic_pid;
+1 -1
View File
@@ -9,6 +9,7 @@ pub struct PidController {
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
@@ -65,7 +66,6 @@ impl PidController {
// Set values
self.ep = ep;
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
@@ -0,0 +1,92 @@
use std::time::Instant;
#[derive(Debug)]
pub struct TimeagnosticPidController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Derivative error
ed: f64,
last: Option<Instant>,
}
impl TimeagnosticPidController {
pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
kd,
ep: 0.0,
ei: 0.0,
ed: 0.0,
last: None,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let signal = match self.last {
// First update
None => {
// Calculate error
let ep = error;
// Calculate signal
let signal = self.kp * ep;
// Set values
self.ep = ep;
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
signal
}
// Subsequent updates
Some(last) => {
// Calculate the time delta in seconds
let dt = t.duration_since(last).as_secs_f64();
// Calculate errors
let ep = error;
let ei = self.ei + ep * dt;
let ed = (ep - self.ep) / dt;
// Make factors timeagnostic
let kp = self.kp / dt;
let ki = self.ki / dt;
let kd = self.kd / dt;
// Calculate signal
let signal = kp * ep + ki * ei + kd * ed;
// Set values
self.ep = ep;
self.ei = ei;
self.ed = ed;
self.last = Some(t);
signal
}
};
signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
self.ei = 0.0;
self.ed = 0.0;
self.last = None;
}
}
@@ -1,279 +0,0 @@
//! # Circular Converter
//!
//! This module provides the `CircularConverter` which handles transformations between
//! linear and angular measurements for circular motion systems.
//!
//! ## Usage
//!
//! The `CircularConverter` is designed to work alongside other converters:
//! - Use `AngularStepConverter` for angular-to-step conversions
//! - Use `LinearStepConverter` for complete linear-to-step conversions (uses `CircularConverter` internally)
//! - Use `CircularConverter` directly when you need linear-to-angular conversions without steps
//!
//! ## Examples
//!
//! ```rust
//! use control_core::converters::circular_converter::CircularConverter;
//! 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 uom::si::{
acceleration::meter_per_second_squared,
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
/// A converter that handles transformations between linear and angular measurements
/// for circular motion. This includes conversions for position, velocity, acceleration,
/// and higher-order derivatives based on a circle's geometric properties.
#[derive(Debug, Clone)]
pub struct CircularConverter {
radius: Length,
circumference: Length,
diameter: Length,
}
impl CircularConverter {
/// Create a new converter from radius
pub fn from_radius(radius: Length) -> Self {
let diameter = radius * 2.0;
let circumference =
Length::new::<meter>(2.0 * std::f64::consts::PI * radius.get::<meter>());
Self {
radius,
diameter,
circumference,
}
}
/// Create a new converter from diameter
pub fn from_diameter(diameter: Length) -> Self {
let radius = diameter / 2.0;
Self::from_radius(radius)
}
/// Create a new converter from circumference
pub fn from_circumference(circumference: Length) -> Self {
let radius =
Length::new::<meter>(circumference.get::<meter>() / (2.0 * std::f64::consts::PI));
Self::from_radius(radius)
}
/// Get the radius
pub fn radius(&self) -> Length {
self.radius
}
/// Get the diameter
pub fn diameter(&self) -> Length {
self.diameter
}
/// Get the circumference
pub fn circumference(&self) -> Length {
self.circumference
}
// Linear to Angular Position Conversions
/// Convert linear distance to angular position
/// Formula: angle = distance / radius
pub fn linear_to_angular_position(&self, distance: Length) -> Angle {
let angle_rad = distance.get::<meter>() / self.radius.get::<meter>();
Angle::new::<radian>(angle_rad)
}
/// Convert angular position to linear distance
/// Formula: distance = angle * radius
pub fn angular_to_linear_position(&self, angle: Angle) -> Length {
let distance = angle.get::<radian>() * self.radius.get::<meter>();
Length::new::<meter>(distance)
}
// Linear to Angular Velocity Conversions
/// Convert linear velocity to angular velocity
/// Formula: angular_velocity = linear_velocity / radius
pub fn linear_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
let angular_velocity_rad_per_s =
velocity.get::<meter_per_second>() / self.radius.get::<meter>();
AngularVelocity::new::<radian_per_second>(angular_velocity_rad_per_s)
}
/// Convert angular velocity to linear velocity
/// Formula: linear_velocity = angular_velocity * radius
pub fn angular_to_linear_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
let velocity = angular_velocity.get::<radian_per_second>() * self.radius.get::<meter>();
Velocity::new::<meter_per_second>(velocity)
}
// Linear to Angular Acceleration Conversions
/// Convert linear acceleration to angular acceleration
/// Formula: angular_acceleration = linear_acceleration / radius
pub fn linear_to_angular_acceleration(
&self,
acceleration: Acceleration,
) -> AngularAcceleration {
let angular_acceleration_rad_per_s2 =
acceleration.get::<meter_per_second_squared>() / self.radius.get::<meter>();
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2)
}
/// Convert angular acceleration to linear acceleration
/// Formula: linear_acceleration = angular_acceleration * radius
pub fn angular_to_linear_acceleration(
&self,
angular_acceleration: AngularAcceleration,
) -> Acceleration {
let acceleration =
angular_acceleration.get::<radian_per_second_squared>() * self.radius.get::<meter>();
Acceleration::new::<meter_per_second_squared>(acceleration)
}
// Jerk Conversions (using f64 for now since UOM may not have jerk units)
/// Convert linear jerk to angular jerk
/// Formula: angular_jerk = linear_jerk / radius
/// Units: rad/s³ = (m/s³) / m
pub fn linear_to_angular_jerk(&self, linear_jerk: f64) -> f64 {
linear_jerk / self.radius.get::<meter>()
}
/// Convert angular jerk to linear jerk
/// Formula: linear_jerk = angular_jerk * radius
/// Units: m/s³ = (rad/s³) * m
pub fn angular_to_linear_jerk(&self, angular_jerk: f64) -> f64 {
angular_jerk * self.radius.get::<meter>()
}
// Arc Length Calculations
/// Calculate arc length from angle
/// Formula: arc_length = angle * radius
pub fn angle_to_arc_length(&self, angle: Angle) -> Length {
self.angular_to_linear_position(angle)
}
/// Calculate angle from arc length
/// Formula: angle = arc_length / radius
pub fn arc_length_to_angle(&self, arc_length: Length) -> Angle {
self.linear_to_angular_position(arc_length)
}
// Circumference-based calculations
/// Convert linear distance to number of revolutions
/// Formula: revolutions = distance / circumference
pub fn linear_distance_to_revolutions(&self, distance: Length) -> f64 {
distance.get::<meter>() / self.circumference.get::<meter>()
}
/// Convert number of revolutions to linear distance
/// Formula: distance = revolutions * circumference
pub fn revolutions_to_linear_distance(&self, revolutions: f64) -> Length {
Length::new::<meter>(revolutions * self.circumference.get::<meter>())
}
/// Convert linear velocity to revolutions per second
/// Formula: rps = velocity / circumference
pub fn linear_velocity_to_rps(&self, velocity: Velocity) -> f64 {
velocity.get::<meter_per_second>() / self.circumference.get::<meter>()
}
/// Convert revolutions per second to linear velocity
/// Formula: velocity = rps * circumference
pub fn rps_to_linear_velocity(&self, rps: f64) -> Velocity {
Velocity::new::<meter_per_second>(rps * self.circumference.get::<meter>())
}
}
#[cfg(test)]
mod tests {
use super::*;
use uom::si::length::centimeter;
#[test]
fn test_constructors() {
let radius = Length::new::<centimeter>(5.0);
let diameter = Length::new::<centimeter>(10.0);
let circumference = Length::new::<centimeter>(2.0 * std::f64::consts::PI * 5.0);
let converter1 = CircularConverter::from_radius(radius);
let converter2 = CircularConverter::from_diameter(diameter);
let converter3 = CircularConverter::from_circumference(circumference);
// All should have approximately the same values
assert!((converter1.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter2.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter3.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
assert!((converter1.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
assert!((converter2.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
assert!((converter3.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
}
#[test]
fn test_position_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
// Test linear to angular
let distance = Length::new::<meter>(std::f64::consts::PI);
let angle = converter.linear_to_angular_position(distance);
assert!((angle.get::<radian>() - std::f64::consts::PI).abs() < 1e-10);
// Test angular to linear
let angle = Angle::new::<radian>(std::f64::consts::PI / 2.0);
let distance = converter.angular_to_linear_position(angle);
assert!((distance.get::<meter>() - std::f64::consts::PI / 2.0).abs() < 1e-10);
}
#[test]
fn test_velocity_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(0.5));
// Test linear to angular
let velocity = Velocity::new::<meter_per_second>(1.0);
let angular_velocity = converter.linear_to_angular_velocity(velocity);
assert!((angular_velocity.get::<radian_per_second>() - 2.0).abs() < 1e-10);
// Test angular to linear
let angular_velocity = AngularVelocity::new::<radian_per_second>(4.0);
let velocity = converter.angular_to_linear_velocity(angular_velocity);
assert!((velocity.get::<meter_per_second>() - 2.0).abs() < 1e-10);
}
#[test]
fn test_revolution_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
// One full revolution should equal circumference
let revolutions = 1.0;
let distance = converter.revolutions_to_linear_distance(revolutions);
assert!((distance.get::<meter>() - converter.circumference().get::<meter>()).abs() < 1e-10);
// Converting back should give 1 revolution
let calculated_revolutions = converter.linear_distance_to_revolutions(distance);
assert!((calculated_revolutions - 1.0).abs() < 1e-10);
}
#[test]
fn test_jerk_conversions() {
let converter = CircularConverter::from_radius(Length::new::<meter>(2.0));
let linear_jerk = 4.0; // m/s³
let angular_jerk = converter.linear_to_angular_jerk(linear_jerk);
assert!((angular_jerk - 2.0).abs() < 1e-10); // 4.0 / 2.0 = 2.0 rad/s³
let calculated_linear_jerk = converter.angular_to_linear_jerk(angular_jerk);
assert!((calculated_linear_jerk - linear_jerk).abs() < 1e-10);
}
}
@@ -1,11 +1,14 @@
use uom::si::{
acceleration::meter_per_second_squared,
angle::revolution,
angular_acceleration::radian_per_second_squared,
angular_velocity::revolution_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
use super::angular_step_converter::AngularStepConverter;
use super::circular_converter::CircularConverter;
/// Converts between linear measurements and motor steps
///
@@ -15,48 +18,53 @@ use super::circular_converter::CircularConverter;
#[derive(Debug, Clone)]
pub struct LinearStepConverter {
angular_step_converter: AngularStepConverter,
circular_converter: CircularConverter,
radius: Length,
circumference: Length,
}
// Constructor and basic getters
impl LinearStepConverter {
/// Create a new converter from radius and steps per revolution
pub fn from_radius(steps_per_revolution: i16, radius: Length) -> Self {
let circumference =
Length::new::<meter>(2.0 * std::f64::consts::PI * radius.get::<meter>());
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_radius(radius),
radius,
circumference,
}
}
/// Create a new converter from diameter and steps per revolution
pub fn from_diameter(steps_per_revolution: i16, diameter: Length) -> Self {
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_diameter(diameter),
}
let radius = diameter / 2.0;
Self::from_radius(steps_per_revolution, radius)
}
/// Create a new converter from circumference and steps per revolution
pub fn from_circumference(steps_per_revolution: i16, circumference: Length) -> Self {
let radius =
Length::new::<meter>(circumference.get::<meter>() / (2.0 * std::f64::consts::PI));
Self {
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
circular_converter: CircularConverter::from_circumference(circumference),
radius,
circumference,
}
}
/// Get the radius used by the converter
pub fn radius(&self) -> Length {
self.circular_converter.radius()
self.radius
}
/// Get the diameter of the system
pub fn diameter(&self) -> Length {
self.circular_converter.diameter()
self.radius * 2.0
}
/// Get the circumference of the system
pub fn circumference(&self) -> Length {
self.circular_converter.circumference()
self.circumference
}
/// Get the steps per revolution
@@ -71,10 +79,8 @@ impl LinearStepConverter {
///
/// Formula: steps = (distance / circumference) * steps_per_revolution
pub fn distance_to_steps(&self, distance: Length) -> f64 {
// Convert distance to revolutions using CircularConverter
let revolutions = self
.circular_converter
.linear_distance_to_revolutions(distance);
// Convert distance to angle: angle = distance/circumference (in revolutions)
let revolutions = distance.get::<meter>() / self.circumference.get::<meter>();
let angle = Angle::new::<revolution>(revolutions);
// Convert angle to steps
@@ -88,19 +94,21 @@ impl LinearStepConverter {
// Convert steps to angle
let angle = self.angular_step_converter.steps_to_angle(steps);
// Convert angle to distance using CircularConverter
// Convert angle to distance: distance = angle * circumference
let revolutions = angle.get::<revolution>();
self.circular_converter
.revolutions_to_linear_distance(revolutions)
let distance = revolutions * self.circumference.get::<meter>();
Length::new::<meter>(distance)
}
/// Convert linear velocity to steps/second
///
/// Formula: steps/second = (velocity / circumference) * steps_per_revolution
pub fn velocity_to_steps(&self, velocity: Velocity) -> f64 {
// Convert linear velocity to revolutions per second using CircularConverter
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
let angular_velocity = AngularVelocity::new::<revolution_per_second>(rps);
// Convert linear velocity to angular velocity: ω = v/circumference (in rev/s)
let linear_velocity = velocity.get::<meter_per_second>();
let angular_velocity_rps = linear_velocity / self.circumference.get::<meter>();
let angular_velocity = AngularVelocity::new::<revolution_per_second>(angular_velocity_rps);
// Convert angular velocity to steps/second
self.angular_step_converter
@@ -116,19 +124,23 @@ impl LinearStepConverter {
.angular_step_converter
.steps_to_angular_velocity(steps_per_second);
// Convert angular velocity to linear velocity using CircularConverter
let rps = angular_velocity.get::<revolution_per_second>();
self.circular_converter.rps_to_linear_velocity(rps)
// Convert angular velocity to linear velocity: v = ω * circumference
let angular_velocity_rps = angular_velocity.get::<revolution_per_second>();
let linear_velocity = angular_velocity_rps * self.circumference.get::<meter>();
Velocity::new::<meter_per_second>(linear_velocity)
}
/// Convert linear acceleration to steps/second²
///
/// Formula: steps/second² = (acceleration / radius) * (steps_per_revolution / (2π))
pub fn acceleration_to_steps(&self, acceleration: Acceleration) -> f64 {
// Convert linear acceleration to angular acceleration using CircularConverter
let angular_acceleration = self
.circular_converter
.linear_to_angular_acceleration(acceleration);
// Convert linear acceleration to angular acceleration: α = a/r
let linear_acceleration = acceleration.get::<meter_per_second_squared>();
// Calculate angular acceleration in radians per second squared
let angular_acceleration_rad_per_s2 = linear_acceleration / self.radius.get::<meter>();
let angular_acceleration =
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2);
// Convert angular acceleration to steps/second²
self.angular_step_converter
@@ -144,9 +156,12 @@ impl LinearStepConverter {
.angular_step_converter
.steps_to_angular_acceleration(steps_per_second_squared);
// Convert angular acceleration to linear acceleration using CircularConverter
self.circular_converter
.angular_to_linear_acceleration(angular_acceleration)
// Convert angular acceleration to linear acceleration: a = α * r
let angular_acceleration_rad_per_s2 =
angular_acceleration.get::<radian_per_second_squared>();
let linear_acceleration = angular_acceleration_rad_per_s2 * self.radius.get::<meter>();
Acceleration::new::<meter_per_second_squared>(linear_acceleration)
}
}
@@ -156,9 +171,7 @@ impl LinearStepConverter {
///
/// Formula: angle (in revolutions) = distance / circumference
pub fn distance_to_angle(&self, distance: Length) -> Angle {
let revolutions = self
.circular_converter
.linear_distance_to_revolutions(distance);
let revolutions = distance.get::<meter>() / self.circumference.get::<meter>();
Angle::new::<revolution>(revolutions)
}
@@ -167,24 +180,26 @@ impl LinearStepConverter {
/// Formula: distance = angle (in revolutions) * circumference
pub fn angle_to_distance(&self, angle: Angle) -> Length {
let revolutions = angle.get::<revolution>();
self.circular_converter
.revolutions_to_linear_distance(revolutions)
let distance = revolutions * self.circumference.get::<meter>();
Length::new::<meter>(distance)
}
/// Convert linear velocity to angular velocity
///
/// Formula: angular velocity (in rev/s) = velocity / circumference
pub fn velocity_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
AngularVelocity::new::<revolution_per_second>(rps)
let linear_velocity = velocity.get::<meter_per_second>();
let angular_velocity_rps = linear_velocity / self.circumference.get::<meter>();
AngularVelocity::new::<revolution_per_second>(angular_velocity_rps)
}
/// Convert angular velocity to linear velocity
///
/// Formula: velocity = angular velocity (in rev/s) * circumference
pub fn angular_velocity_to_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
let rps = angular_velocity.get::<revolution_per_second>();
self.circular_converter.rps_to_linear_velocity(rps)
let angular_velocity_rps = angular_velocity.get::<revolution_per_second>();
let linear_velocity = angular_velocity_rps * self.circumference.get::<meter>();
Velocity::new::<meter_per_second>(linear_velocity)
}
/// Convert linear acceleration to angular acceleration
@@ -194,8 +209,9 @@ impl LinearStepConverter {
&self,
acceleration: Acceleration,
) -> AngularAcceleration {
self.circular_converter
.linear_to_angular_acceleration(acceleration)
let linear_acceleration = acceleration.get::<meter_per_second_squared>();
let angular_acceleration_rad_per_s2 = linear_acceleration / self.radius.get::<meter>();
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2)
}
/// Convert angular acceleration to linear acceleration
@@ -205,8 +221,10 @@ impl LinearStepConverter {
&self,
angular_acceleration: AngularAcceleration,
) -> Acceleration {
self.circular_converter
.angular_to_linear_acceleration(angular_acceleration)
let angular_acceleration_rad_per_s2 =
angular_acceleration.get::<radian_per_second_squared>();
let linear_acceleration = angular_acceleration_rad_per_s2 * self.radius.get::<meter>();
Acceleration::new::<meter_per_second_squared>(linear_acceleration)
}
}
@@ -251,10 +269,6 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
#[test]
fn test_new() {
+1 -1
View File
@@ -1,5 +1,5 @@
pub mod angle_converter;
pub mod angular_step_converter;
pub mod circular_converter;
pub mod linear_step_converter;
pub mod motor_converter;
pub mod transmission_converter;
@@ -0,0 +1,22 @@
use uom::si::f64::AngularVelocity;
#[derive(Debug, Clone)]
pub struct TransmissionConverter {
pub transmission_ratio: f64,
}
impl TransmissionConverter {
pub fn new() -> Self {
Self {
transmission_ratio: 34.0,
}
}
pub fn calculate_screw_output_rpm(&self, rpm: AngularVelocity) -> AngularVelocity {
rpm / self.transmission_ratio
}
pub fn calculate_screw_input_rpm(&self, screw_rpm: AngularVelocity) -> AngularVelocity {
screw_rpm * self.transmission_ratio
}
}
@@ -4,10 +4,8 @@ use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
use ethercat_hal::devices::el1002::EL1002_IDENTITY_A;
use ethercat_hal::devices::el1008::EL1008_IDENTITY_A;
use ethercat_hal::devices::el2002::EL2002_IDENTITY_A;
use ethercat_hal::devices::el2002::EL2002_IDENTITY_B;
use ethercat_hal::devices::el2004::EL2004_IDENTITY_A;
use ethercat_hal::devices::el2008::EL2008_IDENTITY_A;
use ethercat_hal::devices::el2008::EL2008_IDENTITY_B;
use ethercat_hal::devices::el2521::{
EL2521_IDENTITY_0000_A, EL2521_IDENTITY_0000_B, EL2521_IDENTITY_0024_A,
};
@@ -15,10 +13,8 @@ use ethercat_hal::devices::el2522::EL2522_IDENTITY_A;
use ethercat_hal::devices::el3001::EL3001_IDENTITY_A;
use ethercat_hal::devices::el3021::EL3021_IDENTITY_A;
use ethercat_hal::devices::el3024::EL3024_IDENTITY_A;
use ethercat_hal::devices::el3062_0030::EL3062_0030_IDENTITY_A;
use ethercat_hal::devices::el3204::EL3204_IDENTITY_A;
use ethercat_hal::devices::el3204::EL3204_IDENTITY_B;
use ethercat_hal::devices::el4002::EL4002_IDENTITY_A;
use ethercat_hal::devices::el6021::{
EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D,
};
@@ -232,19 +228,17 @@ pub fn get_identification_addresses<'maindevice>(
EK1100_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2002_IDENTITY_A | EL2002_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2004_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2008_IDENTITY_A | EL2008_IDENTITY_B => MachineIdentificationAddresses::default(),
EL2008_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
MachineIdentificationAddresses::default()
}
EL2522_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3062_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL4002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
@@ -1,360 +0,0 @@
use serde::Serialize;
use serde::ser::{
SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
SerializeTupleStruct, SerializeTupleVariant, Serializer,
};
use std::error::Error;
use std::fmt::Display;
use std::hash::{DefaultHasher, Hasher};
#[derive(Debug)]
pub struct HashSerializerError {}
impl std::fmt::Display for HashSerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{:?}", self))
// f.debug_struct("HashSerialiyerError")
// .finish()
}
}
impl Error for HashSerializerError {}
impl serde::ser::Error for HashSerializerError {
fn custom<T>(_msg: T) -> Self
where
T: Display,
{
HashSerializerError {}
}
}
pub struct HashSerializer<'a, H: Hasher>(&'a mut H);
impl<'a, H: Hasher> SerializeSeq for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeTuple for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeTupleStruct for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeTupleVariant for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeMap for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
key.serialize(HashSerializer(self.0))
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeStruct for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
// Optional: hash the field name to avoid collisions
self.0.write(key.as_bytes());
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> SerializeStructVariant for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
self.0.write(key.as_bytes());
value.serialize(HashSerializer(self.0))
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl<'a, H: Hasher> Serializer for HashSerializer<'a, H> {
type Ok = ();
type Error = HashSerializerError;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Self;
type SerializeTupleVariant = Self;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Self;
fn serialize_bool(self, v: bool) -> Result<(), Self::Error> {
self.0.write_u8(v as u8);
Ok(())
}
fn serialize_i8(self, v: i8) -> Result<(), Self::Error> {
self.0.write_i8(v);
Ok(())
}
fn serialize_i16(self, v: i16) -> Result<(), Self::Error> {
self.0.write_i16(v);
Ok(())
}
fn serialize_i32(self, v: i32) -> Result<(), Self::Error> {
self.0.write_i32(v);
Ok(())
}
fn serialize_i64(self, v: i64) -> Result<(), Self::Error> {
self.0.write_i64(v);
Ok(())
}
fn serialize_u8(self, v: u8) -> Result<(), Self::Error> {
self.0.write_u8(v);
Ok(())
}
fn serialize_u16(self, v: u16) -> Result<(), Self::Error> {
self.0.write_u16(v);
Ok(())
}
fn serialize_u32(self, v: u32) -> Result<(), Self::Error> {
self.0.write_u32(v);
Ok(())
}
fn serialize_u64(self, v: u64) -> Result<(), Self::Error> {
self.0.write_u64(v);
Ok(())
}
fn serialize_f32(self, v: f32) -> Result<(), Self::Error> {
self.0.write_u32(v.to_bits());
Ok(())
}
fn serialize_f64(self, v: f64) -> Result<(), Self::Error> {
self.0.write_u64(v.to_bits());
Ok(())
}
fn serialize_char(self, v: char) -> Result<(), Self::Error> {
let mut buf = [0; 4];
self.0.write(v.encode_utf8(&mut buf).as_bytes());
Ok(())
}
fn serialize_str(self, v: &str) -> Result<(), Self::Error> {
self.0.write(v.as_bytes());
Ok(())
}
fn serialize_bytes(self, v: &[u8]) -> Result<(), Self::Error> {
self.0.write(v);
Ok(())
}
fn serialize_none(self) -> Result<(), Self::Error> {
self.0.write_u8(0); // type tag for None
Ok(())
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
self.0.write_u8(1); // type tag for Some
value.serialize(self)
}
fn serialize_unit(self) -> Result<(), Self::Error> {
self.0.write_u8(0);
Ok(())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> {
self.serialize_unit()
}
fn serialize_unit_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
) -> Result<(), Self::Error> {
self.0.write_u32(variant_index);
Ok(())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
value: &T,
) -> Result<(), Self::Error> {
self.0.write_u32(variant_index);
value.serialize(self)
}
// For sequences, tuples, maps, structs, just return self for now
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Ok(self)
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Ok(self)
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Ok(self)
}
fn serialize_tuple_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
self.0.write_u32(variant_index);
Ok(self)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Ok(self)
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(self)
}
fn serialize_struct_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
self.0.write_u32(variant_index);
Ok(self)
}
}
// A helper that uses Serialize to feed into a Hasher without allocating a buffer:
pub fn hash_with_serde_model<T: Serialize, H: Hasher>(value: T, hasher: H) -> u64 {
let mut h = hasher;
let ser = HashSerializer(&mut h);
value.serialize(ser).expect("serialize to hasher failed");
h.finish()
}
/// if hash of a and b are different this function returns true
/// a and b need to impl Serialize
pub fn check_hash_different<T: Serialize>(a: T, b: T) -> bool {
// Yes DefaultHasher has to be defined multiple times
// If you use one for both the results will never match even if they are the same value
let hasher = DefaultHasher::new();
let hash = hash_with_serde_model(a, hasher);
let hasher = DefaultHasher::new();
let hash_old = hash_with_serde_model(b, hasher);
return hash != hash_old;
}
#[test]
fn test_same_values() {
let a = 10;
let b = 10;
assert_eq!(
check_hash_different(a, b),
false,
"Hashes should be equal for identical values"
);
}
+22 -90
View File
@@ -1,108 +1,40 @@
/// Folds bytes using XOR to produce a u128 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u128.
/// If there are fewer than 16 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u128(bytes: &[u8]) -> u128 {
let mut result = 0u128;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 16) * 8;
result ^= (byte as u128) << shift;
}
result
pub fn xor_u128_to_u64(value: u128) -> u64 {
(value as u64) ^ ((value >> 64) as u64)
}
/// Folds bytes using XOR to produce a u64 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u64.
/// If there are fewer than 8 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u64(bytes: &[u8]) -> u64 {
let mut result = 0u64;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 8) * 8;
result ^= (byte as u64) << shift;
}
result
pub fn xor_u128_to_u32(value: u128) -> u32 {
(value as u32) ^ ((value >> 32) as u32) ^ ((value >> 64) as u32) ^ ((value >> 96) as u32)
}
/// Folds bytes using XOR to produce a u32 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u32.
/// If there are fewer than 4 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u32(bytes: &[u8]) -> u32 {
let mut result = 0u32;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 4) * 8;
result ^= (byte as u32) << shift;
}
result
pub fn xor_u128_to_u16(value: u128) -> u16 {
value
.to_le_bytes()
.iter()
.fold(0, |buf, &byte| buf ^ byte as u16)
}
/// Folds bytes using XOR to produce a u16 value.
///
/// Takes a slice of bytes and XORs them together in chunks to produce a u16.
/// If there are fewer than 2 bytes, the remaining bytes are padded with zeros.
pub fn byte_folding_u16(bytes: &[u8]) -> u16 {
let mut result = 0u16;
for (i, &byte) in bytes.iter().enumerate() {
let shift = (i % 2) * 8;
result ^= (byte as u16) << shift;
}
result
pub fn xor_u128_to_u8(value: u128) -> u8 {
value.to_le_bytes().iter().fold(0, |buf, &byte| buf ^ byte)
}
/// Folds bytes using XOR to produce a u8 value.
///
/// Takes a slice of bytes and XORs them all together to produce a u8.
pub fn byte_folding_u8(bytes: &[u8]) -> u8 {
bytes.iter().fold(0u8, |acc, &byte| acc ^ byte)
}
// hash algorithm djb2
pub fn hashing(input_to_hash: &str) -> u128 {
let mut hash: u128 = 5381;
let chars = input_to_hash.chars();
/// Computes a djb2 hash of the input bytes.
///
/// The djb2 algorithm is a simple non-cryptographic hash function created by
/// Daniel J. Bernstein. It uses the formula: `hash = hash * 33 + byte` for each byte.
///
/// This implementation uses the traditional 32-bit hash value and uses wrapping
/// arithmetic to handle potential overflows gracefully.
///
/// # Arguments
///
/// * `bytes` - A slice of bytes to hash
///
/// # Returns
///
/// A 32-bit hash value as a `u32`
///
/// # Examples
///
/// ```
/// use control_core::helpers::hashing::hash_djb2;
///
/// let hash = hash_djb2(b"hello world");
/// assert_ne!(hash, 0);
/// ```
///
/// # Algorithm Details
///
/// - Initial hash value: 5381 (djb2 magic number)
/// - For each byte: `hash = hash * 33 + byte`
/// - Uses wrapping arithmetic to prevent overflow panics
pub fn hash_djb2(bytes: &[u8]) -> u32 {
let mut hash: u32 = 5381;
for byte in bytes {
hash = hash.wrapping_mul(33).wrapping_add(*byte as u32);
for ch in chars {
hash = ((hash << 5) + hash) + (ch as u128);
}
hash
}
#[test]
fn test_hash() {
let test_case = "/dev/ttyUSB1";
let hash_u32 = hash_djb2(test_case.as_bytes());
let hash_u16 = byte_folding_u16(&hash_u32.to_le_bytes());
let hash = hashing(test_case);
let hash_2bytes = xor_u128_to_u16(hash);
tracing::info!("{}", test_case);
assert_eq!(test_case, "/dev/ttyUSB1");
assert_eq!(hash_u32, 266121182);
assert_eq!(hash_u16, 40962);
assert!(hash == 8977446972540388683742);
assert!(hash_2bytes == 165);
println!("{:?}", hash);
}
+5 -699
View File
@@ -1,26 +1,4 @@
/// Normalizes a value from a given range [min, max] to the range [0, 1].
///
/// This function maps a value from an arbitrary range to the standard
/// normalized range [0, 1]. Values o/// Plot for Steepness = -1:ge are clamped
/// to the boundaries (0.0 or 1.0).
///
/// # Arguments
/// * `value` - The input value to normalize
/// * `min` - The minimum value of the input range
/// * `max` - The maximum value of the input range
///
/// # Returns
/// * Normalized value in range [0.0, 1.0]
///
/// # Panics
/// * Panics if `min >= max`
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::normalize;
/// let normalized = normalize(5.0, 0.0, 10.0); // Returns 0.5
/// let clamped = normalize(-2.0, 0.0, 10.0); // Returns 0.0 (clamped)
/// ```
pub fn normalize<T>(value: T, min: T, max: T) -> f64
where
T: Into<f64>,
@@ -41,24 +19,6 @@ where
}
/// Scales a normalized value from the range [0, 1] to a new range [new_min, new_max].
///
/// This function takes a normalized value (typically from 0 to 1) and maps it
/// to a new target range. It's the inverse operation of normalization.
///
/// # Arguments
/// * `normalized_value` - Input value in range [0.0, 1.0]
/// * `new_min` - The minimum value of the target range
/// * `new_max` - The maximum value of the target range
///
/// # Returns
/// * Scaled value in the range [new_min, new_max]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::scale;
/// let scaled = scale(0.5, 0.0, 100.0); // Returns 50.0
/// let negative_range = scale(0.25, -10.0, 10.0); // Returns -5.0
/// ```
pub fn scale<T>(normalized_value: f64, new_min: T, new_max: T) -> T
where
T: Into<f64> + From<f64>,
@@ -71,395 +31,24 @@ where
T::from(result)
}
/// Performs hinge interpolation creating a piecewise linear function.
///
/// This function creates a piecewise linear curve that:
/// - For values < x: linearly interpolates from (0, 0) to (x, y)
/// - For values > x: linearly interpolates from (x, y) to (1, 1)
/// - For value == x: returns y
///
/// The function assumes the input value is normalized to the range [0, 1],
/// and creates a "hinge" or "elbow" shape at the point (x, y).
///
/// # Arguments
/// * `value` - The input value (typically in range [0, 1])
/// * `x` - The x-coordinate of the hinge point (typically in range [0, 1])
/// * `y` - The y-coordinate of the hinge point (typically in range [0, 1])
///
/// # Returns
/// * Interpolated value based on the hinge function
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_hinge;
/// // Create a hinge at (0.3, 0.7)
/// let result1 = interpolate_hinge(0.15, 0.3, 0.7); // Returns 0.35 (halfway to hinge)
/// let result2 = interpolate_hinge(0.3, 0.3, 0.7); // Returns 0.7 (at hinge)
/// let result3 = interpolate_hinge(0.65, 0.3, 0.7); // Returns 0.85 (halfway from hinge to 1)
/// ```
/// ```ignore
/// Hinge at (0.8, 0.2)
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠤⠒⠒⠊⠁⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠒⠒⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⣀⣀⠤⠤⠔⠒⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠉⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_hinge<T>(value: T, x: T, y: T) -> f64
where
T: Into<f64>,
{
let value_f64: f64 = value.into();
let x_f64: f64 = x.into();
let y_f64: f64 = y.into();
// If we are under X we scale linearly from 0 to y
// If we are over X we scale linearly from y to 1
if value_f64 < x_f64 {
return normalize(value_f64, 0.0, x_f64) * y_f64;
} else if value_f64 > x_f64 {
return normalize(value_f64, x_f64, 1.0) * (1.0 - y_f64) + y_f64;
} else {
return y_f64;
}
}
/// Transforms a normalized value from the range [0, 1] to a [0, 1] range using a normalized exponential function.
///
/// This function creates smooth curves that are either convex (fast start, slow end)
/// or concave (slow start, fast end) depending on the steepness parameter sign.
/// The curve always passes through (0,0) and (1,1).
///
/// # Arguments
/// * `normalized_value` - Input value in range [0.0, 1.0]
/// * `steepness` - Controls the curve shape:
/// - `steepness = 0.0`: Linear interpolation (y = x)
/// - `steepness > 0.0`: Convex curve (slow start → fast end)
/// - `steepness < 0.0`: Concave curve (fast start → slow end)
/// - Typical range: -5.0 to 5.0 for most applications
///
/// # Returns
/// * Transformed value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_exponential;
/// let convex = interpolate_exponential(0.5, 2.0); // Slow start, fast end
/// let concave = interpolate_exponential(0.5, -2.0); // Fast start, slow end
/// let linear = interpolate_exponential(0.5, 0.0); // Linear (returns 0.5)
/// ```
///
/// # Plots
/// Steepness = -3
/// Steepness = -3
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠔⠒⠒⠉⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
/// 0.0 1.0
/// ```
///
/// Steepness = -1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡲⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 0
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢂⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⢀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢂⣀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 3
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⣀⣀⠤⠤⠒⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠉⠉⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
if steepness.abs() < 1e-10 {
/// Transforma normalized value from the range [0, 1] to a [0, 1] range using an normalized exponential function.
pub fn interpolate_exponential(normalized_value: f64, a: f64) -> f64 {
if a.abs() < 1e-10 {
// When a is very close to zero, the function becomes linear
return normalized_value;
}
let numerator = steepness.exp().powf(normalized_value) - 1.0;
let denominator = steepness.exp() - 1.0;
let numerator = a.exp().powf(normalized_value) - 1.0;
let denominator = a.exp() - 1.0;
clip(numerator / denominator)
}
/// A piecewise exponential interpolation function that creates smooth S-curves
/// through the points (0,0), (0.5,0.5), and (1,1).
///
/// The function combines two normalized exponential segments:
/// - First half (0 to 0.5): inverted exponential (concave, fast start → slow end)
/// - Second half (0.5 to 1): standard exponential (convex, slow start → fast end)
///
/// This creates an S-like curve that's concave then convex, similar to
/// a smoothed step function or sigmoid-like transition.
///
/// # Arguments
/// * `x` - Input value, should be in range [0.0, 1.0]
/// * `steepness` - Controls how steep the S-curve is
/// - `steepness = 0.0`: Linear interpolation (y = x)
/// - `steepness > 0.0`: More pronounced S-curve
/// - Higher values create sharper transitions at the boundaries
/// - Typical range: 0.5 to 5.0 for most applications
///
/// # Returns
/// * Interpolated y value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::interpolate_inflected_exponential;
/// let y = interpolate_inflected_exponential(0.25, 2.0); // Gentle S-curve
/// let y = interpolate_inflected_exponential(0.75, 5.0); // Sharp S-curve
/// ```
///
/// # Plots
/// Steepness = 0.5
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢀⠤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡢⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 1
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡲⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 2
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠒⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
///
/// Steepness = 5
/// ```ignore
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠉⠀⠀⠀⠀⠀
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠤⠤⠤⠤⠤⠤⠒⠒⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⠀⠀⠀⠀⢀⠔⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡁⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠄⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠂⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⣁⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⢼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
/// 0.0 1.0
/// ```
pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
debug_assert!((0.0..=1.0).contains(&x), "x must be in range [0.0, 1.0]");
debug_assert!(steepness >= 0.0, "steepness must be non-negative");
if steepness == 0.0 {
// Linear case
return x;
}
if x <= 0.5 {
// First segment: inverted exponential (concave)
// Maps [0, 0.5] → [0, 0.5] with concave shape
let x_norm = 2.0 * x; // Scale to [0, 1]
// Use inverted exponential interpolation
let result = 1.0 - interpolate_exponential(1.0 - x_norm, steepness);
0.5 * result
} else {
// Second segment: standard exponential (convex)
// Maps [0.5, 1] → [0.5, 1] with convex shape
let x_norm = 2.0 * (x - 0.5); // Scale to [0, 1]
// Use standard exponential interpolation
let result = interpolate_exponential(x_norm, steepness);
0.5 + 0.5 * result
}
}
/// Inverts a value from the range [0, 1] to [1, 0].
///
/// This function flips a normalized value, where 0 becomes 1 and 1 becomes 0.
/// Values outside [0, 1] are first clamped to this range before inversion.
///
/// # Arguments
/// * `value` - Input value (will be clamped to [0.0, 1.0])
///
/// # Returns
/// * Inverted value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::invert;
/// let inverted = invert(0.3); // Returns 0.7
/// let boundary = invert(0.0); // Returns 1.0
/// let clamped = invert(1.5); // Returns 0.0 (input clamped to 1.0 first)
/// ```
pub fn invert(value: f64) -> f64 {
let value = clip(value);
1.0 - value
}
/// Clips a value to the range [0, 1].
///
/// This function ensures that any input value is constrained to the
/// normalized range [0, 1] by clamping values below 0 to 0 and
/// values above 1 to 1.
///
/// # Arguments
/// * `value` - Input value to clip
///
/// # Returns
/// * Clipped value in range [0.0, 1.0]
///
/// # Example
/// ```rust
/// use control_core::helpers::interpolation::clip;
/// let normal = clip(0.5); // Returns 0.5
/// let low = clip(-0.3); // Returns 0.0
/// let high = clip(1.7); // Returns 1.0
/// ```
pub fn clip(value: f64) -> f64 {
if value < 0.0 {
return 0.0;
@@ -589,289 +178,6 @@ mod tests {
assert_relative_eq!(clip(1.5), 1.0, epsilon = EPSILON);
}
#[test]
fn test_interpolate_inflected_exponential() {
// Test boundary points (0,0), (0.5,0.5), and (1,1) for various steepness values
for steepness in [0.0, 0.5, 1.0, 2.0, 5.0].iter() {
assert_relative_eq!(
interpolate_inflected_exponential(0.0, *steepness),
0.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_inflected_exponential(0.5, *steepness),
0.5,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_inflected_exponential(1.0, *steepness),
1.0,
epsilon = EPSILON
);
}
// Test linear case (steepness = 0)
for x in [0.0, 0.25, 0.5, 0.75, 1.0].iter() {
assert_relative_eq!(
interpolate_inflected_exponential(*x, 0.0),
*x,
epsilon = EPSILON
);
}
// Test monotonicity - function should be strictly increasing
for steepness in [0.5, 1.0, 2.0, 5.0].iter() {
let mut prev = interpolate_inflected_exponential(0.0, *steepness);
for i in 1..=20 {
let x = i as f64 / 20.0;
let current = interpolate_inflected_exponential(x, *steepness);
assert!(
current >= prev,
"Failed monotonicity test at x={}, steepness={}, prev={}, current={}",
x,
steepness,
prev,
current
);
prev = current;
}
}
// Test S-curve property: first half should be concave, second half convex
let steepness = 2.0;
// First half: check concave property (derivative decreasing)
let x1 = 0.1;
let x2 = 0.2;
let x3 = 0.3;
let y1 = interpolate_inflected_exponential(x1, steepness);
let y2 = interpolate_inflected_exponential(x2, steepness);
let y3 = interpolate_inflected_exponential(x3, steepness);
let slope1 = (y2 - y1) / (x2 - x1);
let slope2 = (y3 - y2) / (x3 - x2);
assert!(
slope1 > slope2,
"First half should be concave (decreasing slope)"
);
// Second half: check convex property (derivative increasing)
let x1 = 0.7;
let x2 = 0.8;
let x3 = 0.9;
let y1 = interpolate_inflected_exponential(x1, steepness);
let y2 = interpolate_inflected_exponential(x2, steepness);
let y3 = interpolate_inflected_exponential(x3, steepness);
let slope1 = (y2 - y1) / (x2 - x1);
let slope2 = (y3 - y2) / (x3 - x2);
assert!(
slope1 < slope2,
"Second half should be convex (increasing slope)"
);
// Test that output is always in [0, 1] range
for steepness in [0.0, 1.0, 5.0, 10.0].iter() {
for i in 0..=100 {
let x = i as f64 / 100.0;
let y = interpolate_inflected_exponential(x, *steepness);
assert!(
y >= 0.0 && y <= 1.0,
"Output {} out of range [0,1] for x={}, steepness={}",
y,
x,
steepness
);
}
}
}
#[test]
fn test_interpolate_hinge() {
// Test basic functionality with hinge at (0.3, 0.7)
let x_hinge = 0.3;
let y_hinge = 0.7;
// Test exact hinge point
assert_relative_eq!(
interpolate_hinge(x_hinge, x_hinge, y_hinge),
y_hinge,
epsilon = EPSILON
);
// Test left segment (value < x): should interpolate from (0,0) to (x,y)
assert_relative_eq!(
interpolate_hinge(0.0, x_hinge, y_hinge),
0.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.15, x_hinge, y_hinge),
0.35, // 0.15/0.3 * 0.7 = 0.5 * 0.7 = 0.35
epsilon = EPSILON
);
// Test right segment (value > x): should interpolate from (x,y) to (1,1)
assert_relative_eq!(
interpolate_hinge(1.0, x_hinge, y_hinge),
1.0,
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.65, x_hinge, y_hinge),
0.85, // y + ((0.65-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + 0.5 * 0.3 = 0.85
epsilon = EPSILON
);
// Test with different hinge points
// Hinge at (0.5, 0.5) - should be linear
assert_relative_eq!(interpolate_hinge(0.25, 0.5, 0.5), 0.25, epsilon = EPSILON);
assert_relative_eq!(interpolate_hinge(0.75, 0.5, 0.5), 0.75, epsilon = EPSILON);
// Hinge at (0.2, 0.9) - steep left, shallow right
assert_relative_eq!(
interpolate_hinge(0.1, 0.2, 0.9),
0.45, // 0.1/0.2 * 0.9 = 0.5 * 0.9 = 0.45
epsilon = EPSILON
);
assert_relative_eq!(
interpolate_hinge(0.6, 0.2, 0.9),
0.95, // 0.9 + ((0.6-0.2)/(1.0-0.2)) * (1.0-0.9) = 0.9 + 0.5 * 0.1 = 0.95
epsilon = EPSILON
);
// Test edge cases
// Hinge at (0, 0)
assert_relative_eq!(interpolate_hinge(0.0, 0.0, 0.0), 0.0, epsilon = EPSILON);
assert_relative_eq!(
interpolate_hinge(0.5, 0.0, 0.0),
0.5, // Should interpolate from (0,0) to (1,1)
epsilon = EPSILON
);
// Hinge at (1, 1)
assert_relative_eq!(
interpolate_hinge(0.5, 1.0, 1.0),
0.5, // Should interpolate from (0,0) to (1,1)
epsilon = EPSILON
);
assert_relative_eq!(interpolate_hinge(1.0, 1.0, 1.0), 1.0, epsilon = EPSILON);
// Test with different numeric types
assert_relative_eq!(
interpolate_hinge(0.5f32, 0.3f32, 0.7f32),
0.7857142857142857, // 0.7 + ((0.5-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + (2/7) * 0.3
epsilon = 1e-7 // Relaxed epsilon for f32 precision
);
assert_relative_eq!(
interpolate_hinge(1u8, 0u8, 1u8), // 1.0, 0.0, 1.0 - should return 1.0
1.0,
epsilon = EPSILON
);
// Test monotonicity - function should be non-decreasing
let test_x = 0.4;
let test_y = 0.6;
let mut prev = interpolate_hinge(0.0, test_x, test_y);
for i in 1..=20 {
let value = i as f64 / 20.0;
let current = interpolate_hinge(value, test_x, test_y);
assert!(
current >= prev,
"Failed monotonicity test at value={}, x={}, y={}, prev={}, current={}",
value,
test_x,
test_y,
prev,
current
);
prev = current;
}
}
#[test]
fn test_interpolate_exponential_graph() {
use textplots::{Chart, Plot, Shape};
println!("\n=== Exponential Interpolation Examples ===");
// Test different steepness values (both positive and negative)
let steepness_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
for steepness in steepness_values.iter() {
println!("\nSteepness = {}", steepness);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_exponential(x, *steepness);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
fn test_interpolate_hinge_graph() {
use textplots::{Chart, Plot, Shape};
println!("\n=== hinge Interpolation Examples ===");
// Test different hinge points
let hinges = [(0.3, 0.7), (0.5, 0.5), (0.2, 0.9), (0.8, 0.2)];
for &(x_hinge, y_hinge) in hinges.iter() {
println!("\nHinge at ({}, {})", x_hinge, y_hinge);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_hinge(x, x_hinge, y_hinge);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
fn test_interpolate_inflected_exponential_graph() {
use textplots::{Chart, Plot, Shape};
// Test different steepness values
let steepness_values = [0.5, 1.0, 2.0, 5.0];
for steepness in steepness_values.iter() {
println!("\nSteepness = {}", steepness);
// Generate data points
let points: Vec<(f32, f32)> = (0..=50)
.map(|i| {
let x = i as f64 / 50.0;
let y = interpolate_inflected_exponential(x, *steepness);
(x as f32, y as f32)
})
.collect();
// Create and display chart
Chart::new(60, 60, 0.0, 1.0)
.lineplot(&Shape::Lines(&points))
.display();
}
}
#[test]
#[should_panic]
fn test_normalize_invalid_range() {
-1
View File
@@ -1,5 +1,4 @@
pub mod compare_lists;
pub mod hasher_serializer;
pub mod hashing;
pub mod interpolation;
pub mod loop_trottle;
@@ -56,7 +56,7 @@ impl DivideByCount for u64 {
/// let mut window = MovingTimeWindow::new(Duration::from_secs(10), 100);
/// window.update(1.5, Instant::now());
/// window.update(2.5, Instant::now());
///
///
/// let average = window.average();
/// println!("Average: {}", average);
/// ```
+1 -2
View File
@@ -1,14 +1,13 @@
pub mod actors;
pub mod controllers;
pub mod converters;
pub mod ethercat;
pub mod helpers;
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;
#[macro_use]
+4 -53
View File
@@ -12,10 +12,7 @@ use crate::{
serial::SerialDevice,
socketio::event::GenericEvent,
};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use std::{collections::HashMap, sync::Arc};
use super::{
Machine,
@@ -26,9 +23,9 @@ use super::{
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
pub serial_machines:
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl MachineManager {
@@ -45,7 +42,6 @@ impl MachineManager {
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<MachineManager>>,
) {
// empty ethercat machines
self.ethercat_machines.clear();
@@ -73,7 +69,6 @@ impl MachineManager {
device_group,
hardware: &machine_new_hardware,
socket_queue_tx: socket_queue_tx.clone(),
machine_manager: machine_manager.clone(),
});
// insert the machine into the ethercat machines map
@@ -87,7 +82,7 @@ impl MachineManager {
pub fn get(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
) -> Option<&Result<Box<Mutex<dyn Machine>>, anyhow::Error>> {
self.ethercat_machines
.get(machine_identification)
.or_else(|| self.serial_machines.get(machine_identification))
@@ -99,7 +94,6 @@ impl MachineManager {
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<MachineManager>>,
) {
let hardware = MachineNewHardwareSerial { device };
@@ -113,7 +107,6 @@ impl MachineManager {
device_group: &vec![device_identification_identified.clone()],
hardware: &MachineNewHardware::Serial(&hardware),
socket_queue_tx,
machine_manager: machine_manager.clone(),
});
tracing::info!("Adding serial machine {:?}", new_machine);
@@ -144,48 +137,6 @@ impl MachineManager {
.machine_identification_unique,
);
}
pub fn get_machine_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.ethercat_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
return Some(machine_weak);
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let machine = self.serial_machines.get(machine_identification);
let machine = match machine {
Some(machine) => machine,
None => {
return None;
}
};
let machine = match machine {
Ok(machine) => machine,
Err(_) => {
return None;
}
};
let machine_weak = Arc::downgrade(machine);
return Some(machine_weak);
}
}
/// Groups devices by machine identification
+5 -5
View File
@@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use smol::lock::Mutex;
@@ -9,14 +9,14 @@ pub enum MachineManagerIter<'a> {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
>,
},
Done,
@@ -25,13 +25,13 @@ pub enum MachineManagerIter<'a> {
pub struct MachineManagerIterator<'a> {
iter: MachineManagerIter<'a>,
serial_machines:
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
&'a HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
}
impl<'a> Iterator for MachineManagerIterator<'a> {
type Item = (
&'a MachineIdentificationUnique,
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
&'a Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
);
fn next(&mut self) -> Option<Self::Item> {
+2 -69
View File
@@ -1,14 +1,8 @@
use anyhow::anyhow;
use crate::actors::Actor;
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;
@@ -16,65 +10,4 @@ pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
fn as_any(&self) -> &dyn Any;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
// Acquire a read lock on the RwLock
let read_lock = machine.lock().await;
// Check if the inner type can be downcasted to T
if read_lock.as_any().is::<T>() {
// Clone the Arc and return it as the desired type
let cloned_machine = Arc::clone(&machine);
// Transmute the Arc to the desired type
unsafe {
Ok(Arc::from_raw(
Arc::into_raw(cloned_machine) as *const Mutex<T>
))
}
} else {
Err(anyhow!(
"[{}::downcast_machine] Downcast failed",
module_path!()
))
}
}
/// Trait to get strong count to see if Weak has reference
pub trait GetStrongCount {
fn get_strong_count(&self) -> usize;
}
impl<T> GetStrongCount for Weak<Mutex<T>> {
fn get_strong_count(&self) -> usize {
self.strong_count()
}
}
#[derive(Debug)]
pub struct ConnectedMachine<T: GetStrongCount> {
pub machine_identification_unique: MachineIdentificationUnique,
pub machine: T,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct ConnectedMachineData {
pub machine_identification_unique: MachineIdentificationUnique,
pub is_available: bool,
}
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
where
T: GetStrongCount,
{
fn from(value: &ConnectedMachine<T>) -> Self {
Self {
machine_identification_unique: value.machine_identification_unique.clone(),
is_available: value.machine.get_strong_count() != 0,
}
}
}
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
+2 -11
View File
@@ -1,4 +1,4 @@
use crate::{machines::manager::MachineManager, serial::SerialDevice};
use crate::serial::SerialDevice;
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
@@ -8,11 +8,7 @@ use ethercat_hal::{
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use socketioxide::extract::SocketRef;
use std::{
pin::Pin,
sync::{Arc, Weak},
time::Instant,
};
use std::sync::Arc;
use crate::socketio::event::GenericEvent;
@@ -24,10 +20,6 @@ pub trait MachineNewTrait {
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
@@ -51,7 +43,6 @@ pub struct MachineNewParams<
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub machine_manager: Weak<RwLock<MachineManager>>,
}
pub enum MachineNewHardware<
+4 -4
View File
@@ -1,10 +1,10 @@
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
use anyhow::Error;
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
use std::{any::TypeId, collections::HashMap};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
Box<dyn Fn(&MachineNewParams) -> Result<Box<Mutex<dyn Machine>>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
@@ -27,7 +27,7 @@ impl MachineRegistry {
machine_identficiation,
// create a machine construction closure
Box::new(|machine_new_params| {
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
Ok(Box::new(Mutex::new(T::new(machine_new_params)?)))
}),
),
);
@@ -36,7 +36,7 @@ impl MachineRegistry {
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
) -> Result<Box<Mutex<dyn Machine>>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
+4 -57
View File
@@ -1,10 +1,7 @@
pub mod modbus_serial_interface;
use anyhow::Error;
use crc::{CRC_16_MODBUS, Crc};
use serialport::SerialPort;
use serial;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParityType {
Even,
@@ -25,58 +22,6 @@ pub enum ModbusFunctionCode {
DiagnoseFunction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ModbusExceptionCode {
None = 0,
IllegalFunction = 1,
IllegalDataAddress = 2,
IllegalDataValue = 3,
SlaveDeviceFailure = 4,
Acknowledge = 5,
SlaveDeviceBusy = 6,
MemoryParityError = 8,
GatewayPathUnavailable = 10,
GatewayTargetDeviceFailedToRespond = 11,
Unknown(u8), // For any unknown exception codes
}
impl From<ModbusExceptionCode> for u8 {
fn from(code: ModbusExceptionCode) -> Self {
match code {
ModbusExceptionCode::None => 0,
ModbusExceptionCode::IllegalFunction => 1,
ModbusExceptionCode::IllegalDataAddress => 2,
ModbusExceptionCode::IllegalDataValue => 3,
ModbusExceptionCode::SlaveDeviceFailure => 4,
ModbusExceptionCode::Acknowledge => 5,
ModbusExceptionCode::SlaveDeviceBusy => 6,
ModbusExceptionCode::MemoryParityError => 8,
ModbusExceptionCode::GatewayPathUnavailable => 10,
ModbusExceptionCode::GatewayTargetDeviceFailedToRespond => 11,
ModbusExceptionCode::Unknown(code) => code,
}
}
}
impl From<u8> for ModbusExceptionCode {
fn from(value: u8) -> Self {
match value {
0 => ModbusExceptionCode::None,
1 => ModbusExceptionCode::IllegalFunction,
2 => ModbusExceptionCode::IllegalDataAddress,
3 => ModbusExceptionCode::IllegalDataValue,
4 => ModbusExceptionCode::SlaveDeviceFailure,
5 => ModbusExceptionCode::Acknowledge,
6 => ModbusExceptionCode::SlaveDeviceBusy,
8 => ModbusExceptionCode::MemoryParityError,
10 => ModbusExceptionCode::GatewayPathUnavailable,
11 => ModbusExceptionCode::GatewayTargetDeviceFailedToRespond,
other => ModbusExceptionCode::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModbusRequest {
pub slave_id: u8,
@@ -143,7 +88,9 @@ impl From<ModbusRequest> for Vec<u8> {
/// # Returns
/// A `Result` containing an `Option` with the raw Modbus response as a `Vec<u8>` if successful,
/// or `None` if the response is invalid or an error occurs.
pub fn receive_data_modbus(port: &mut dyn SerialPort) -> Result<Option<Vec<u8>>, anyhow::Error> {
pub fn receive_data_modbus(
port: &mut dyn serial::SerialPort,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
let mut buf: [u8; 256] = [0; 256];
let data_length = port.read(&mut buf)?;
if data_length == 0 {
@@ -1,268 +0,0 @@
use crate::modbus::{ModbusRequest, ModbusResponse};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
struct RequestMetaData {
priority: u32,
ignored_times: u32,
/// This is used when a machine for example takes 4ms to process the request
/// and is added ontop of the standard waiting time for a serial transfer
extra_delay: Option<u32>,
no_response_expected: bool,
}
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Request we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub struct ModbusSerialInterface {
serial_interface: SerialInterface,
baudrate: Option<u32>,
encoding: Option<SerialEncoding>,
response: Option<ModbusResponse>,
state: State,
last_message_size: usize,
last_message_delay: u32,
pub last_message_id: u32,
no_response_expected: bool,
request_map: HashMap<u32, ModbusRequest>,
request_metadata_map: HashMap<u32, RequestMetaData>,
last_ts: Instant,
}
impl ModbusSerialInterface {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
baudrate: None,
encoding: None,
request_map: HashMap::new(),
request_metadata_map: HashMap::new(),
response: None,
last_message_size: 0,
last_message_delay: 0,
state: State::Uninitialized,
last_ts: Instant::now(),
last_message_id: 0,
no_response_expected: false,
}
}
pub fn is_initialized(&self) -> bool {
!matches!(self.state, State::Uninitialized)
}
pub fn add_request(
&mut self,
request_id: u32,
priority: u32,
request: ModbusRequest,
no_response_expected: bool,
delay: Option<u32>,
) {
self.request_map.insert(request_id, request);
self.request_metadata_map
.entry(request_id)
.or_insert_with(|| RequestMetaData {
priority,
ignored_times: 0,
extra_delay: delay,
no_response_expected,
});
}
/// This is used internally to read the receive buffer of the el6021
async fn read_modbus_response(&mut self) -> Result<ModbusResponse, anyhow::Error> {
let raw_response = (self.serial_interface.read_message)()
.await
.unwrap_or_default();
let response = ModbusResponse::try_from(raw_response)?;
self.last_message_size = response.data.len() + 4;
self.state = State::WaitingForReceiveAccept;
Ok(response)
}
/// This is used internally to fill the write buffer of the el6021 with the modbus request
/// Decides what requests to send first by finding the one with the highest priority
/// For example Highest Priority requests: ResetInverter StopMotor
async fn send_modbus_request(&mut self) {
if self.request_map.is_empty() {
return;
}
let (highest_id, delay, no_response_expected) = self
.request_metadata_map
.iter_mut()
.map(|(key, value)| {
let effective_priority = value.priority + value.ignored_times;
(
*key,
effective_priority,
value.extra_delay.unwrap_or(0),
value.no_response_expected,
)
})
.max_by_key(|(_, priority, _, _)| *priority)
.map(|(key, _, delay, no_response)| (key, delay, no_response))
.unwrap(); // Safe because we checked is_empty() above
let request = &self.request_map[&highest_id];
let modbus_request: Vec<u8> = request.clone().into();
if let Err(_) = (self.serial_interface.write_message)(modbus_request.clone()).await {
tracing::error!("ERROR: serial_interface.write_message has failed");
} else {
self.last_message_delay = delay;
self.last_message_id = highest_id;
self.no_response_expected = no_response_expected;
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
}
pub async fn initialize_communication_settings(&mut self) -> bool {
let baudrate = (self.serial_interface.get_baudrate)().await;
let encoding = (self.serial_interface.get_serial_encoding)().await;
match (baudrate, encoding) {
(Some(b), Some(e)) => {
self.baudrate = Some(b);
self.encoding = Some(e);
true
}
_ => false,
}
}
pub async fn initialize(&mut self) -> bool {
if matches!(self.state, State::Uninitialized) {
let success = (self.serial_interface.initialize)().await;
if success {
self.state = State::ReadyToSend;
self.initialize_communication_settings().await;
}
success
} else {
false
}
}
fn increment_ignored_times(&mut self) {
for metadata in self.request_metadata_map.values_mut() {
metadata.ignored_times += 1;
}
}
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
/// This silent time is needed to identify the start and end of messages
/// This function also takes into account the time that the slave we are talking to needs to process our request
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
/// baudrate: bits per second
/// message_size: size of original message in bytes
pub fn calculate_modbus_rtu_timeout(
&self,
machine_operation_delay: Duration,
message_size: usize,
) -> Option<Duration> {
let baudrate = self.baudrate?;
let encoding = self.encoding?;
let nanoseconds_per_bit = 1_000_000_000 / baudrate as u64;
let nanoseconds_per_byte = encoding.total_bits() as u64 * nanoseconds_per_bit;
let transmission_timeout = nanoseconds_per_byte * message_size as u64;
let silent_time = (nanoseconds_per_byte * 35) / 10; // silent_time is 3.5x of character length
let total_timeout =
transmission_timeout + machine_operation_delay.as_nanos() as u64 + silent_time;
Some(Duration::from_nanos(total_timeout))
}
pub fn get_response(&self) -> Option<&ModbusResponse> {
self.response.as_ref()
}
pub async fn act(&mut self, now_ts: Instant) {
let elapsed = now_ts.duration_since(self.last_ts);
let timeout = self.calculate_modbus_rtu_timeout(
Duration::from_nanos(self.last_message_delay as u64),
self.last_message_size,
);
let Some(timeout) = timeout else { return };
// Check if we need to wait for timeout (except for specific states)
if !matches!(
self.state,
State::Uninitialized | State::WaitingForReceiveAccept | State::WaitingForRequestAccept
) && elapsed < timeout
{
return;
}
self.last_ts = now_ts;
self.response = None;
match self.state {
State::WaitingForResponse => match self.read_modbus_response().await {
Ok(response) => {
self.response = Some(response);
}
Err(_) => {
self.response = None;
if self.no_response_expected {
self.state = State::ReadyToSend;
}
}
},
State::ReadyToSend => {
self.send_modbus_request().await;
// Remove the sent request
self.request_map.remove(&self.last_message_id);
self.request_metadata_map.remove(&self.last_message_id);
self.increment_ignored_times();
}
State::WaitingForReceiveAccept => {
// Waste at least one Ethercat Cycle here to ensure that request/response stay in sync
self.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
// An empty vec is used to check if we are finished with writing the message
// This is to keep the Serialinterface more simple
match (self.serial_interface.write_message)(vec![]).await {
Ok(finished) if finished => {
self.state = State::WaitingForResponse;
}
Err(_) => {
self.state = State::ReadyToSend;
}
_ => {} // Still waiting
}
}
State::Uninitialized => {}
}
}
}
-178
View File
@@ -1,178 +0,0 @@
use tracing::debug;
use tracing::error;
#[cfg(unix)]
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
///
/// This function configures the current thread with real-time scheduling properties by:
///
/// 1. Setting the scheduling policy to SCHED_FIFO (First-In-First-Out), which is a real-time
/// scheduling policy that allows the thread to run until it voluntarily yields, blocks on I/O,
/// or is preempted by a higher-priority real-time thread.
///
/// 2. Setting the real-time priority to 80 (on a scale of 1-99, where higher values indicate
/// higher priority). This priority level is chosen to be higher than most IRQ handlers
/// (typically priority 50) but lower than critical kernel tasks (priority 99).
///
/// When running on a PREEMPT_RT patched Linux kernel, this combination provides deterministic
/// scheduling with minimal latency, which is essential for real-time applications.
///
/// # Requirements
///
/// - The program must run with appropriate permissions to set real-time priorities.
/// This typically means either:
/// - Running as root
/// - Configuring `/etc/security/limits.conf` to allow the user to set real-time priorities
///
/// - The system should be running a Linux kernel with PREEMPT_RT patch applied for
/// optimal real-time performance.
///
/// # Example
///
/// ```ignore
/// fn main() {
/// // Lock memory to prevent page faults
/// lock_memory();
///
/// // Create a thread and make it real-time
/// std::thread::spawn(|| {
/// set_realtime_priority();
///
/// // Real-time code here
/// });
/// }
/// ```
///
/// # Returns
///
/// - `Ok(())` if the real-time priority was successfully set
/// - `Err` with the error message if setting the priority failed
#[cfg(unix)]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
use anyhow::anyhow;
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
use std::mem;
unsafe {
// Get the pthread_t of the current thread using pthread_self()
let pthread_self = libc::pthread_self();
// Set up the scheduling parameters
let mut param: sched_param = mem::zeroed();
param.sched_priority = 95;
// Set the thread to use SCHED_FIFO scheduling policy with our priority
let result = pthread_setschedparam(pthread_self, SCHED_FIFO, &param);
if result != 0 {
error!(
"Failed to set real-time scheduling policy for thread \"{:?}\": {}",
std::thread::current().name().unwrap_or("unknown"),
std::io::Error::last_os_error()
);
return Err(anyhow!(
"Failed to set real-time priority: {}",
std::io::Error::last_os_error()
));
} else {
debug!(
"Successfully set real-time priority for the thread \"{:?}\"",
std::thread::current().name().unwrap_or("unknown")
);
}
}
Ok(())
}
#[cfg(not(unix))]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
log::error!("This platform does not support realtime threads");
Ok(())
}
/// Locks all current and future memory pages of the process into RAM to prevent page faults.
///
/// # When to call this function
///
/// This function should be called from the main thread, as early as possible in your program's
/// execution, ideally before creating any threads and before allocating large amounts of memory.
///
/// Important: You only need to call this function ONCE per process, not in each thread.
/// The memory locking applies to the entire process, including:
/// - The main thread
/// - All threads created after the call
///
/// This happens because all threads in a process share the same virtual memory space, and
/// `mlockall()` operates on the entire process's memory space, not just the calling thread.
///
/// # Example: Correct usage
///
/// ```ignore
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Call early in main, before creating threads
/// lock_memory()?;
///
/// // Now create real-time threads...
/// std::thread::spawn(|| {
/// set_realtime_priority();
/// // Real-time code here
/// });
///
/// Ok(())
/// }
/// ```
///
/// # Example: Incorrect usage
///
/// ```ignore
/// fn main() {
/// // Don't do this - calling in each thread is unnecessary
/// std::thread::spawn(|| {
/// lock_memory().unwrap(); // Redundant if already called in main
/// set_realtime_priority();
/// });
/// }
/// ```
///
/// # How it works
///
/// [Rest of documentation as before...]
#[cfg(unix)]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
use libc::{MCL_FUTURE, mlockall};
unsafe {
if mlockall(MCL_FUTURE) != 0 {
error!("Failed to lock memory: {}", std::io::Error::last_os_error());
return Err(
format!("Failed to lock memory: {}", std::io::Error::last_os_error()).into(),
);
} else {
debug!("Successfully locked memory for the process");
}
}
Ok(())
}
#[cfg(not(unix))]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
log::error!("This platform does not support memory locking");
Ok(())
}
pub fn set_core_affinity(core_id: usize) -> Result<(), anyhow::Error> {
let cores = core_affinity::get_core_ids().unwrap_or_default();
let core = cores.get(core_id);
if let Some(core) = core {
core_affinity::set_for_current(*core);
debug!(
"Set core affinity of thread \"{:?}\" to core {:?}",
std::thread::current().name().unwrap_or("unknown"),
core
);
Ok(())
} else {
error!("No cores available to set affinity");
Err(anyhow::anyhow!("No cores available to set affinity"))
}
}
+3 -5
View File
@@ -1,11 +1,9 @@
use std::{any::Any, sync::Arc};
use smol::{channel::Sender, lock::RwLock};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::{
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
};
use crate::machines::identification::DeviceIdentification;
pub mod panic;
pub mod registry;
@@ -27,7 +25,7 @@ pub trait SerialDeviceThread {
pub struct SerialDeviceNewParams {
pub path: String,
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
pub device_thread_panic_tx: smol::channel::Sender<(String, anyhow::Error)>,
}
#[derive(PartialEq, Clone, Debug)]
+4 -5
View File
@@ -1,13 +1,11 @@
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>>,
thread_panic_tx: Sender<(T, anyhow::Error)>,
) where
T: Send + Sync + Clone + 'static,
{
@@ -18,6 +16,7 @@ pub fn send_serial_device_panic<T>(
}
}
let device_id_clone = device_identifier.clone();
std::panic::set_hook(Box::new(move |panic_info| {
let thread_name = std::thread::current()
.name()
@@ -40,8 +39,8 @@ pub fn send_serial_device_panic<T>(
// Send device panic info through channel
smol::block_on(async {
let _ = thread_panic_tx
.send(SerialDeviceRemoval::Error(
device_identifier.clone(),
.send((
device_id_clone.clone(),
anyhow::anyhow!("{}", panic_message),
))
.await;
+13 -35
View File
@@ -21,11 +21,6 @@ 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<
@@ -36,13 +31,14 @@ pub struct SerialDetection<'serialdeviceregistry> {
Arc<RwLock<dyn SerialDevice>>,
),
>,
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
pub device_removal_signal_rx: Receiver<(String, anyhow::Error)>,
pub device_removal_signal_tx: Sender<(String, anyhow::Error)>,
}
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
let (device_removal_signal_tx, device_removal_signal_rx) =
unbounded::<(String, anyhow::Error)>();
SerialDetection {
serial_device_registry,
ports: HashMap::new(),
@@ -162,36 +158,18 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
match self.device_removal_signal_rx.try_recv() {
Ok((path, error)) => {
// remove the device when the tuple positon 1 equals signal
if let Some(info) = self.ports.get(&path.clone()) {
removed_signals.push(info.1.clone());
};
self.ports.remove(&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);
};
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);
tracing::trace!("Removed device: {:?}: {}", path, error);
}
Err(_) => {}
}
removed_signals
}
}
-4
View File
@@ -21,10 +21,6 @@ impl std::fmt::Debug for GenericEvent {
}
}
pub trait BuildEvent: Serialize + Send + Sync + Clone {
fn build(&self) -> Event<Self>;
}
#[derive(Debug, Clone, Serialize)]
pub struct Event<T>
where
+3 -45
View File
@@ -44,22 +44,6 @@ impl Namespace {
self.sockets.retain(|s| s.id != socket.id);
}
/// Disconnects all clients in the namespace.
///
/// This will forcefully disconnect all sockets and clear the socket list.
#[instrument(skip_all)]
pub fn disconnect_all(&mut self) {
tracing::info!("Disconnecting {} sockets in namespace", self.sockets.len());
// Disconnect each socket
for socket in &self.sockets {
let _ = socket.clone().disconnect(); // Ignore errors if socket is already disconnected
}
// Clear the socket list
self.sockets.clear();
}
/// Re-emits cached events to a specific socket.
///
/// This is typically used when a socket reconnects or joins an existing namespace
@@ -169,12 +153,6 @@ impl Namespace {
}
}
impl Drop for Namespace {
fn drop(&mut self) {
self.disconnect_all();
}
}
pub trait NamespaceBufferCacheKey {
fn to_key(&self) -> String;
}
@@ -193,7 +171,7 @@ pub trait CacheableEvents<Events> {
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>;
/// [`BufferFn`] that stores the last n events
/// [BufferFn] that stores the last n events
pub fn cache_n_events(n: usize) -> CacheFn {
Box::new(move |events, event| {
if events.len() >= n {
@@ -203,32 +181,12 @@ pub fn cache_n_events(n: usize) -> CacheFn {
})
}
/// [`BufferFn`] that stores only one event
/// [BufferFn] that stores only one event
pub fn cache_one_event() -> CacheFn {
cache_n_events(1)
}
/// [`BufferFn`] that stores first and last event
///
/// The primary use case of this function is to cache both the default state of a machine, which should be emitted first,
/// and the last event, which is the most recent state of the machine.
pub fn cache_first_and_last_event() -> CacheFn {
Box::new(move |events, event| {
// if the events length 0 or 1, we just push the event
if events.is_empty() || events.len() == 1 {
events.push(event.clone());
return;
}
// if the event length is 2 we remove the last event and append a new one
if events.len() == 2 {
events.remove(1);
events.push(event.clone());
return;
}
})
}
/// [`BufferFn`] that stores events for a certain duration
/// [BufferFn] that stores events for a certain duration
pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
Box::new(move |events, event| {
// Use event.ts instead of system time
-27
View File
@@ -1,27 +0,0 @@
use crate::transmission::Transmission;
#[derive(Debug, Clone)]
pub struct FixedTransmission {
ratio: f64,
}
impl Transmission for FixedTransmission {
fn get_ratio(&self) -> f64 {
self.ratio
}
}
impl FixedTransmission {
/// Creates a new FixedTransmission with the specified ratio.
///
/// # Arguments
///
/// * `ratio` - The transmission ratio (output over input).
///
/// # Returns
///
/// A new instance of FixedTransmission.
pub fn new(ratio: f64) -> Self {
Self { ratio }
}
}
-95
View File
@@ -1,95 +0,0 @@
use uom::si::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
length::meter,
velocity::meter_per_second,
};
pub mod fixed;
/// A trait representing a mechanical transmission system that converts input to output
/// with a specific gear ratio.
///
/// This trait provides methods for working with transmission ratios and converting
/// between input and output values based on the transmission's gear ratio.
pub trait Transmission {
/// Returns the transmission ratio of this transmission system.
///
/// The ratio represents the relationship between input and output speeds/torques.
/// A ratio greater than 1.0 indicates speed reduction (torque multiplication),
/// while a ratio less than 1.0 indicates speed increase (torque reduction).
///
/// # Returns
///
/// The transmission ratio as a floating-point number.
fn get_ratio(&self) -> f64;
/// Converts an input value to the corresponding output value using the transmission ratio.
///
/// This method multiplies the input by the transmission ratio to calculate the output.
/// Typically used for converting input speed to output speed in a transmission system.
///
/// # Arguments
///
/// * `input` - The input value to be converted
///
/// # Returns
///
/// The output value after applying the transmission ratio.
fn calculate_output(&self, input: f64) -> f64 {
input * self.get_ratio()
}
/// Converts an output value to the corresponding input value using the transmission ratio.
///
/// This method divides the output by the transmission ratio to calculate the required input.
/// Typically used for determining what input is needed to achieve a desired output.
///
/// # Arguments
///
/// * `output` - The output value to be converted back to input
///
/// # Returns
///
/// The input value required to produce the given output.
fn calculate_input(&self, output: f64) -> f64 {
output / self.get_ratio()
}
fn calculate_angular_output(&self, input: Angle) -> Angle {
Angle::new::<radian>(self.calculate_output(input.get::<radian>()))
}
fn calculate_angular_input(&self, output: Angle) -> Angle {
Angle::new::<radian>(self.calculate_input(output.get::<radian>()))
}
fn calculate_angular_velocity_output(&self, input: AngularVelocity) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
self.calculate_output(input.get::<radian_per_second>()),
)
}
fn calculate_angular_velocity_input(&self, output: AngularVelocity) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
self.calculate_input(output.get::<radian_per_second>()),
)
}
fn calculate_linear_output(&self, input: Length) -> Length {
Length::new::<meter>(self.calculate_output(input.get::<meter>()))
}
fn calculate_linear_input(&self, output: Length) -> Length {
Length::new::<meter>(self.calculate_input(output.get::<meter>()))
}
fn calculate_linear_velocity_output(&self, input: Velocity) -> Velocity {
Velocity::new::<meter_per_second>(self.calculate_output(input.get::<meter_per_second>()))
}
fn calculate_linear_velocity_input(&self, output: Velocity) -> Velocity {
Velocity::new::<meter_per_second>(self.calculate_input(output.get::<meter_per_second>()))
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

-94
View File
@@ -1,94 +0,0 @@
# Code Style
# General Code styling
Try to adhere to the rust style guide:
https://doc.rust-lang.org/nightly/style-guide/
# Beckhoff terminals and Machines
For Beckhoff terminals you should first implement the basic functionality, then an Interpreter of the device/data is built on top.
A good example of how to do this is by looking at the following code:
el3021.rs , AnalogInputDevice
El3021 implements a so called AnalogInputDevice, which acts like an interface.
This allows us to use AnalogInputDevice instead of having to always specify which device should be used.
In Essence any Device that implements AnalogInputDevice can be used.
AnalogInputDevice is then used for creating an AnalogInput, which is plug and play for machines.
Then a further layer of Abstraction is used on top,
where the data is interpreted and incorporated into a sort of control loop for certain functions, for example:
```rust
// ScrewSpeedController controlls the speed (frequency) and direction of the motor
// AnalogInput is used to collect measurements from the pressure sensor.
// screw_speed_controller.rs
#[derive(Debug)]
pub struct ScrewSpeedController {
pub pid: ClampingTimeagnosticPidController,
pub target_pressure: Pressure,
pub target_rpm: AngularVelocity,
pub inverter: MitsubishiCS80,
pressure_sensor: AnalogInput,
last_update: Instant,
uses_rpm: bool,
forward_rotation: bool,
transmission_converter: TransmissionConverter,
frequency: Frequency,
maximum_frequency: Frequency,
minimum_frequency: Frequency,
motor_on: bool,
nozzle_pressure_limit: Pressure,
nozzle_pressure_limit_enabled: bool,
}
// Under the hood for ExtruderV2 a El3021 is used:
// (from new.rs of extruder)
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// But the extruder does not directly use the AnalogInput, instead the logic is decoupled in the screw_speed_controller:
let mut extruder: ExtruderV2 = Self {
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: Instant::now(),
mode: ExtruderV2Mode::Standby,
temperature_controller_front: temperature_controller_front,
temperature_controller_middle: temperature_controller_middle,
temperature_controller_back: temperature_controller_back,
temperature_controller_nozzle: temperature_controller_nozzle,
screw_speed_controller: screw_speed_controller,
emitted_default_state: false,
};
// Then in act.rs
// The extruder simply calls the update function of the screw_speed_controller,
// which contains "all" of the logic needed to properly drive the motor
self.screw_speed_controller.update(now_ts, true);
```
This is desired behaviour for all implemented devices, the machines should stay as modular as possible.
# Representing physical Units (Volt,Ampere,Hz,RPM etc)
To represent physical units like voltage ampere etc... we use uom (units of measurement), which acts as a wrapper for units and supplies all conversions between certain units.
For example:
```rust
// Lets say a device measures voltage, you just got the raw value
let volts : ElectricPotential = ElectricPotential::new::<volt>(raw_value_here);
// Then you can do this to get milli_volts
// In api responses we always use the RAW float value (64bit):
let milli_volts : f64 = volts.get::<millivolt>();
```
# General Advice
- Try to avoid duplicate code (DRY principle)
- Try to avoid lifetimes if at all possible
- Avoid excessive abstractions
- Avoid async code unless it is required
- Avoid taking Ownership of values, instead borrow the value
- When implementing a Trait for a struct, do it in the same file as the struct definition
- Split up large impl blocks into smaller impl blocks
-84
View File
@@ -1,84 +0,0 @@
# Environment Setup
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
## Ubuntu/Debian Installation
```bash
# Just press enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
apt install npm
apt install nodejs
# assuming you dont have git already, Git setup not included in here
apt install git
git clone git@github.com:qitechgmbh/control.git
cd control
cd electron
npm install
```
# Running Backend and Frontend Linux
## Backend
To Compile backend code and run it on Linux:
```bash
./cargo_run_linux.sh
```
The script sets capabilities on the compiled binary like raw socket access.
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
## Mock-Machine
The Mock-Machine can be used to test code that does not require an actual machine connection.
Like Frontend Code for example.
Mock machines can be used if the compile feature mock-machine is enabled.
```bash
# in root git folder
./cargo_run_linux.sh mock-machine
```
## Frontend
To run the Frontend Code:
```bash
# need to be in the electron folder and an additional terminal
cd electron
npm run start
```
# Contributing
Generally you contribute to the codebase by:
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
2. Work on it locally
3. Commit your changes locally
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
```bash
git fetch
git rebase origin/master
```
5. Push changes
```bash
# IF you needed to rebase
git push --force-with-lease
# ELSE you just push
git push
```
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
7. Request a Review and hope for the best :)
# Recommended Editor Setup
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
-166
View File
@@ -1,166 +0,0 @@
# Presets
To allow the user to save the current configuration of a machine, they can create a preset in the presets page. Presets are presited in electron's local storage.
Presets are all handled by the `PresetsPage`. To use the `PresetsPage` for a single machine, one must implement the following.
Create a zod schema for the data, the presets contain.
```tsx
const mock1PresetDataSchema = z
.object({
frequency1: z.number(),
frequency2: z.number(),
frequency3: z.number(),
})
.partial();
type Mock1 = typeof mock1PresetDataSchema;
```
Here, intensionally, `Mock1` is the schema type itself. It can be used as a type argument everything that uses presets.
Next, create a map containing all schema versions currently supported (the new schema, to be precise).
```tsx
const schemas = new Map([[1, mock1PresetDataSchema]]);
```
New, the schema will be used to parse version `1`. To create new versions, see migrations below.
One also needs to define a table, how the preset data should be shown to the user.
```tsx
const previewEntries: PresetPreviewEntries<Mock1> = [
{
name: "Frequency 1",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
},
{
name: "Frequency 2",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
},
{
name: "Frequency 3",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
},
];
```
`renderValue` gets the data itself and renders the corresponding part of it. Returning undefined will automatically be rendered as the value is not set, showing "N/A".
Create a function for the new react component and use the appropreate machine.
```tsx
export function Mock1PresetsPage() {
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
useMock1();
// ...
}
```
Here, two more functions need to be created. const toPresetData = (s: typeof state): PresetData<Mock1> => ({
frequency1: s?.frequency1 ?? defaultState?.frequency1,
frequency2: s?.frequency2 ?? defaultState?.frequency2,
frequency3: s?.frequency3 ?? defaultState?.frequency3,The first takes in a preset and applies it on the machine. If data is missing, use the default defined by the backend.
```tsx
export function Mock1PresetsPage() {
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
useMock1();
const applyPreset = (preset: Preset<Mock1>) => {
const frequency1 = preset.data?.frequency1 ?? 100;
const frequency2 = preset.data?.frequency2 ?? 200;
const frequency3 = preset.data?.frequency3 ?? 500;
setFrequency1(frequency1);
setFrequency2(frequency2);
setFrequency3(frequency3);
};
// ...
}
```
The later does the opposite, it turns a machine state, comming from the backend, into the preset data
```tsx
export function Mock1PresetsPage() {
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
useMock1();
// ...
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
frequency1: s?.frequency1 ?? defaultState?.frequency1,
frequency2: s?.frequency2 ?? defaultState?.frequency2,
frequency3: s?.frequency3 ?? defaultState?.frequency3,
});
// ...
}
```
Having all this, we can simple pass it to the `PresetsPage`
```tsx
export function Mock1PresetsPage() {
// ...
return (
<PresetsPage
machine_identification={mock1.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={toPresetData(defaultState)}
/>
);
}
```
Also mind the machine identification, that has to match the machine we are using and the schema versionen. Here we pass `1`, as we also used `1` in the schemas map.
## Migrations
Migrations are handled by the `PresetsPage`, however, one still has to define what to migrate.
To do so, first create the new schema.
```tsx
const fancyNewMock1PresetDataSchema = z
.object({
frequency1_now_with_ai: z.number(),
frequency2: z.number(),
frequency3: z.number(),
frequency4: z.number(),
})
.partial();
type Mock1 = typeof fancyNewMock1PresetDataSchema;
```
and then, also create a migration that uses the new schema, but can handle the old format.
```tsx
const mock1PresetDataSchemaMigration: PresetSchema = z.preprocess(
(obj: any): PresetData<Mock1> => ({
frequency1_now_with_ai: obj?.frequency1, // Here, a field have been renamed
frequency2: obj?.frequency2,
frequency3: obj?.frequency3,
frequency4: 666.0, // A new field is added. Initialize it with default the value
}),
fancyNewMock1PresetDataSchema,
);
```
Usings zods preprocessing step,
we can first transform the data as needed and then validate using the new schema accordingly.
We have to register both schemas in our schema map.
```tsx
const schemas = new Map<number, Mock1>([
[1, mock1PresetDataSchemaMigration as Mock1],
[2, fancyNewMock1PresetDataSchema],
]);
```
Mind the cast, which is ok, because `mock1PresetDataSchemaMigration` parses `fancyNewMock1PresetDataSchema` by construction.
Lastly, simply change the schema version being passed to the `PresetsPage`.
```diff
@@ -62,7 +77,7 @@ export function Mock1PresetsPage() {
machine_identification={mock1.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
- schemaVersion={1}
+ schemaVersion={2}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={toPresetData(defaultState)}
```
Make sure, that all (exsiting and new migrations) return a schema version 2 conform format.
Of corse, one can chain migrations of needed - just the end result must be complient.
-27
View File
@@ -1,27 +0,0 @@
# Testing Ram and Cpu (Hardware)
There were instability problems on one of the hmi panel pc's related to ram.
To test Ram and Cpu for stability problems we use memtest86.
You can download it here: https://www.memtest86.com/download.htm
Then in the unzipped folder you have to flash the .img file to a USB drive with atleast 2GB of space.
We already have USB-Sticks flashed with memtest though.
1. When starting up the Pc mash Del (Entf in german) on an external keyboard and enter the bios
2. Select the Memtest86 usb stick in the Bios
3. Save changes and exit
4. Now memtest boots up, just let it do its thing from here
It works if the screen looks something like this:
![](../assets/Memtest86_v83p.png)
Simply let it do one Pass ~20 minutes and shutdown and unplug usb stick afterwards
If it freezes completely -> unstable
If it shows errors the ram should be swapped out and the test repeated
-13
View File
@@ -1,13 +0,0 @@
# Electrical Diagrams
This directory contains electrical wiring diagrams for various machines and systems supported by the control system.
## Available Diagrams
### Extruder
- **Location**: [`extruder/`](extruder/)
- **File**: [QiTech Pro Extruder Electrical Diagram 2025.pdf](extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025.pdf)
### Winder
- **Location**: [`winder/`](winder/)
- **File**: [QiTech Pro Winder Electrical Diagram.pdf](winder/QiTech_Pro_Winder_Electrical_Diagram.pdf)
+75 -144
View File
@@ -11,8 +11,6 @@ A functionality can be virtually wired to any device that matches the IO type. E
Functionalities of different machines should not be wired to the same devices because of possibly conflicting configuration and machine identification issues (which devices belong to what machine).
The IO types provide direct synchronous methods for interacting with hardware, eliminating the need for actor patterns while maintaining clean abstractions.
# Implemented IO types
- Digital Output (DO)
@@ -37,13 +35,13 @@ pub enum EL2002Port {
Then the device must implement the respective device trait like `DigitalOutputDevice`.
The `set_output` method is used to set the desired level of the output of a given port.
The `digital_output_write` is used to set the desired level of the output of a given port.
The `get_output` method is used to read the current state of the output of a given port.
The `digital_output_state` is used to read the current state of the output of a given port.
```rust
impl DigitalOutputDevice<EL2002Port> for EL2002 {
fn set_output(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_mut().expect(&expect_text).value = value.into(),
@@ -51,95 +49,84 @@ impl DigitalOutputDevice<EL2002Port> for EL2002 {
}
}
fn get_output(&self, port: EL2002Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2002Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
}),
}
}
}
```
## Using IO types in machine logic
## Implementing for an `Actor`
IO types now provide direct synchronous methods for reading and writing values, making them easy to use in machine logic without async/await patterns.
The `act` function of an `Actor` will be called after the inputs are read from the device and before the outputs are written for the next EtherCAT frame.
The actor needs to be constructed with the `DigitalOutput` and hold it in its struct.
```rust
#[derive(Debug)]
pub struct DigitalOutputToggler {
output: DigitalOutput,
}
impl DigitalOutputToggler {
pub fn new(output: DigitalOutput) -> Self {
Self { output }
}
pub fn toggle(&mut self) {
let current_state = self.output.get();
self.output.set(!current_state);
}
pub fn turn_on(&mut self) {
self.output.set(true);
}
pub fn turn_off(&mut self) {
self.output.set(false);
}
```
Implementing the `act` function which will toggle the output every cycle.
The `state` and `write` values are callbacks and must be wrapped with parentheses to be called.
```rust
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
let state = (self.output.state)().await;
match state.output.into() {
true => (self.output.write)(false.into()).await,
false => (self.output.write)(true.into()).await,
}
}
})
}
```
## Getting `DigitalOutput` from a `Device`
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. The IO type provides direct synchronous methods for interaction.
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. We can then provide this `DigitalOutput` instance to the `Actor` struct.
```rust
let el2002 = Arc::new(RwLock::new(EL2002::new()));
let el2002 = EL2002::new();
let digital_output = DigitalOutput::new(el2002, EL2002Port::DO1);
let mut toggler = DigitalOutputToggler::new(digital_output);
// Direct usage
toggler.turn_on();
toggler.toggle();
let actor = DigitalOutputToggler::new(digital_output);
```
# Creating a new IO type
To create a new IO type, we must know if it's writable (output) or readable (input), or combined (input/output) type.
For output types, we define the struct with sync closures for setting and getting values:
We first define the `DigitalOutput` struct with the `write` and `state` fields. Every IO needs a `state` field but only writable types need a `write` field.
The `state` field is an async callback which returns the `DigitalOutputState` struct.
The `write` field is an async callback which takes the value.
```rust
pub struct DigitalOutput {
set_output: Box<dyn Fn(DigitalOutputOutput) -> () + Send + Sync>,
get_output: Box<dyn Fn() -> DigitalOutputOutput + Send + Sync>,
pub write: Box<dyn Fn(DigitalOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> + Send + Sync>,
}
```
For input types, we only need the getter:
Next, we need to implement the `DigitalOutputState` and `DigitalOutputOutput` structs.
The `DigitalOutputState` contains the timestamps of the output (and inputs if applicable) and the `DigitalOutputOutput` struct (and the input struct if applicable). This is the return type of `DigitalOutput::state`.
The `DigitalOutputOutput` is both contained in the `DigitalOutputState` and is the input type of the `DigitalOutput::write` function. It implements the `From` trait in both directions to `bool` to allow easy access to the inner value on this simple type (as seen in the `act` example above).
```rust
pub struct DigitalInput {
get_input: Box<dyn Fn() -> Result<DigitalInputInput, anyhow::Error> + Send + Sync>,
#[derive(Debug, Clone)]
pub struct DigitalOutputState {
pub output: DigitalOutputOutput,
}
```
For combined input/output types, we have separate getters for input and output:
```rust
pub struct StepperVelocityEL70x1 {
set_output: Box<dyn Fn(StepperVelocityEL70x1Output) -> Result<(), Error> + Send + Sync>,
get_output: Box<dyn Fn() -> Result<StepperVelocityEL70x1Output, Error> + Send + Sync>,
get_input: Box<dyn Fn() -> Result<StepperVelocityEL70x1Input, Error> + Send + Sync>,
// ... additional closures as needed
}
```
Next, we need to implement the input and output structs separately.
For a digital output, we only need the output struct:
```rust
/// Output value
/// true: high
/// false: low
@@ -159,35 +146,8 @@ impl From<DigitalOutputOutput> for bool {
}
```
For input types, we define input structs:
```rust
#[derive(Debug, Clone)]
pub struct DigitalInputInput {
pub value: bool,
}
```
For combined input/output types, we define both structs separately, avoiding the need for state containers.
```rust
pub struct DigitalOutputOutput(pub bool);
impl From<bool> for DigitalOutputOutput {
fn from(value: bool) -> Self {
DigitalOutputOutput(value)
}
}
impl From<DigitalOutputOutput> for bool {
fn from(value: DigitalOutputOutput) -> Self {
value.0
}
}
```
Now we implement the constructor for the IO type. The `new` function takes a device and port and returns the IO instance with sync closures:
Now we need to implement the constructor for a `DigitalOutput` instance. The `new` function takes a device and a port and returns a `DigitalOutput` instance.
We need to construct the async closures which is a bit of type madness since we also need to move the device `Arc` into them.
```rust
impl DigitalOutput {
pub fn new<PORT>(
@@ -197,75 +157,46 @@ impl DigitalOutput {
where
PORT: Clone + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: DigitalOutputOutput| {
let mut device = block_on(device1.write());
device.set_output(port1.clone(), value);
});
let write = Box::new(
move |value: DigitalOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let device_clone = device1.clone();
let port_clone = port1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.digital_output_write(port_clone, value);
})
},
);
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_output = Box::new(move || -> DigitalOutputOutput {
let device = block_on(device2.read());
device.get_output(port2.clone())
});
DigitalOutput {
set_output,
get_output,
}
}
/// Set the digital output value
pub fn set(&self, enabled: bool) {
(self.set_output)(enabled.into());
}
/// Get the current output value
pub fn get(&self) -> bool {
let output = (self.get_output)();
output.into()
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.digital_output_state(port_clone)
})
},
);
DigitalOutput { write, state }
}
}
```
Lastly, we create the device trait that needs to be implemented by devices. The trait methods are simplified and separated by input/output:
Lastly, we can create the `DigitalOutputDevice` trait which needs to be implemented by the device.
```rust
pub trait DigitalOutputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: DigitalOutputOutput);
fn get_output(&self, port: PORT) -> DigitalOutputOutput;
fn digital_output_write(&mut self, port: PORT, value: DigitalOutputOutput);
fn digital_output_state(&self, port: PORT) -> DigitalOutputState;
}
```
For input devices:
```rust
pub trait DigitalInputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn get_input(&self, port: PORT) -> Result<DigitalInputInput, anyhow::Error>;
}
```
For combined input/output devices:
```rust
pub trait StepperVelocityEL70x1Device<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: StepperVelocityEL70x1Output) -> Result<(), Error>;
fn get_output(&self, port: PORT) -> Result<StepperVelocityEL70x1Output, Error>;
fn get_input(&self, port: PORT) -> Result<StepperVelocityEL70x1Input, Error>;
// Additional methods as needed for specific device functionality
fn get_speed_range(&self, port: PORT) -> SpeedRange;
}
```
Binary file not shown.
View File
Whitespace-only changes.
+36 -28
View File
@@ -134,38 +134,46 @@ A slave recognizes message data as one message when a **3.5 character long no-da
---
# Inverter settings
# Mitsubishi Inverter settings for Modbus RTU
## Motor Constants
Protocol Selection: set Pr 549 to 1 (Modbus RTU)
set 551 to a non zero value (9999)
Operation mode selection: set Pr 79 to 0
PU communication station number: set Pr 117 to a value of 1-32 (this is also the slave address)
PU communication speed: set Pr 118 the same baudrate as the Beckhoff terminal. For example Terminal has 19200 then you need to set 192
PU communication stop bit: set Pr 119 according to Beckhoff terminal setting
PU communication parity check: set Pr 120 according to Beckhoff terminal setting
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
# el6021 settings needed for Modbus RTU
Continuous is needed for Modbus RTU
8000:04 Enable send fifo data continuous TRUE
8000:06 Enable half duplex
8000:11 Enable Baud rate 19200 Baud
8000:15 Data Frame 8N2
# Inverter settings relevant for Motor
## Motor Constant
The following is a table that shows which Registers need to be set for the safe operation of the Motor.
| Parameter number (Pr) | Name | Value |
| --------------------- | ---------------------------- | ------------- |
| `71` | Applied Motor | 0 |
| `80` | Motor Capacity | 15 (1.5 kw) |
| `96` | Auto tuning setting/status | 1 |
| `1` | Max Frequency | 5000 |
| `2` | Min Frequency | 0 |
| `9` | Electronic Thermal O/L Relay | 3.30 (Ampere) |
| `3` | Base frequency | 5000 (50 Hz) |
| `19` | Base frequency voltage | 400 (Volt) |
| 71 | Applied Motor | 0 |
| 80 | Motor Capacity | 15 (1.5 kw) |
| 96 | Auto tuning setting/status | 1 |
| 1 | Max Frequency | 5000 |
| 2 | Min Frequency | 0 |
| 9 | Electronic Thermal O/L Relay | 3.30 (Ampere) |
| 3 | Base frequency | 5000 (50 Hz) |
| 19 | Base frequency voltage | 400 (Volt) |
## All Settings needed for the Inverter Communication/Operation with Extruder
## Settings relevant for Communication/Control
The Following settings are required to get the inverter Communication and Operation working properly
| Parameter Number (Pr) | Value | Description |
| --------------------- | ----- | -------------------------------------------------------------------------------------------- |
| `Pr. 78` | 0 | Allows Reverse Rotation |
| `Pr. 79` | 0 | Operation Mode Selection |
| `Pr. 117` | 1 | Station Number (Slave Number) |
| `Pr. 118` | 192 | Baudrate (192 → 19200 bps) |
| `Pr. 119` | 0 | Stop Bit Length |
| `Pr. 120` | 2 | Parity Check |
| `Pr. 121` | 9999 | PU communication retry count |
| `Pr. 122` | 1.0 | Communication Check Time (Time until no communication causes E.PUE fault), 0.1 seconds steps |
| `Pr. 338` | 0 | Communication operation command source, 0 -> given through PU |
| `Pr. 339` | 0 | Communication speed command source, 0 -> given through PU |
| `Pr. 340` | 1 | Startup in Network Mode |
| `Pr. 549` | 1 | Protocol Selection (1 = Modbus) |
| `Pr. 551` | 9999 | Automatically Detect PU Connector |
| Parameter number (Pr) | Name | Value |
| --------------------- | ------------------------------------ | ----- |
| 79 | Operation Mode | 0 |
| 340 | Communication startup mode selection | 1 |
The Combination of 79 = 0 and 340 = 1 starts the inverter in the NET operation Mode, which enables us to start/stop the motor.
+15 -16
View File
@@ -6,32 +6,31 @@ This guide will help you set up a NixOS system with QiTech Control and manage up
## Initial Setup
### 1. Install Git
### 1. Add Home Manager Channel
```bash
nixos-env -i git
sudo nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
sudo nix-channel --update
```
### 2. Clone the Control Repo
### 2. Configure System Files
Create the three main configuration files:
- `flake.nix` - Defines inputs, overlays, and system configuration
- `configuration.nix` - Main NixOS system configuration
- `home.nix` - Home Manager configuration for the qitech user
### 3. Build and Apply Configuration
```bash
git clone https://github.com/qitechgmbh/control
cd control
# Build and switch to the new configuration
sudo nixos-rebuild switch --flake .#nixos
```
### 3. Run Install Script
```bash
# run as admin either with sudo or as root!
sudo ./nixos-install.sh
```
After the Installation script is finished the Computer should have rebooted and automatically started the control software.
### 4. Manual configuration
Disable any power management features or display sleep settings in the GNOME settings.
Check that the on-screen keyboard is enabled under accessibility settings.
Disable any power management features or display sleep settings in the GNOME settings. Check that the on-screen keyboard is enabled under accessibility settings.
## System Configuration Overview
+1 -1
View File
@@ -3,6 +3,6 @@
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"endOfLine": "lf",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}
+20 -20
View File
@@ -1,21 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+1512 -1163
View File
File diff suppressed because it is too large. Load diff
+9 -12
View File
@@ -11,7 +11,6 @@
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint .",
"lint:write": "eslint . --fix",
"format": "prettier --check .",
"format:write": "prettier --write .",
"test": "vitest run",
@@ -24,14 +23,14 @@
"devDependencies": {
"@electron-forge/cli": "^7.6.1",
"@electron-forge/maker-deb": "^7.8.0",
"@electron-forge/maker-rpm": "^7.8.3",
"@electron-forge/maker-rpm": "^7.6.1",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^2.0.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
@@ -41,21 +40,21 @@
"@testing-library/user-event": "^14.6.1",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/node": "^24.2.1",
"@types/node": "^22.13.4",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"electron": "^37.2.6",
"electron": "36.4.0",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"prettier": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
@@ -66,20 +65,19 @@
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.8",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.2",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-query": "^5.66.7",
"@tanstack/react-router": "^1.105.0",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
@@ -99,7 +97,6 @@
"react-hook-form": "^7.57.0",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
"socket.io-msgpack-parser": "^3.0.2",
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
'@tailwindcss/postcss': {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

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