Compare commits

...
Author SHA1 Message Date
Robin Krämer 92cacbc6c8 Merge pull request #513 from qitechgmbh/510-add-a-getting-started-dev-documentation
510 add a getting started dev documentation
2025-07-21 14:28:26 +02:00
93094049+kraemr@users.noreply.github.com 4bad318ce7 updated readme and getting-started dev docs 2025-07-21 14:25:41 +02:00
93094049+kraemr@users.noreply.github.com 0eeb7f372e added Contributing 2025-07-21 14:18:30 +02:00
93094049+kraemr@users.noreply.github.com 1a72f3f8bb added inital dev docs and updated build script 2025-07-21 14:11:38 +02:00
Robin Krämer 522ace8a4f Merge pull request #508 from qitechgmbh/501-raise-pressure-limit-of-extruder-and-add-conifrmation
501 raise pressure limit of extruder and add confirmation to EditValue
2025-07-21 12:05:50 +02:00
93094049+kraemr@users.noreply.github.com 5c00d28396 raised maximum for Target Pressure 2025-07-16 16:09:19 +02:00
93094049+kraemr@users.noreply.github.com 2b0cff358e added optional confirmation to EditValue 2025-07-16 16:08:49 +02:00
Robin Krämer c3b7f9d050 Merge pull request #498 from qitechgmbh/497-laser-control-page-diameter-circle-extends-over-the-boundaries
clamped diameter visualisation to avoid large circles
2025-07-15 16:08:17 +02:00
93094049+kraemr@users.noreply.github.com a7fce331a0 fixed lint error 2025-07-15 16:06:24 +02:00
93094049+kraemr@users.noreply.github.com 6dd8573584 removed console log and cleaned up code 2025-07-15 16:04:24 +02:00
93094049+kraemr@users.noreply.github.com 124418dc7a clamped diameter visualisation to avoid large circles 2025-07-15 16:00:49 +02:00
Robin Krämer 59e8ddbf82 Update CHANGELOG.md 2025-07-15 12:28:14 +02:00
Robin Krämer 366f58f072 Update CHANGELOG.md 2025-07-15 12:25:06 +02:00
Robin Krämer 7a12950c22 Update CHANGELOG.md 2025-07-15 12:24:47 +02:00
Robin Krämer 668000adf9 Update CHANGELOG.md 2025-07-15 12:24:07 +02:00
Robin Krämer f2a2fee8f6 Delete tmux-dev.sh
Keep this out of master branch please
2025-07-15 12:22:47 +02:00
Robin Krämer b412fb9d4e Merge pull request #492 from qitechgmbh/169-conditions-to-enter-extrusion-mode
added confirmation for extrusion mode based on temperature
2025-07-14 17:49:34 +02:00
93094049+kraemr@users.noreply.github.com 73de9f2e4c added confirmation for extrusion mode based on temperature 2025-07-14 17:47:49 +02:00
Robin Krämer f4e5570ad5 Merge pull request #480 from qitechgmbh/471-add-winder-auto-stop-or-pull-after-n-meters-of-filament
471 add winder auto stop or pull after n meters of filament
2025-07-14 17:46:09 +02:00
93094049+kraemr@users.noreply.github.com 2d1da2bbf5 small refactor/cleanup 2025-07-14 17:43:01 +02:00
93094049+kraemr@users.noreply.github.com 23cb7ce992 frontend/backend refactor and changed progress to meters 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 941a1edb0c backend refactored 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 19af948deb frontend refactoring 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 63ea9a0f7d fixed lint error 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 784e0f8652 changed naming 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com e5f6f3f8e6 implemented backend 2025-07-14 17:33:31 +02:00
93094049+kraemr@users.noreply.github.com 7c47ef0dee implemented frontend for auto pull/stop 2025-07-14 17:33:31 +02:00
Robin Krämer 2a668d8602 Merge pull request #495 from qitechgmbh/138-update-nixos-quickstart-guidemd-to-match-current-installation-flow
138 update nixos quickstart guidemd to match current installation flow
2025-07-14 17:30:13 +02:00
93094049+kraemr@users.noreply.github.com efe18e9d81 updated quick-start guide 2025-07-14 17:29:07 +02:00
Icedsoda cf20bb57b9 Update CHANGELOG.md 2025-07-14 14:24:44 +02:00
Icedsoda 284f427da0 Update CHANGELOG.md 2025-07-14 14:24:08 +02:00
Icedsoda 9f55171286 Merge pull request #121 from qitechgmbh/fix/7
Use Realtime Scheduling
2025-07-14 14:20:54 +02:00
Icedsoda d427f50037 put both realtime threads on separate cores 2025-07-14 14:19:11 +02:00
Icedsoda 5bfaf8178f fix realtime logging 2025-07-14 14:19:11 +02:00
Icedsoda d57866e1a6 move lock emmory after tracing
Only lock future memory

disable memory locking
2025-07-14 14:19:11 +02:00
Icedsoda 401f1bf7db # This is a combination of 2 commits.
# This is the 1st commit message:

move lock emmory after tracing

# This is the commit message #2:

Only lock future memory
2025-07-14 14:18:56 +02:00
Icedsoda a46dcbd0ae Set core affinity 2025-07-14 14:18:56 +02:00
Icedsoda d2fea5f23b Add logging 2025-07-14 14:18:21 +02:00
Icedsoda 11b78638ec Merge pull request #487 from qitechgmbh/195-remove-actor-trait
Remove `Actor` trait
2025-07-11 19:09:03 +02:00
Icedsoda d6b6f76b55 move serial_interface_actors to modbus_serial_interface 2025-07-11 18:14:58 +02:00
Icedsoda bfba0b1d62 refactor mondbus/mitsubishi stuff 2025-07-11 16:52:29 +02:00
Icedsoda 3b93e6ddc6 Remove Actor trait 2025-07-11 16:52:29 +02:00
Robin Krämer 2a0ba7f72a Merge pull request #392 from qitechgmbh/391-control-server-doesnt-shutdown-properly-on-a-panic
instead of panic_handling directly shutdown
2025-07-11 16:19:27 +02:00
93094049+kraemr@users.noreply.github.com ff9fbe6a6e instead of panic_handling directly shutdown 2025-07-11 16:02:59 +02:00
93094049+kraemr@users.noreply.github.com 7c712faff9 send pressure state event when pressure sensor disconnected 2025-07-11 14:56:12 +02:00
93094049+kraemr@users.noreply.github.com 5a4ca867a8 added wiring_error to AnalogInputState 2025-07-11 14:53:45 +02:00
93094049+kraemr@users.noreply.github.com 88633798e7 added frontend logic for pressure wiring error 2025-07-11 14:53:05 +02:00
Icedsoda 86df807b01 Merge pull request #490 from qitechgmbh/489-emit-default-state
Emit default state
2025-07-10 17:44:37 +02:00
Icedsoda 3ae04f5547 Emit default state 2025-07-10 17:40:54 +02:00
Icedsoda c1ad22266d Merge pull request #488 from qitechgmbh/refactor-mock-api
Use only a StateEvent and LiveValuesEvent on Laser V1
2025-07-10 16:52:14 +02:00
Icedsoda ec4f2517f6 Refactor API of MockMachine 2025-07-10 16:50:28 +02:00
Robin Krämer 4c73710665 Merge pull request #486 from qitechgmbh/485-extruder-requestresponse-out-of-sync-again
485 fixed out of sync error and sped up inverter reaction time
2025-07-08 11:49:04 +02:00
93094049+kraemr@users.noreply.github.com fcea61979f fixed out of sync error 2025-07-08 11:43:19 +02:00
Robin Krämer bc68644468 Merge pull request #484 from qitechgmbh/483-fix-extruder-timeout
483 fixed timeout issue
2025-07-07 19:37:27 +02:00
93094049+kraemr@users.noreply.github.com 652af28c76 fixed timeout issue 2025-07-07 19:34:09 +02:00
Robin Krämer 31d6c5c4b9 Merge pull request #470 from qitechgmbh/196-extract-act-function-from-mitsubishiinverterrs485actor-by-creating-serialinterfaceactor
196 extract act function from mitsubishiinverterrs485actor by creating serialinterfaceactor
2025-07-07 16:51:26 +02:00
93094049+kraemr@users.noreply.github.com 28fdbd8bce Fixed Error Msgs in New 2025-07-07 16:36:03 +02:00
93094049+kraemr@users.noreply.github.com 314f5b3eed added improved timeout 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 8983a67390 updated imports 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 74d67cc261 moved implementations of serial_interface and invertercontroller 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 2c4c64c0b7 prevented possible panics and finished refactoring 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com 758816815e extracted functionality from mitsubishicontroller and refactored 2025-07-07 16:29:10 +02:00
93094049+kraemr@users.noreply.github.com bf52030506 refactored out the act functionanality 2025-07-07 16:26:59 +02:00
93094049+kraemr@users.noreply.github.com 29cf495dd0 started refactoring 2025-07-07 16:26:59 +02:00
Robin Krämer 500917937b Merge pull request #476 from qitechgmbh/475-inverter-waits-unnecessarily-long
475 inverter actor only waits when necessary
2025-07-04 16:57:43 +02:00
93094049+kraemr@users.noreply.github.com 812080341c inverter actor only waits when necessary 2025-07-04 16:54:02 +02:00
Robin Krämer 221fa7edb3 Merge pull request #474 from qitechgmbh/473-usb-disconnects-randomly-nixos
473 added usbcore autosuspend to os config
2025-07-04 15:47:00 +02:00
93094049+kraemr@users.noreply.github.com fd58786524 added usbcore autosuspend to os config 2025-07-04 15:43:21 +02:00
Robin Krämer a3398cbe34 Merge pull request #468 from qitechgmbh/467-pid-pressure-mode-overshoots-drastically-when-switching-from-rpm
467 pid pressure mode overshoots drastically when switching from rpm
2025-07-03 16:46:39 +02:00
Robin Krämer b4cb5916bd remove println from clamping_timeagnostic_pid.rs 2025-07-03 16:44:41 +02:00
93094049+kraemr@users.noreply.github.com b7e9c3c1eb removed print 2025-07-03 16:15:04 +02:00
93094049+kraemr@users.noreply.github.com c291c532ec fine tuned Pressure PID 2025-07-03 15:52:43 +02:00
Robin Krämer edded9e5a6 Merge pull request #466 from qitechgmbh/465-state-data-doesnt-work-properly-when-winder-is-in-use
465 now emits state on extruder startup
2025-07-03 14:03:50 +02:00
93094049+kraemr@users.noreply.github.com eb2f0f34a4 now emits state on startup 2025-07-03 14:00:01 +02:00
Icedsoda a1fca74d66 Merge pull request #464 from qitechgmbh/453-use-only-a-stateevent-and-livevaluesevent-on-extruder-v2
refactor(extruder2): Consolidate state and event handling
2025-07-01 19:06:19 +02:00
Icedsoda 36089c3e75 refactor(extruder2): Consolidate state and event handling
This patch refactors the state management for the Extruder v2 on both the frontend and backend to use a more centralized and robust model.

- **Backend (Rust):**
  - Replaced numerous individual events with two primary events: `StateEvent` for configuration and less frequent state, and `LiveValuesEvent` for high-frequency telemetry.
  - State-mutating API calls now emit the entire updated `StateEvent`.

- **Frontend (TypeScript):**
  - Replaced multiple specialized hooks (`useInverter`, `useMode`, `useMotor`, etc.) with a single, comprehensive `useExtruder2` hook.
  - Centralized all client-side state, including optimistic updates, into the new `useExtruder2` hook.
  - UI components are updated to consume all data and actions from the single hook, simplifying data flow.
  - Renamed action functions to a consistent verb-first convention (e.g., `extruderSetMode` -> `setExtruderMode`).
2025-07-01 18:52:25 +02:00
Icedsoda d27eecd54c Merge pull request #457 from qitechgmbh/454-use-only-a-stateevent-and-livevaluesevent-on-laser-v1
Use only a StateEvent and LiveValuesEvent on Laser V1
2025-07-01 17:29:12 +02:00
Icedsoda 183166d8d5 refactor(laser): Consolidate state and event handling
This commit refactors the state management and event handling for the laser machine on both the frontend and backend. The changes clarify the distinction between infrequently changing state and frequently updated live values.

- **State and Event Consolidation:**
    - Renamed `DiameterEvent` to `LiveValuesEvent` and `LaserStateEvent` to `StateEvent` for better clarity.
    - The `StateEvent` now nests laser properties under a `laser_state` object.
    - Frontend hook `useLaser1` now manages a single, consolidated state object with optimistic updates using `immer`.

- **API and Naming Simplification:**
    - Simplified mutation names by removing the "Target" prefix (e.g., `TargetSetTargetDiameter` is now `SetTargetDiameter`).
    - Renamed variables and functions in the frontend for better readability (e.g., `laserDiameter` to `diameter`, `laserSet...` to `set...`).

- **Improved Backend Efficiency:**
    - The backend now emits the full state only when it changes, rather than on every tick.
    - Live values (diameter) are still emitted at 60 FPS.
2025-07-01 16:59:00 +02:00
Icedsoda e01a8268aa Merge pull request #461 from qitechgmbh/459-serial-machines-wont-get-executed-if-ethercat-is-not-initalized
refactor: Run EtherCAT initialization in a separate thread
2025-07-01 16:48:31 +02:00
Icedsoda d4d1969ec3 refactor: Run EtherCAT initialization in a separate thread
This change refactors the EtherCAT initialization process to be non-blocking by moving it into its own dedicated thread.

- The `init_ethercat` function now spawns a named thread ("init_ethercat") to handle interface discovery and the setup loop.
- This prevents the main application from blocking during startup while searching for a valid EtherCAT interface.
- The initialization order in `main.rs` has been slightly adjusted.
- A minor optimization was made to the laser machine actor to avoid redundant `Instant::now()` calls.
2025-07-01 16:45:48 +02:00
Icedsoda 3dc371ce68 Merge pull request #458 from qitechgmbh/455-improve-or-replace-pressure-pid
455 improve or replace pressure pid
2025-07-01 16:12:45 +02:00
93094049+kraemr@users.noreply.github.com fba882adb8 updated Changelog 2025-07-01 16:08:32 +02:00
93094049+kraemr@users.noreply.github.com e8ca268f1c removed constants, uses uom now, smooth transition from pressure to rpm, rpm to pressure 2025-07-01 16:08:32 +02:00
93094049+kraemr@users.noreply.github.com 3a0840d758 Now set_regulation doesnt reset Pressure PID 2025-07-01 16:08:24 +02:00
93094049+kraemr@users.noreply.github.com 409460736d greatly improved the precision and stability of Pressure PID 2025-07-01 15:57:57 +02:00
Icedsoda 879de6ffcc Merge pull request #456 from qitechgmbh/452-use-only-a-stateevent-and-livevaluesevent-on-winder-v2
Use only a StateEvent and LiveValuesEvent on Winder V2
2025-07-01 14:31:52 +02:00
Icedsoda d513f1ad84 Rename Winder2 Mutations 2025-07-01 14:17:43 +02:00
Icedsoda 98b65fadb6 Simplify Winder2 Events 2025-07-01 14:17:34 +02:00
Icedsoda 832bafa217 Remove Autostop API 2025-07-01 10:39:23 +02:00
Icedsoda 769dc6220f Merge pull request #451 from qitechgmbh/450-laser-ui-stops-working-when-disconnecting-and-reconnecting
Disconnect all sockets when `Namespace` is `drop`ped
2025-07-01 10:03:24 +02:00
Icedsoda a15af859d4 Disconnect all sockets when Namespace is dropped 2025-07-01 10:01:53 +02:00
Robin Krämer 9a4d1e288d Merge pull request #449 from qitechgmbh/448-extruder-does-not-do-anything-in-pressure-pid-mode
replaced Timeagnostic PID with normal PID
2025-07-01 09:02:34 +02:00
93094049+kraemr@users.noreply.github.com e20791aa6b replaced Timeagnostic PID with normal PID 2025-07-01 08:58:23 +02:00
Robin Krämer 19c4a48150 Merge pull request #444 from qitechgmbh/440-add-clamping-to-all-error-values-for-timeagnosticpid
440 add clamping to all error values for timeagnosticpid
2025-06-30 16:56:41 +02:00
93094049+kraemr@users.noreply.github.com fba5c5c4f3 renamed timeagnostic to clamping_timeagnostic 2025-06-30 16:50:53 +02:00
93094049+kraemr@users.noreply.github.com 1f05edc79f added min_signal, max_signal and tests 2025-06-30 16:48:03 +02:00
93094049+kraemr@users.noreply.github.com f79c7ba58d added clamping to timeagnostic_pid 2025-06-30 16:47:27 +02:00
Icedsoda 5e7a6f1523 Merge pull request #443 from qitechgmbh/442-function-hashing-panics-sometimes
Replaces XOR-based hashing with byte folding methods
2025-06-30 16:42:24 +02:00
Icedsoda cc41a2e696 Replaces XOR-based hashing with byte folding methods
Replaces previous XOR-based hashing functions with new byte folding methods for improved clarity and flexibility. Introduces a djb2 hashing implementation for non-cryptographic use cases.

Updates affected modules to use the new hashing functions, simplifying logic and enhancing maintainability.

Fixes inconsistencies in hashing logic across different data types.
2025-06-30 16:40:45 +02:00
Icedsoda 32ba1b0f75 Merge pull request #441 from qitechgmbh/markdown-inline-styles-in-headings
Support inline styles in markdown headings
2025-06-30 16:15:22 +02:00
Icedsoda d38b7e8051 Support inline styles in markdown headings 2025-06-30 16:08:00 +02:00
Robin Krämer 353be9a4f8 Merge pull request #439 from qitechgmbh/436-extruder-doesnt-properly-stay-in-pressure-mode
added an integral windup prevention (clamp) for integral error
2025-06-30 15:33:35 +02:00
Icedsoda ff14618ec0 Update Changelog for 2.1.0 2025-06-30 14:50:50 +02:00
93094049+kraemr@users.noreply.github.com 208306cba5 added an integral windup prevention (clamp) for integral error 2025-06-30 14:46:28 +02:00
Darmenov Alisher 7ef2c95b32 Merge pull request #415 from qitechgmbh/fix/414
Enhance Big Graph
2025-06-30 14:26:06 +02:00
Anatomik5 a41c822618 enhanced animation, graph logic, buttons, excel and etc. 2025-06-30 14:25:37 +02:00
Icedsoda 69a3cdbef3 Merge pull request #429 from qitechgmbh/428-make-adaptivespoolspeedcontroller-core-parameters-customizable
Make `AdaptiveSpoolSpeedController` Core Parameters Customizable
2025-06-28 17:23:49 +02:00
Icedsoda ff51018d35 Make AdaptiveSpoolSpeedController Core Parameters Customizable 2025-06-28 17:22:18 +02:00
Icedsoda a228906eed Merge pull request #427 from qitechgmbh/426-show-estimated-spool-diameter
Show estimated diameter
2025-06-28 16:49:29 +02:00
Icedsoda a238fc9246 Show estimated diameter 2025-06-28 16:48:00 +02:00
Icedsoda 82a7e2fbfa Merge pull request #401 from qitechgmbh/388-improve-spool-speed-controller-again
Improve spool speed controller (again)
2025-06-28 15:58:33 +02:00
Icedsoda 23b2fa5faf Merge pull request #424 from qitechgmbh/403-cancel-update
Make an update cancelable
2025-06-28 15:10:35 +02:00
Icedsoda 1d78bacbe8 fix test errors 2025-06-28 14:36:40 +02:00
Icedsoda 043e781049 Improve AdaptiveSpoolSpeedController 2025-06-28 14:36:40 +02:00
Icedsoda 2d23d9db2f Make two spool speed algos available 2025-06-28 14:36:40 +02:00
Icedsoda c260403370 Make update store global 2025-06-28 14:07:35 +02:00
Icedsoda 8ac9b73bd5 Delete repo folder using fs 2025-06-28 12:41:14 +02:00
Icedsoda 609cce0a42 Make update cancelable
use tree-kill
2025-06-28 12:40:58 +02:00
Icedsoda ec304649bd Merge pull request #423 from qitechgmbh/422-warning-input-crane-has-an-override-for-a-non-existent-input-nixpkgs
Fix crane nixos issue
2025-06-28 12:12:21 +02:00
Icedsoda 217edd7db0 Add update-flake-lock.sh 2025-06-28 12:03:28 +02:00
Icedsoda 5d441b2b9a Fix crane nixos issue 2025-06-28 12:01:25 +02:00
Icedsoda 157989316e Merge branch 'rename-machine-presets' 2025-06-27 20:05:10 +02:00
Oshgnacknak 7fc5a35b6a Run formatter in ci to test 2025-06-27 20:02:32 +02:00
Oshgnacknak a93c863688 Move machine properties to their own file
- rename "Presets" to "Properties"

Script to start both dev servers using tmux

Rename MachinePresets to MachineProperties
2025-06-27 20:02:32 +02:00
Icedsoda 7735a76b1f Merge pull request #412 from qitechgmbh/411-make-nixos-rust-builds-faster
Make nixos rust builds faster
2025-06-27 19:11:48 +02:00
Icedsoda 58b0412c68 Use 2 cargo build jobs 2025-06-27 19:11:16 +02:00
Icedsoda 7ade900f46 Use crane for nixos rust builds 2025-06-27 19:11:16 +02:00
Icedsoda ad678e9c67 remove rust overlay 2025-06-27 19:11:16 +02:00
Icedsoda 6455a8b6f1 Merge pull request #419 from qitechgmbh/418-traverse-doesnt-move-at-low-spool-speeds
Change `steps_per_second` from `i32` to `f64`
2025-06-27 17:24:04 +02:00
Icedsoda b1b26fb5ae Change steps_per_second from i32 to f64 2025-06-27 17:22:10 +02:00
Robin Krämer d68be75be1 Merge pull request #417 from qitechgmbh/416-remove-arbitrary-limits-in-extruder-frontend
removed hardcoded limits from HeatingZone, now configurable
2025-06-27 16:30:43 +02:00
93094049+kraemr@users.noreply.github.com f40a5ac3af removed hardcoded limits from HeatingZone, now configurable 2025-06-27 16:04:08 +02:00
Robin Krämer 0c2cf6f460 Merge pull request #413 from qitechgmbh/186-fix-rust-warnings-in-modbus-implementation
fixed warnings in modbus and extruder
2025-06-27 12:56:35 +02:00
93094049+kraemr@users.noreply.github.com b5a989b7fe fixed warnings in modbus and extruder 2025-06-27 12:51:41 +02:00
Icedsoda ab9fad2514 Add Wireshark (#410)
* Add wireshark

* fix wireshark permision issue
2025-06-27 12:35:15 +02:00
Icedsoda e3e7cbb20f Increase PDU timeout 2025-06-26 17:40:23 +02:00
Icedsoda 7b1395b803 Merge pull request #405 from qitechgmbh/404-remove-hover-styling-of-buttons
Remove `hover:` styling
2025-06-26 17:32:18 +02:00
Icedsoda d67db133ea Remove hover: styling 2025-06-26 15:49:16 +02:00
Robin Krämer d74f3aad96 Merge pull request #400 from qitechgmbh/342-mode-button-switches-back-and-forth
Fixed Mode button and Regulation button switching back and forth
2025-06-26 13:44:55 +02:00
93094049+kraemr@users.noreply.github.com 7f2b24f075 fixed warning 2025-06-26 13:41:47 +02:00
93094049+kraemr@users.noreply.github.com d609d41628 Fixed Mode button and Regulation button switching back and forth 2025-06-26 13:35:47 +02:00
Darmenov Alisher 50c241dd59 Merge pull request #399 from qitechgmbh/fix/393
Fix/393
2025-06-25 19:10:18 +02:00
Darmenov Alisher 1a6e7da996 Merge branch 'master' into fix/393 2025-06-25 19:10:12 +02:00
Anatomik5 3a0eaf53f3 handlers fix 2025-06-25 19:04:44 +02:00
Anatomik5 16a952349f graphs enchanced 2025-06-25 18:47:55 +02:00
Anatomik5 c40f256df5 graph visual enchance 2025-06-25 18:42:30 +02:00
Anatomik5 8f0cc645ef small fix graphs 2025-06-25 17:57:18 +02:00
Anatomik5 1f436a6abe color fix 2025-06-25 17:15:32 +02:00
Anatomik5 477b44c8bb fix graph and button color 2025-06-25 17:15:04 +02:00
Anatomik5 4147528d0e fixed scale problem 2025-06-25 16:01:19 +02:00
Anatomik5 2b533f2c57 fix 2 2025-06-24 17:53:40 +02:00
Darmenov Alisher afaf8c919e Merge branch 'master' into fix/393 2025-06-24 17:08:57 +02:00
Anatomik5 2e87efdb63 update handlers and graph visual 2025-06-24 17:07:15 +02:00
Anatomik5 65fe3626ba graphs enchanced 2025-06-24 16:11:00 +02:00
Darmenov Alisher 36bae74613 Merge pull request #390 from qitechgmbh/fix/389
fix
2025-06-24 11:01:58 +02:00
Anatomik5 2e34fa11f8 fix 2025-06-24 11:01:38 +02:00
Robin Krämer 3410493c62 Merge pull request #387 from qitechgmbh/386-add-configuration-for-pressure-pid
386 add configuration for pressure pid
2025-06-23 18:33:12 +02:00
93094049+kraemr@users.noreply.github.com f13ad8fd0a fixed errors 2025-06-23 18:29:25 +02:00
93094049+kraemr@users.noreply.github.com 8208762da5 removed temperature pid kept pressure pid 2025-06-23 18:24:42 +02:00
93094049+kraemr@users.noreply.github.com 1beb01da98 Revert "Revert "frontend changes for api""
This reverts commit eb08493fdc.
2025-06-23 18:21:56 +02:00
93094049+kraemr@users.noreply.github.com e3dcfaec3b Revert "Revert "bakcend api""
This reverts commit 11e965ddeb.
2025-06-23 18:19:27 +02:00
Robin Krämer efbf7bc350 Merge pull request #378 from qitechgmbh/377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses
377 inverter state machine and el6021 incorrectly handle requestresponses
2025-06-23 17:23:54 +02:00
93094049+kraemr@users.noreply.github.com 6cf769891f removed unnecessary comment 2025-06-23 17:21:17 +02:00
Robin Krämer 840e4b8566 Merge pull request #385 from qitechgmbh/384-add-graph-to-extruder
added extruder graphs
2025-06-23 17:16:42 +02:00
93094049+kraemr@users.noreply.github.com 38341f6968 added extruder graphs 2025-06-23 17:13:26 +02:00
93094049+kraemr@users.noreply.github.com 80e9726ba3 simplified SerialInterface 2025-06-23 17:07:21 +02:00
93094049+kraemr@users.noreply.github.com 8afce68864 cleanup 2025-06-23 16:10:37 +02:00
Robin Krämer 5f7b7544c3 Merge pull request #381 from qitechgmbh/355-enable-inverter-timeout-when-communication-is-interrupted
355 enable inverter timeout when communication is interrupted
2025-06-23 15:20:59 +02:00
93094049+kraemr@users.noreply.github.com 27897182f9 updated docs 2025-06-23 15:17:54 +02:00
93094049+kraemr@users.noreply.github.com 2af1f0d79c updated Docs 2025-06-23 15:16:56 +02:00
Robin Krämer 4eeaebe355 Merge pull request #380 from qitechgmbh/379-dre-laser-graph-is-broken
fixed assignment error
2025-06-23 14:45:43 +02:00
93094049+kraemr@users.noreply.github.com 1dc85828d9 fixed assignment error 2025-06-23 14:42:59 +02:00
Darmenov Alisher bf5408cd18 Merge pull request #324 from qitechgmbh/fix/320
Group Excel Button
2025-06-23 12:39:13 +02:00
Anatomik5 d80b06256a nicer code + props sync 2025-06-23 12:36:16 +02:00
Anatomik5 aa0fb88fd2 nicer code + props sync 2025-06-23 12:36:16 +02:00
Darmenov Alisher fa85501578 Merge pull request #343 from qitechgmbh/fix/318
Add Graphs to Extruder
2025-06-23 12:28:14 +02:00
Anatomik5 500390e6dd extruder graph code enchancing 2025-06-23 12:22:48 +02:00
Anatomik5 bf73f996ca fix 2025-06-23 12:22:48 +02:00
Anatomik5 cb7fb902be extruder graph 2025-06-23 12:22:48 +02:00
Anatomik5 8fabc46ad4 fix padding 2025-06-23 12:22:48 +02:00
Anatomik5 38a7da7ee2 extruder graph var 1 2025-06-23 12:22:48 +02:00
Anatomik5 bbae14a727 Group Excel Button 2025-06-23 12:22:48 +02:00
93094049+kraemr@users.noreply.github.com f4609bfb3c Merge branch 'master' into 377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses 2025-06-23 12:04:27 +02:00
93094049+kraemr@users.noreply.github.com afa4663176 cleanup of EL6021 and inverter 2025-06-23 12:02:21 +02:00
93094049+kraemr@users.noreply.github.com 9b806ee8b3 Merge branch 'inverter-fix' into 377-inverter-state-machine-and-el6021-incorrectly-handle-requestresponses 2025-06-23 11:54:57 +02:00
93094049+kraemr@users.noreply.github.com 4975d2f6a7 fixed el6021 and inverter state machine 2025-06-23 11:54:30 +02:00
93094049+kraemr@users.noreply.github.com 1a675db872 Merge branch 'master' of github.com:qitechgmbh/control 2025-06-23 11:47:00 +02:00
Robin Krämer 0230ff4f81 Merge pull request #373 from qitechgmbh/360-show-inverter-errors-and-recover
360 show inverter errors and recover
2025-06-23 11:46:50 +02:00
Robin Krämer d8039156ae Update mod.rs 2025-06-23 11:45:09 +02:00
Darmenov Alisher e7105334c4 Merge pull request #358 from qitechgmbh/fix/319
Fix/319 Code Nicer
2025-06-20 15:44:22 +02:00
Darmenov Alisher ad04ea0a62 Merge branch 'master' into fix/319 2025-06-20 15:42:48 +02:00
Anatomik5 0128268719 nicer code + props sync 2025-06-20 15:28:14 +02:00
Anatomik5 7873975980 Group Excel Button 2025-06-20 15:17:29 +02:00
Icedsoda 57883fcb7e Add retry logic to syncToStore 2025-06-20 15:16:17 +02:00
Icedsoda 5979d9497e Ship Chrome DevTools in Production 2025-06-20 15:16:17 +02:00
Icedsoda 5dade5a633 Retry EtherCAT TX/RX if it fails 2025-06-20 15:16:17 +02:00
Icedsoda 694b46afbe Reduce logging of ethercrab 2025-06-20 15:16:17 +02:00
Icedsoda 5f81b2df5b increase namespace disconnect time 2025-06-20 15:16:17 +02:00
Icedsoda ff47ed3261 fix correct cacheing durations 2025-06-20 15:16:17 +02:00
Icedsoda da84e2a562 fix heating power value flickering 2025-06-20 15:16:17 +02:00
Icedsoda a272fd57bb Use 16.67 instead of 16ms for 60fps event updates
Emit events at 60FPS
2025-06-20 15:16:17 +02:00
Icedsoda e2fb623cbb Throttle Event Stores' setState to 60fps 2025-06-20 15:16:17 +02:00
Icedsoda d797584637 Use msgpack with SocketIO 2025-06-20 15:16:17 +02:00
Icedsoda 4549d806c9 Don't use parse to improve performance 2025-06-20 15:16:17 +02:00
Icedsoda a22d1d0ff1 Don't use produce to improve performance 2025-06-20 15:16:17 +02:00
Icedsoda adde259aa0 Reduce namespace logging 2025-06-20 15:16:17 +02:00
Icedsoda 2768609286 Fix some thread assigments 2025-06-20 15:16:17 +02:00
Icedsoda 1973d8841d Add global SocketIO Queue
add init_socketio_queue
2025-06-20 15:16:17 +02:00
Icedsoda b337031272 Pass GenericEvents using Arc
make socket queue faster
2025-06-20 15:16:17 +02:00
Icedsoda 507b7d507a Fix 0 division bug in Namespace 2025-06-20 15:16:17 +02:00
Icedsoda 2f231b203c Remove trait NamespaceInterface 2025-06-20 15:16:17 +02:00
Icedsoda 6e7a528d6f Workaround for socket performance issues (hopefully solved by #269) 2025-06-20 15:16:17 +02:00
Icedsoda 43dd37e8e0 Workaround for socket performance issues (hopefully solved by #269) 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 11e965ddeb Revert "bakcend api"
This reverts commit db039c7ccc.
2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com eb08493fdc Revert "frontend changes for api"
This reverts commit 7f7984ebe4.
2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 27b0386eac added comments to el6021 init 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com c4314a45dd refactored, clamped nozzle temp 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 02a436f053 el6021 properly handles receive_request 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com fea487c74e rpm now gets read perfectly again 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 7e317c4b23 temporary fix for rpm monitor 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 2c02d98890 bakcend api 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com bdbad1dca2 frontend changes for api 2025-06-20 15:16:17 +02:00
93094049+kraemr@users.noreply.github.com 202ae24eea fixed pressure PID 2025-06-20 15:16:17 +02:00
Icedsoda 38db16468e Add Extruder Icon 2025-06-20 15:16:17 +02:00
Icedsoda 1f272f4bce Selection buttons can have a confirmation prompt 2025-06-20 15:16:17 +02:00
dependabot[bot] ed2b94ce05 Bump globals from 15.15.0 to 16.2.0 in /electron
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.2.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.2.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
dependabot[bot] 146365fae2 Bump crc from 3.2.1 to 3.3.0
Bumps [crc](https://github.com/mrhooray/crc-rs) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/mrhooray/crc-rs/releases)
- [Commits](https://github.com/mrhooray/crc-rs/commits)

---
updated-dependencies:
- dependency-name: crc
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
dependabot[bot] 3bf9956c41 Bump @vitejs/plugin-react from 4.3.4 to 4.5.1 in /electron
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.4 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.5.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:16:17 +02:00
Icedsoda 7d8f833063 Add EL6021_IDENTITY_D 2025-06-20 15:16:17 +02:00
Icedsoda fcdac437eb Change Laser Icon 2025-06-20 15:16:17 +02:00
Icedsoda cb74dbc22b Reanem Dre to Laser 2025-06-20 15:16:17 +02:00
Icedsoda a749f98e81 Fix NixOS settings with wrong units 2025-06-20 15:15:11 +02:00
Icedsoda 634c00cdd1 Switches to tee for safer file redirection in script
Replaces `cat` with `sudo tee` to avoid potential permission issues and
ensure proper handling of file redirection when generating `installInfo.nix`.
Redirects output to `/dev/null` to suppress unnecessary output.
2025-06-20 15:15:11 +02:00
dependabot[bot] 5524be3ce0 Bump react-hook-form from 7.54.2 to 7.57.0 in /electron
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.2 to 7.57.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.2...v7.57.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 15:15:11 +02:00
Icedsoda 77bdb2f92c Bump OpenTelemetry Dependencies 2025-06-20 15:15:11 +02:00
nixos-install.sh 163fae21eb Remove KV pairs in logs 2025-06-20 15:15:11 +02:00
nixos-install.sh f58226aa27 Add Markdown TOC 2025-06-20 15:15:11 +02:00
nixos-install.sh 2742a296ef Revert "Add installInfo.nix file with current commit information"
This reverts commit d0a98557d3.
2025-06-20 15:15:11 +02:00
Icedsoda bb204d6efb delete summary.txt 2025-06-20 14:56:39 +02:00
Anatomik5 3a7c99cbbd fixed lint bugs 2025-06-18 17:51:47 +02:00
Anatomik5 7796d20984 nice code, modular 2025-06-18 16:40:00 +02:00
Anatomik5 9b3223c014 fixed problem with time and problem with mode switching 2025-06-18 16:40:00 +02:00
Anatomik5 a5c5ba4b87 nicer code + props sync 2025-06-18 16:40:00 +02:00
93094049+kraemr@users.noreply.github.com b53fea1812 frontendChanges for readStatus and resetInverter 2025-06-17 18:59:44 +02:00
93094049+kraemr@users.noreply.github.com b71a3456ce backend changes for readStatus and ResetInverter 2025-06-17 18:59:27 +02:00
93094049+kraemr@users.noreply.github.com 81cb44ed2b handle_response inverterstatus 2025-06-17 17:48:35 +02:00
Icedsoda 8c1fe6df1f Merge pull request #370 from qitechgmbh/add-retry-logic-to-sync-to-store
Add retry logic to `syncToStore`
2025-06-17 17:05:34 +02:00
Icedsoda 91fadd1c3d Merge pull request #372 from qitechgmbh/371-ship-chrome-devtools-in-production
Ship Chrome DevTools in Production
2025-06-17 16:51:02 +02:00
Icedsoda 28660a0873 Ship Chrome DevTools in Production 2025-06-17 16:49:57 +02:00
Icedsoda 5171ef5ef4 Add retry logic to syncToStore 2025-06-17 16:42:54 +02:00
Icedsoda 0cfd7eec9f Merge pull request #369 from qitechgmbh/367-retry-ethercat-txrx-if-it-fails
Retry EtherCAT TX/RX if it fails
2025-06-17 16:41:24 +02:00
Icedsoda 2ed2361fdf Retry EtherCAT TX/RX if it fails 2025-06-17 16:39:30 +02:00
Icedsoda 7c8b05241a Merge pull request #366 from qitechgmbh/365-reduce-logging-of-ethercrab
Reduce logging of ethercrab
2025-06-17 15:42:22 +02:00
Icedsoda 80e6edc474 Reduce logging of ethercrab 2025-06-17 15:29:06 +02:00
93094049+kraemr@users.noreply.github.com 9f13765db6 Merge branch 'master' of github.com:qitechgmbh/control 2025-06-17 14:13:56 +02:00
93094049+kraemr@users.noreply.github.com 465f8e9f07 added revision 0x10000 el6021 to frontend 2025-06-17 14:13:46 +02:00
Anatomik5 87112035fb nicer code + props sync 2025-06-16 17:13:05 +02:00
Anatomik5 ecb27ea068 sync enchance 2025-06-16 16:32:36 +02:00
Anatomik5 8c7a71a3f6 enchance 2025-06-16 15:41:38 +02:00
Anatomik5 4ea2d3bee2 button format changed 2025-06-16 01:17:06 +02:00
Anatomik5 42867bf41c Group Excel Button 2025-06-12 17:34:59 +02:00
207 changed files with 15678 additions and 11371 deletions

No files matched your search

+3
View File
@@ -36,6 +36,9 @@ jobs:
- name: Install
run: npm i
- name: Format
run: npm run format
- name: Lint
run: npm run lint
+69 -4
View File
@@ -1,4 +1,69 @@
# 2.0.0
Initial stable release of QiTech Control.
## Features
- Stabilized **Winder V2**
# `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**
Generated
+26
View File
@@ -581,17 +581,20 @@ dependencies = [
"approx",
"axum",
"bitvec",
"core_affinity",
"crc",
"erased-serde",
"ethercat_hal",
"ethercrab",
"interfaces",
"libc",
"serde",
"serde_json",
"serial",
"serialport",
"smol",
"socketioxide",
"textplots",
"tracing",
"uom",
]
@@ -612,6 +615,17 @@ 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"
@@ -992,6 +1006,7 @@ dependencies = [
"bitvec",
"ethercat_hal_derive",
"ethercrab",
"futures",
"rand",
"smol",
"uom",
@@ -1945,6 +1960,16 @@ 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"
@@ -2689,6 +2714,7 @@ dependencies = [
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
+5 -3
View File
@@ -41,6 +41,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
## Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
@@ -66,7 +68,6 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
# More Docs
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -90,8 +91,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- Write Device Identification
- Server
- [X] [Threading](./docs/control-loop.md)
- [X] [Logging](./docs/logging.md)
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
+18 -8
View File
@@ -1,13 +1,23 @@
#!/bin/bash
if [ $1 == "release" ]; then
echo "building Release Code"
cargo build --release
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"
else
echo "building Debug Code"
cargo build
echo "Building Debug Code"
fi
# set cap for server executable
sudo setcap cap_net_raw=eip ./target/debug/server
# run
# Build the project with or without features
cargo build $FEATURE
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap cap_net_raw=eip ./target/debug/server
fi
# Run the server
./target/debug/server
+6
View File
@@ -20,5 +20,11 @@ crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.6"
core_affinity = "0.8.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2.172"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8"
@@ -1,91 +0,0 @@
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
})
}
@@ -1,40 +0,0 @@
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);
})
}
}
@@ -1,35 +0,0 @@
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;
})
}
}
@@ -1,25 +0,0 @@
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);
})
}
}
@@ -1,51 +0,0 @@
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;
}
}
})
}
}
@@ -1,57 +0,0 @@
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();
}
})
}
}
@@ -1,38 +0,0 @@
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;
}
})
}
}
@@ -1,625 +0,0 @@
use super::Actor;
use crate::modbus::{
ModbusFunctionCode, ModbusRequest, ModbusResponse, calculate_modbus_rtu_timeout,
};
use axum::http::request;
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
*/
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: u16::MAX - 3,
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
ReadInverterStatus,
WriteFrequency,
InverterStatus,
InverterControl,
}
#[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>,
// 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,
}
}
/// 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_response(&mut self, resp: ModbusResponse) {
match self.next_response_type {
ResponseType::ReadFrequency => (),
ResponseType::ReadInverterStatus => (),
ResponseType::WriteFrequency => (),
ResponseType::ReadMotorFrequency => self.handle_motor_frequency(resp),
ResponseType::InverterStatus => (),
ResponseType::InverterControl => (),
ResponseType::NoResponse => (),
}
}
pub fn reset_state(&mut self) {
self.state = State::ReadyToSend;
self.last_ts = Instant::now();
self.request_map = HashMap::new();
self.response = None;
self.next_response_type = ResponseType::InverterStatus;
self.last_request_type = RequestType::OperationCommand;
self.last_message_size = 0;
self.frequency = Frequency::ZERO;
self.last_control_request_type = MitsubishiControlRequests::ReadInverterStatus;
}
}
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.last_ts = now_ts;
match self.state {
State::WaitingForResponse => {
let ret = self.read_modbus_response().await;
match ret {
Ok(ret) => {
self.handle_response(ret);
}
Err(_) => (), // Do nothing for now
}
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.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
self.state = State::WaitingForResponse;
}
_ => (),
}
self.response = None;
})
}
}
-19
View File
@@ -1,19 +0,0 @@
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 + '_>>;
}
@@ -1,91 +0,0 @@
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);
}
}
})
}
}
@@ -1,37 +0,0 @@
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))
}
}
@@ -1,222 +0,0 @@
/// 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);
}
}
@@ -1,51 +0,0 @@
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;
}
}
@@ -1,43 +0,0 @@
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;
}
})
}
}
@@ -1,23 +0,0 @@
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);
})
}
}
@@ -0,0 +1,241 @@
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);
}
}
+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;
+20 -1
View File
@@ -9,7 +9,6 @@ pub struct PidController {
ki: f64,
/// Derivative gain
kd: f64,
// State
/// Proportional error
ep: f64,
@@ -34,6 +33,25 @@ impl PidController {
}
}
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
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 update(&mut self, error: f64, t: Instant) -> f64 {
let signal = match self.last {
// First update
@@ -47,6 +65,7 @@ impl PidController {
// Set values
self.ep = ep;
self.ei = 0.0;
self.ed = 0.0;
self.last = Some(t);
@@ -1,92 +0,0 @@
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;
}
}
+90 -22
View File
@@ -1,40 +1,108 @@
pub fn xor_u128_to_u64(value: u128) -> u64 {
(value as u64) ^ ((value >> 64) as u64)
/// 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_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 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_u16(value: u128) -> u16 {
value
.to_le_bytes()
.iter()
.fold(0, |buf, &byte| buf ^ byte as u16)
/// 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_u8(value: u128) -> u8 {
value.to_le_bytes().iter().fold(0, |buf, &byte| buf ^ byte)
/// 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
}
// hash algorithm djb2
pub fn hashing(input_to_hash: &str) -> u128 {
let mut hash: u128 = 5381;
let chars = input_to_hash.chars();
/// 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)
}
for ch in chars {
hash = ((hash << 5) + hash) + (ch as u128);
/// 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);
}
hash
}
#[test]
fn test_hash() {
let test_case = "/dev/ttyUSB1";
let hash = hashing(test_case);
let hash_2bytes = xor_u128_to_u16(hash);
let hash_u32 = hash_djb2(test_case.as_bytes());
let hash_u16 = byte_folding_u16(&hash_u32.to_le_bytes());
tracing::info!("{}", test_case);
assert_eq!(test_case, "/dev/ttyUSB1");
assert!(hash == 8977446972540388683742);
assert!(hash_2bytes == 165);
println!("{:?}", hash);
assert_eq!(hash_u32, 266121182);
assert_eq!(hash_u16, 40962);
}
+699 -5
View File
@@ -1,4 +1,26 @@
/// 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>,
@@ -19,6 +41,24 @@ 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>,
@@ -31,24 +71,395 @@ where
T::from(result)
}
/// 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 {
/// 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 {
// When a is very close to zero, the function becomes linear
return normalized_value;
}
let numerator = a.exp().powf(normalized_value) - 1.0;
let denominator = a.exp() - 1.0;
let numerator = steepness.exp().powf(normalized_value) - 1.0;
let denominator = steepness.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;
@@ -178,6 +589,289 @@ 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() {
+415 -1
View File
@@ -1,4 +1,38 @@
pub fn retry<T, E, F>(n: usize, mut f: F) -> Result<T, E>
use std::future::Future;
/// Retries a fallible operation up to `n` times synchronously.
///
/// This function executes the provided closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - All retry attempts are exhausted
///
/// # Arguments
///
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
/// * `f` - A mutable closure that returns a `Result<T, E>`
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt if all retries are exhausted.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_n_times;
///
/// let mut attempt = 0;
/// let result = retry_n_times(3, || {
/// attempt += 1;
/// if attempt < 3 {
/// Err("failed")
/// } else {
/// Ok("success")
/// }
/// });
/// assert_eq!(result, Ok("success"));
/// ```
pub fn retry_n_times<T, E, F>(n: usize, mut f: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
@@ -11,3 +45,383 @@ where
}
Err(last_err.unwrap())
}
/// Retries a fallible async operation up to `n` times.
///
/// This function executes the provided async closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - All retry attempts are exhausted
///
/// Each attempt is awaited before proceeding to the next retry or returning.
///
/// # Arguments
///
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt if all retries are exhausted.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_n_times_async;
/// use std::sync::Arc;
/// use std::sync::atomic::{AtomicUsize, Ordering};
///
/// async fn example() {
/// let attempt = Arc::new(AtomicUsize::new(0));
/// let attempt_clone = attempt.clone();
/// let result = retry_n_times_async(3, move || {
/// let attempt = attempt_clone.clone();
/// async move {
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
/// if current < 3 {
/// Err("failed")
/// } else {
/// Ok("success")
/// }
/// }
/// }).await;
/// assert_eq!(result, Ok("success"));
/// }
/// ```
pub async fn retry_n_times_async<T, E, F, Fut>(n: usize, mut f: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut last_err: Option<E> = None;
for _ in 0..=n {
match f().await {
Ok(value) => return Ok(value),
Err(err) => last_err = Some(err),
}
}
Err(last_err.unwrap())
}
/// Retries a fallible operation until a closure decides to stop retrying.
///
/// This function executes the provided closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - The retry condition returns false
///
/// # Arguments
///
/// * `f` - A mutable closure that returns a `Result<T, E>`
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt when retries stop.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_conditionally;
///
/// let mut attempt = 0;
/// let result = retry_conditionally(
/// || {
/// attempt += 1;
/// if attempt < 3 {
/// Err(format!("failed attempt {}", attempt))
/// } else {
/// Ok("success")
/// }
/// },
/// |err| {
/// // Retry up to 3 times
/// !err.contains("attempt 3")
/// }
/// );
/// assert_eq!(result, Ok("success"));
/// ```
pub fn retry_conditionally<T, E, F, R>(mut f: F, mut should_retry: R) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
R: FnMut(&E) -> bool,
{
loop {
match f() {
Ok(value) => return Ok(value),
Err(err) => {
if !should_retry(&err) {
return Err(err);
}
}
}
}
}
/// Retries a fallible async operation until a closure decides to stop retrying.
///
/// This function executes the provided async closure `f` repeatedly until either:
/// - The operation succeeds (returns `Ok(T)`)
/// - The retry condition returns false
///
/// # Arguments
///
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
///
/// # Returns
///
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
/// from the last failed attempt when retries stop.
///
/// # Examples
///
/// ```rust
/// use control_core::helpers::retry::retry_conditionally_async;
/// use std::sync::Arc;
/// use std::sync::atomic::{AtomicUsize, Ordering};
///
/// async fn example() {
/// let attempt = Arc::new(AtomicUsize::new(0));
/// let attempt_clone = attempt.clone();
/// let result = retry_conditionally_async(
/// move || {
/// let attempt = attempt_clone.clone();
/// async move {
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
/// if current < 3 {
/// Err(format!("failed attempt {}", current))
/// } else {
/// Ok("success")
/// }
/// }
/// },
/// |err| {
/// // Retry up to 3 times
/// !err.contains("attempt 3")
/// }
/// ).await;
/// assert_eq!(result, Ok("success"));
/// }
/// ```
pub async fn retry_conditionally_async<T, E, F, Fut, R>(
mut f: F,
mut should_retry: R,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
R: FnMut(&E) -> bool,
{
loop {
match f().await {
Ok(value) => return Ok(value),
Err(err) => {
if !should_retry(&err) {
return Err(err);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
// Simple blocking executor for async functions in tests
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use std::thread;
struct DummyWaker;
impl std::task::Wake for DummyWaker {
fn wake(self: Arc<Self>) {}
}
let waker = Waker::from(Arc::new(DummyWaker));
let mut context = Context::from_waker(&waker);
let mut future = Box::pin(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(result) => return result,
Poll::Pending => {
thread::yield_now();
}
}
}
}
#[test]
fn test_retry_success_first_attempt() {
let result = retry_n_times(3, || Ok::<i32, &str>(42));
assert_eq!(result, Ok(42));
}
#[test]
fn test_retry_success_after_failures() {
let mut attempt = 0;
let result = retry_n_times(3, || {
attempt += 1;
if attempt < 3 { Err("failed") } else { Ok(42) }
});
assert_eq!(result, Ok(42));
assert_eq!(attempt, 3);
}
#[test]
fn test_retry_all_attempts_fail() {
let mut attempt = 0;
let result: Result<i32, &str> = retry_n_times(2, || {
attempt += 1;
Err("always fails")
});
assert_eq!(result, Err("always fails"));
assert_eq!(attempt, 3); // 0..=2 means 3 attempts
}
#[test]
fn test_retry_zero_retries() {
let mut attempt = 0;
let result: Result<i32, &str> = retry_n_times(0, || {
attempt += 1;
Err("failed")
});
assert_eq!(result, Err("failed"));
assert_eq!(attempt, 1); // Only one attempt
}
#[test]
fn test_retry_async_success_first_attempt() {
let result = block_on(retry_n_times_async(3, || async { Ok::<i32, &str>(42) }));
assert_eq!(result, Ok(42));
}
#[test]
fn test_retry_async_success_after_failures() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result = block_on(retry_n_times_async(3, move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
if *a < 3 { Err("failed") } else { Ok(42) }
}
}));
assert_eq!(result, Ok(42));
assert_eq!(*attempt.borrow(), 3);
}
#[test]
fn test_retry_async_all_attempts_fail() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, &str> = block_on(retry_n_times_async(2, move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
Err("always fails")
}
}));
assert_eq!(result, Err("always fails"));
assert_eq!(*attempt.borrow(), 3); // 0..=2 means 3 attempts
}
#[test]
fn test_retry_and_success_with_condition() {
let mut attempt = 0;
let result: Result<i32, String> = retry_conditionally(
|| {
attempt += 1;
if attempt < 3 {
Err(format!("failed attempt {}", attempt))
} else {
Ok(42)
}
},
|err| {
// Retry as long as it's not the 3rd attempt
!err.contains("attempt 3")
},
);
assert_eq!(result, Ok(42));
assert_eq!(attempt, 3);
}
#[test]
fn test_retry_and_stop_early() {
let mut attempt = 0;
let result: Result<i32, String> = retry_conditionally(
|| {
attempt += 1;
Err(format!("failed attempt {}", attempt))
},
|_err| {
// Stop after first failure
false
},
);
assert!(result.is_err());
assert_eq!(attempt, 1);
assert_eq!(result.unwrap_err(), "failed attempt 1");
}
#[test]
fn test_retry_async_success_with_condition() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, String> = block_on(retry_conditionally_async(
move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
if *a < 3 {
Err(format!("failed attempt {}", *a))
} else {
Ok(42)
}
}
},
|err| {
// Retry as long as it's not the 3rd attempt
!err.contains("attempt 3")
},
));
assert_eq!(result, Ok(42));
assert_eq!(*attempt.borrow(), 3);
}
#[test]
fn test_retry_async_stop_early() {
let attempt = Rc::new(RefCell::new(0));
let attempt_clone = attempt.clone();
let result: Result<i32, String> = block_on(retry_conditionally_async(
move || {
let attempt = attempt_clone.clone();
async move {
let mut a = attempt.borrow_mut();
*a += 1;
Err(format!("failed attempt {}", *a))
}
},
|_err| {
// Stop after first failure
false
},
));
assert!(result.is_err());
assert_eq!(*attempt.borrow(), 1);
assert_eq!(result.unwrap_err(), "failed attempt 1");
}
}
+1 -1
View File
@@ -1,10 +1,10 @@
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;
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::actors::Actor;
use crate::machines::new::MachineAct;
use api::MachineApi;
use new::MachineNewTrait;
use std::any::Any;
@@ -10,4 +10,4 @@ pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
pub trait Machine: MachineNewTrait + MachineApi + MachineAct + Any + Send + Sync + Debug {}
+5 -1
View File
@@ -8,7 +8,7 @@ use ethercat_hal::{
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use socketioxide::extract::SocketRef;
use std::sync::Arc;
use std::{pin::Pin, sync::Arc, time::Instant};
use crate::socketio::event::GenericEvent;
@@ -20,6 +20,10 @@ 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,
+55
View File
@@ -1,8 +1,11 @@
pub mod modbus_serial_interface;
use anyhow::Error;
use crc::{CRC_16_MODBUS, Crc};
use serial;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParityType {
Even,
Odd,
@@ -22,6 +25,58 @@ 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,
@@ -0,0 +1,268 @@
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